Compare commits
34 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fbf39e4932 | ||
|
|
975570fa97 | ||
|
|
d6aa0e98a4 | ||
|
|
4644cd7b0a | ||
|
|
9ddedd8dab | ||
|
|
95030a52c5 | ||
|
|
1911f72536 | ||
|
|
85217452db | ||
|
|
f7a1a2ab58 | ||
|
|
66a2006284 | ||
|
|
610b7f4464 | ||
|
|
ce12635332 | ||
|
|
2182e18f85 | ||
|
|
ad2c7dfdff | ||
|
|
cb36c9103e | ||
|
|
0a1459ee83 | ||
|
|
8896a042f0 | ||
|
|
61ad07c3f2 | ||
|
|
81a5164d99 | ||
|
|
467e80a42f | ||
|
|
58f25aa439 | ||
|
|
377c71f2e5 | ||
|
|
7cf25ac7c8 | ||
|
|
09b54570e1 | ||
|
|
73580de922 | ||
|
|
e32fd3a8fc | ||
|
|
057417c666 | ||
|
|
f1b300af70 | ||
|
|
600049bc6e | ||
|
|
6e91311489 | ||
|
|
f6a464cb3b | ||
|
|
e589c751d7 | ||
|
|
0f91af1df2 | ||
|
|
10ef04510a |
11
.github/ISSUE_TEMPLATE/bug-report.md
vendored
11
.github/ISSUE_TEMPLATE/bug-report.md
vendored
@@ -4,11 +4,18 @@ about: Create a report to help us improve
|
||||
title: ''
|
||||
labels: bug
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
<!--
|
||||
DO NOT CREATE A TOKEN LISTING REQUEST IN THIS REPOSITORY.
|
||||
YOUR ISSUE WILL BE DELETED.
|
||||
SEE https://github.com/Uniswap/default-token-list#adding-a-token
|
||||
|
||||
IF YOU NEED SUPPORT, JOIN THE DISCORD: https://discord.com/invite/EwFs3Pp
|
||||
-->
|
||||
|
||||
**Bug Description**
|
||||
A clear and concise description of what the bug is.
|
||||
A clear and concise description of the bug.
|
||||
|
||||
**Steps to Reproduce**
|
||||
1. Go to ...
|
||||
|
||||
1
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
1
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
@@ -0,0 +1 @@
|
||||
blank_issues_enabled: false
|
||||
9
.github/ISSUE_TEMPLATE/feature-request.md
vendored
9
.github/ISSUE_TEMPLATE/feature-request.md
vendored
@@ -4,9 +4,16 @@ about: Suggest an idea for this project
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
<!--
|
||||
DO NOT CREATE A TOKEN LISTING REQUEST IN THIS REPOSITORY.
|
||||
YOUR ISSUE WILL BE DELETED.
|
||||
SEE https://github.com/Uniswap/default-token-list#adding-a-token
|
||||
|
||||
IF YOU NEED SUPPORT, JOIN THE DISCORD: https://discord.com/invite/EwFs3Pp
|
||||
-->
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
|
||||
10
.github/ISSUE_TEMPLATE/something-else.md
vendored
10
.github/ISSUE_TEMPLATE/something-else.md
vendored
@@ -4,7 +4,15 @@ about: Tell us something else
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
<!--
|
||||
DO NOT CREATE A TOKEN LISTING REQUEST IN THIS REPOSITORY.
|
||||
YOUR ISSUE WILL BE DELETED.
|
||||
SEE https://github.com/Uniswap/default-token-list#adding-a-token
|
||||
|
||||
IF YOU NEED SUPPORT, JOIN THE DISCORD: https://discord.com/invite/EwFs3Pp
|
||||
-->
|
||||
|
||||
|
||||
|
||||
|
||||
2
.github/workflows/tests.yaml
vendored
2
.github/workflows/tests.yaml
vendored
@@ -29,6 +29,8 @@ jobs:
|
||||
- run: yarn install --frozen-lockfile
|
||||
- run: yarn cypress install
|
||||
- run: yarn build
|
||||
env:
|
||||
REACT_APP_NETWORK_URL: "https://mainnet.infura.io/v3/4bf032f2d38a4ed6bb975b80d6340847"
|
||||
- run: yarn integration-test
|
||||
|
||||
unit-tests:
|
||||
|
||||
@@ -3,5 +3,6 @@
|
||||
"pluginsFile": false,
|
||||
"fixturesFolder": false,
|
||||
"supportFile": "cypress/support/index.js",
|
||||
"video": false
|
||||
"video": false,
|
||||
"defaultCommandTimeout": 10000
|
||||
}
|
||||
|
||||
24
cypress/integration/lists.test.ts
Normal file
24
cypress/integration/lists.test.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
describe('Swap', () => {
|
||||
beforeEach(() => {
|
||||
cy.visit('/swap')
|
||||
})
|
||||
|
||||
it('list selection persists', () => {
|
||||
cy.get('#swap-currency-output .open-currency-select-button').click()
|
||||
cy.get('#list-introduction-choose-a-list').click()
|
||||
cy.get('#list-row-tokens-uniswap-eth .select-button').click()
|
||||
cy.reload()
|
||||
cy.get('#swap-currency-output .open-currency-select-button').click()
|
||||
cy.get('#list-introduction-choose-a-list').should('not.exist')
|
||||
})
|
||||
|
||||
it('change list', () => {
|
||||
cy.get('#swap-currency-output .open-currency-select-button').click()
|
||||
cy.get('#list-introduction-choose-a-list').click()
|
||||
cy.get('#list-row-tokens-uniswap-eth .select-button').click()
|
||||
cy.get('#currency-search-selected-list-name').should('contain', 'Uniswap')
|
||||
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')
|
||||
})
|
||||
})
|
||||
@@ -1,5 +1,7 @@
|
||||
describe('Swap', () => {
|
||||
beforeEach(() => cy.visit('/swap'))
|
||||
beforeEach(() => {
|
||||
cy.visit('/swap')
|
||||
})
|
||||
it('can enter an amount into input', () => {
|
||||
cy.get('#swap-currency-input .token-amount-input')
|
||||
.type('0.001', { delay: 200 })
|
||||
@@ -32,6 +34,8 @@ describe('Swap', () => {
|
||||
|
||||
it('can swap ETH for DAI', () => {
|
||||
cy.get('#swap-currency-output .open-currency-select-button').click()
|
||||
cy.get('#list-introduction-choose-a-list').click()
|
||||
cy.get('#list-row-tokens-uniswap-eth .select-button').click()
|
||||
cy.get('.token-item-0xc7AD46e0b8a400Bb3C915120d284AafbA8fc4735').should('be.visible')
|
||||
cy.get('.token-item-0xc7AD46e0b8a400Bb3C915120d284AafbA8fc4735').click({ force: true })
|
||||
cy.get('#swap-currency-input .token-amount-input').should('be.visible')
|
||||
@@ -41,14 +45,33 @@ describe('Swap', () => {
|
||||
cy.get('#confirm-swap-or-send').should('contain', 'Confirm Swap')
|
||||
})
|
||||
|
||||
it('add a recipient', () => {
|
||||
cy.get('#add-recipient-button').click()
|
||||
cy.get('#recipient').should('exist')
|
||||
it('add a recipient does not exist unless in expert mode', () => {
|
||||
cy.get('#add-recipient-button').should('not.exist')
|
||||
})
|
||||
|
||||
it('remove recipient', () => {
|
||||
cy.get('#add-recipient-button').click()
|
||||
cy.get('#remove-recipient-button').click()
|
||||
cy.get('#recipient').should('not.exist')
|
||||
describe('expert mode', () => {
|
||||
beforeEach(() => {
|
||||
cy.window().then(win => {
|
||||
cy.stub(win, 'prompt').returns('confirm')
|
||||
})
|
||||
cy.get('#open-settings-dialog-button').click()
|
||||
cy.get('#toggle-expert-mode-button').click()
|
||||
cy.get('#confirm-expert-mode').click()
|
||||
})
|
||||
|
||||
it('add a recipient is visible', () => {
|
||||
cy.get('#add-recipient-button').should('be.visible')
|
||||
})
|
||||
|
||||
it('add a recipient', () => {
|
||||
cy.get('#add-recipient-button').click()
|
||||
cy.get('#recipient').should('exist')
|
||||
})
|
||||
|
||||
it('remove recipient', () => {
|
||||
cy.get('#add-recipient-button').click()
|
||||
cy.get('#remove-recipient-button').click()
|
||||
cy.get('#recipient').should('not.exist')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,19 +1,17 @@
|
||||
describe('Warning', () => {
|
||||
beforeEach(() => {
|
||||
cy.clearLocalStorage()
|
||||
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.', () => {
|
||||
|
||||
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')
|
||||
})
|
||||
it('Check supression persists across sessions.', () => {
|
||||
cy.get('.token-warning-container').should('be.visible')
|
||||
cy.get('.token-dismiss-button').click()
|
||||
cy.reload()
|
||||
cy.get('.token-warning-container').should('not.be.visible')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -73,6 +73,7 @@ Cypress.Commands.overwrite('visit', (original, url, options) => {
|
||||
...options,
|
||||
onBeforeLoad(win) {
|
||||
options && options.onBeforeLoad && options.onBeforeLoad(win)
|
||||
win.localStorage.clear()
|
||||
const provider = new JsonRpcProvider('https://rinkeby.infura.io/v3/4bf032f2d38a4ed6bb975b80d6340847', 4)
|
||||
const signer = new Wallet(PRIVATE_KEY_TEST_NEVER_USE, provider)
|
||||
win.ethereum = new CustomizedBridge(signer, provider)
|
||||
|
||||
31
package.json
31
package.json
@@ -4,37 +4,30 @@
|
||||
"homepage": ".",
|
||||
"private": true,
|
||||
"devDependencies": {
|
||||
"@ethersproject/address": "5.0.0-beta.134",
|
||||
"@ethersproject/bignumber": "5.0.0-beta.138",
|
||||
"@ethersproject/constants": "5.0.0-beta.133",
|
||||
"@ethersproject/contracts": "5.0.0-beta.151",
|
||||
"@ethersproject/experimental": "5.0.0-beta.141",
|
||||
"@ethersproject/networks": "5.0.0-beta.136",
|
||||
"@ethersproject/providers": "5.0.0-beta.162",
|
||||
"@ethersproject/solidity": "5.0.2",
|
||||
"@ethersproject/strings": "5.0.0-beta.136",
|
||||
"@ethersproject/units": "5.0.0-beta.132",
|
||||
"@ethersproject/wallet": "5.0.0-beta.141",
|
||||
"@ethersproject/experimental": "^5.0.1",
|
||||
"@popperjs/core": "^2.4.4",
|
||||
"@reach/dialog": "^0.10.3",
|
||||
"@reach/portal": "^0.10.3",
|
||||
"@reduxjs/toolkit": "^1.3.5",
|
||||
"@types/jest": "^25.2.1",
|
||||
"@types/lodash.flatmap": "^4.5.6",
|
||||
"@types/multicodec": "^1.0.0",
|
||||
"@types/node": "^13.13.5",
|
||||
"@types/qs": "^6.9.2",
|
||||
"@types/react": "^16.9.34",
|
||||
"@types/react-dom": "^16.9.7",
|
||||
"@types/react-redux": "^7.1.8",
|
||||
"@types/react-router-dom": "^5.0.0",
|
||||
"@types/react-virtualized-auto-sizer": "^1.0.0",
|
||||
"@types/react-window": "^1.8.2",
|
||||
"@types/rebass": "^4.0.5",
|
||||
"@types/styled-components": "^5.1.0",
|
||||
"@types/testing-library__cypress": "^5.0.5",
|
||||
"@typescript-eslint/eslint-plugin": "^2.31.0",
|
||||
"@typescript-eslint/parser": "^2.31.0",
|
||||
"@uniswap/default-token-list": "^1.3.1",
|
||||
"@uniswap/sdk": "3.0.3-beta.1",
|
||||
"@uniswap/token-lists": "^1.0.0-beta.11",
|
||||
"@uniswap/token-lists": "^1.0.0-beta.15",
|
||||
"@uniswap/v2-core": "1.0.0",
|
||||
"@uniswap/v2-periphery": "^1.1.0-beta.0",
|
||||
"@web3-react/core": "^6.0.9",
|
||||
@@ -44,6 +37,7 @@
|
||||
"@web3-react/walletconnect-connector": "^6.1.1",
|
||||
"@web3-react/walletlink-connector": "^6.0.9",
|
||||
"ajv": "^6.12.3",
|
||||
"cids": "^1.0.0",
|
||||
"copy-to-clipboard": "^3.2.0",
|
||||
"cross-env": "^7.0.2",
|
||||
"cypress": "^4.11.0",
|
||||
@@ -52,15 +46,17 @@
|
||||
"eslint-plugin-prettier": "^3.1.3",
|
||||
"eslint-plugin-react": "^7.19.0",
|
||||
"eslint-plugin-react-hooks": "^4.0.0",
|
||||
"ethers": "^5.0.7",
|
||||
"i18next": "^15.0.9",
|
||||
"i18next-browser-languagedetector": "^3.0.1",
|
||||
"i18next-xhr-backend": "^2.0.1",
|
||||
"inter-ui": "^3.13.1",
|
||||
"jazzicon": "^1.5.0",
|
||||
"lodash.flatmap": "^4.5.0",
|
||||
"multicodec": "^2.0.0",
|
||||
"multihashes": "^3.0.1",
|
||||
"polished": "^3.3.2",
|
||||
"prettier": "^1.17.0",
|
||||
"qrcode.react": "^0.9.3",
|
||||
"qs": "^6.9.4",
|
||||
"react": "^16.13.1",
|
||||
"react-device-detect": "^1.6.2",
|
||||
@@ -74,14 +70,17 @@
|
||||
"react-scripts": "^3.4.1",
|
||||
"react-spring": "^8.0.27",
|
||||
"react-use-gesture": "^6.0.14",
|
||||
"react-virtualized-auto-sizer": "^1.0.2",
|
||||
"react-window": "^1.8.5",
|
||||
"rebass": "^4.0.7",
|
||||
"redux-localstorage-simple": "^2.2.0",
|
||||
"redux-localstorage-simple": "^2.3.1",
|
||||
"serve": "^11.3.0",
|
||||
"start-server-and-test": "^1.11.0",
|
||||
"styled-components": "^4.2.0",
|
||||
"typescript": "^3.8.3",
|
||||
"use-media": "^1.4.0"
|
||||
"typescript": "^3.8.3"
|
||||
},
|
||||
"resolutions": {
|
||||
"@walletconnect/web3-provider": "1.1.1-alpha.0"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "react-scripts start",
|
||||
|
||||
@@ -19,8 +19,8 @@
|
||||
"pending": "Đang chờ xử lý",
|
||||
"selectToken": "Chọn một đồng tiền ảo",
|
||||
"searchOrPaste": "Tìm kiếm tên, biểu tượng, hoặc địa chỉ của đồng tiền ảo",
|
||||
"searchOrPasteMobile": "Tên, Biểu tương, hoặc Địa chỉ",
|
||||
"noExchange": "Không Tìm Thấy Giao Dịch",
|
||||
"searchOrPasteMobile": "Tên, Biểu tượng, hoặc Địa chỉ",
|
||||
"noExchange": "Không tìm thấy giao dịch",
|
||||
"exchangeRate": "Tỷ giá",
|
||||
"unknownError": "Rất tiếc! Xảy ra lỗi không xác định. Vui lòng làm mới trang, hoặc truy cập từ trình duyệt hay thiết bị khác.",
|
||||
"enterValueCont": "Nhập một giá trị {{ missingCurrencyValue }} để tiếp tục.",
|
||||
@@ -29,7 +29,7 @@
|
||||
"insufficientLiquidity": "Không đủ tính thanh khoản.",
|
||||
"unlockTokenCont": "Vui lòng mở khoá đồng tiền ảo để tiếp tục",
|
||||
"transactionDetails": "Chi tiết nâng cao",
|
||||
"hideDetails": "Che giấu chi tiết",
|
||||
"hideDetails": "Ẩn chi tiết",
|
||||
"slippageWarning": "Cảnh báo trượt giá",
|
||||
"highSlippageWarning": "Cảnh báo trượt giá cao",
|
||||
"youAreSelling": "Bạn đang bán",
|
||||
@@ -59,7 +59,7 @@
|
||||
"intoPool": "vào nhóm thanh khoản.",
|
||||
"outPool": "từ nhóm thanh khoản.",
|
||||
"youWillMint": "Bạn sẽ đúc tiền",
|
||||
"liquidityTokens": "dồng thanh khoản.",
|
||||
"liquidityTokens": "đồng thanh khoản.",
|
||||
"totalSupplyIs": "Tổng cung hiện tại của đồng thanh khoản là",
|
||||
"youAreSettingExRate": "Bạn đang đặt tỷ giá hối đoái ban đầu thành",
|
||||
"totalSupplyIs0": "Tổng cung hiện tại của đồng thanh khoản là 0.",
|
||||
|
||||
BIN
src/assets/images/token-list/lists-dark.png
Normal file
BIN
src/assets/images/token-list/lists-dark.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.9 KiB |
BIN
src/assets/images/token-list/lists-light.png
Normal file
BIN
src/assets/images/token-list/lists-light.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 8.6 KiB |
@@ -40,11 +40,12 @@ export default function Transaction({ hash }: { hash: string }) {
|
||||
const { chainId } = useActiveWeb3React()
|
||||
const allTransactions = useAllTransactions()
|
||||
|
||||
const summary = allTransactions?.[hash]?.summary
|
||||
const pending = !allTransactions?.[hash]?.receipt
|
||||
const success =
|
||||
!pending &&
|
||||
(allTransactions[hash].receipt.status === 1 || typeof allTransactions[hash].receipt.status === 'undefined')
|
||||
const tx = allTransactions?.[hash]
|
||||
const summary = tx?.summary
|
||||
const pending = !tx?.receipt
|
||||
const success = !pending && tx && (tx.receipt?.status === 1 || typeof tx.receipt?.status === 'undefined')
|
||||
|
||||
if (!chainId) return null
|
||||
|
||||
return (
|
||||
<TransactionWrapper>
|
||||
|
||||
@@ -200,7 +200,7 @@ const MainWalletAction = styled(WalletAction)`
|
||||
color: ${({ theme }) => theme.primary1};
|
||||
`
|
||||
|
||||
function renderTransactions(transactions) {
|
||||
function renderTransactions(transactions: string[]) {
|
||||
return (
|
||||
<TransactionListWrapper>
|
||||
{transactions.map((hash, i) => {
|
||||
@@ -212,8 +212,8 @@ function renderTransactions(transactions) {
|
||||
|
||||
interface AccountDetailsProps {
|
||||
toggleWalletModal: () => void
|
||||
pendingTransactions: any[]
|
||||
confirmedTransactions: any[]
|
||||
pendingTransactions: string[]
|
||||
confirmedTransactions: string[]
|
||||
ENSName?: string
|
||||
openOptions: () => void
|
||||
}
|
||||
@@ -251,26 +251,26 @@ export default function AccountDetails({
|
||||
} else if (connector === walletconnect) {
|
||||
return (
|
||||
<IconWrapper size={16}>
|
||||
<img src={WalletConnectIcon} alt={''} />
|
||||
<img src={WalletConnectIcon} alt={'wallet connect logo'} />
|
||||
</IconWrapper>
|
||||
)
|
||||
} else if (connector === walletlink) {
|
||||
return (
|
||||
<IconWrapper size={16}>
|
||||
<img src={CoinbaseWalletIcon} alt={''} />
|
||||
<img src={CoinbaseWalletIcon} alt={'coinbase wallet logo'} />
|
||||
</IconWrapper>
|
||||
)
|
||||
} else if (connector === fortmatic) {
|
||||
return (
|
||||
<IconWrapper size={16}>
|
||||
<img src={FortmaticIcon} alt={''} />
|
||||
<img src={FortmaticIcon} alt={'fortmatic logo'} />
|
||||
</IconWrapper>
|
||||
)
|
||||
} else if (connector === portis) {
|
||||
return (
|
||||
<>
|
||||
<IconWrapper size={16}>
|
||||
<img src={PortisIcon} alt={''} />
|
||||
<img src={PortisIcon} alt={'portis logo'} />
|
||||
<MainWalletAction
|
||||
onClick={() => {
|
||||
portis.portis.showPortis()
|
||||
@@ -282,15 +282,12 @@ export default function AccountDetails({
|
||||
</>
|
||||
)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
const clearAllTransactionsCallback = useCallback(
|
||||
(event: React.MouseEvent) => {
|
||||
event.preventDefault()
|
||||
dispatch(clearAllTransactions({ chainId }))
|
||||
},
|
||||
[dispatch, chainId]
|
||||
)
|
||||
const clearAllTransactionsCallback = useCallback(() => {
|
||||
if (chainId) dispatch(clearAllTransactions({ chainId }))
|
||||
}, [dispatch, chainId])
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -338,7 +335,7 @@ export default function AccountDetails({
|
||||
<>
|
||||
<div>
|
||||
{getStatusIcon()}
|
||||
<p> {shortenAddress(account)}</p>
|
||||
<p> {account && shortenAddress(account)}</p>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
@@ -349,17 +346,21 @@ export default function AccountDetails({
|
||||
<>
|
||||
<AccountControl>
|
||||
<div>
|
||||
<Copy toCopy={account}>
|
||||
<span style={{ marginLeft: '4px' }}>Copy Address</span>
|
||||
</Copy>
|
||||
<AddressLink
|
||||
hasENS={!!ENSName}
|
||||
isENS={true}
|
||||
href={getEtherscanLink(chainId, ENSName, 'address')}
|
||||
>
|
||||
<LinkIcon size={16} />
|
||||
<span style={{ marginLeft: '4px' }}>View on Etherscan</span>
|
||||
</AddressLink>
|
||||
{account && (
|
||||
<Copy toCopy={account}>
|
||||
<span style={{ marginLeft: '4px' }}>Copy Address</span>
|
||||
</Copy>
|
||||
)}
|
||||
{chainId && account && (
|
||||
<AddressLink
|
||||
hasENS={!!ENSName}
|
||||
isENS={true}
|
||||
href={chainId && getEtherscanLink(chainId, ENSName, 'address')}
|
||||
>
|
||||
<LinkIcon size={16} />
|
||||
<span style={{ marginLeft: '4px' }}>View on Etherscan</span>
|
||||
</AddressLink>
|
||||
)}
|
||||
</div>
|
||||
</AccountControl>
|
||||
</>
|
||||
@@ -367,22 +368,25 @@ export default function AccountDetails({
|
||||
<>
|
||||
<AccountControl>
|
||||
<div>
|
||||
<Copy toCopy={account}>
|
||||
<span style={{ marginLeft: '4px' }}>Copy Address</span>
|
||||
</Copy>
|
||||
<AddressLink
|
||||
hasENS={!!ENSName}
|
||||
isENS={false}
|
||||
href={getEtherscanLink(chainId, account, 'address')}
|
||||
>
|
||||
<LinkIcon size={16} />
|
||||
<span style={{ marginLeft: '4px' }}>View on Etherscan</span>
|
||||
</AddressLink>
|
||||
{account && (
|
||||
<Copy toCopy={account}>
|
||||
<span style={{ marginLeft: '4px' }}>Copy Address</span>
|
||||
</Copy>
|
||||
)}
|
||||
{chainId && account && (
|
||||
<AddressLink
|
||||
hasENS={!!ENSName}
|
||||
isENS={false}
|
||||
href={getEtherscanLink(chainId, account, 'address')}
|
||||
>
|
||||
<LinkIcon size={16} />
|
||||
<span style={{ marginLeft: '4px' }}>View on Etherscan</span>
|
||||
</AddressLink>
|
||||
)}
|
||||
</div>
|
||||
</AccountControl>
|
||||
</>
|
||||
)}
|
||||
{/* {formatConnectorName()} */}
|
||||
</AccountGroupingRow>
|
||||
</InfoCard>
|
||||
</YourAccount>
|
||||
|
||||
@@ -65,11 +65,6 @@ const Input = styled.input<{ error?: boolean }>`
|
||||
}
|
||||
`
|
||||
|
||||
interface Value {
|
||||
address: string
|
||||
name?: string
|
||||
}
|
||||
|
||||
export default function AddressInputPanel({
|
||||
id,
|
||||
value,
|
||||
@@ -106,7 +101,7 @@ export default function AddressInputPanel({
|
||||
<TYPE.black color={theme.text2} fontWeight={500} fontSize={14}>
|
||||
Recipient
|
||||
</TYPE.black>
|
||||
{address && (
|
||||
{address && chainId && (
|
||||
<ExternalLink href={getEtherscanLink(chainId, name ?? address, 'address')} style={{ fontSize: '14px' }}>
|
||||
(View on Etherscan)
|
||||
</ExternalLink>
|
||||
|
||||
@@ -10,7 +10,7 @@ const Base = styled(RebassButton)<{
|
||||
padding?: string
|
||||
width?: string
|
||||
borderRadius?: string
|
||||
altDisbaledStyle?: boolean
|
||||
altDisabledStyle?: boolean
|
||||
}>`
|
||||
padding: ${({ padding }) => (padding ? padding : '18px')};
|
||||
width: ${({ width }) => (width ? width : '100%')};
|
||||
@@ -27,6 +27,8 @@ const Base = styled(RebassButton)<{
|
||||
flex-wrap: nowrap;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
&:disabled {
|
||||
cursor: auto;
|
||||
}
|
||||
@@ -51,12 +53,13 @@ export const ButtonPrimary = styled(Base)`
|
||||
background-color: ${({ theme }) => darken(0.1, theme.primary1)};
|
||||
}
|
||||
&:disabled {
|
||||
background-color: ${({ theme, altDisbaledStyle }) => (altDisbaledStyle ? theme.primary1 : theme.bg3)};
|
||||
color: ${({ theme, altDisbaledStyle }) => (altDisbaledStyle ? 'white' : theme.text3)};
|
||||
background-color: ${({ theme, altDisabledStyle }) => (altDisabledStyle ? theme.primary1 : theme.bg3)};
|
||||
color: ${({ theme, altDisabledStyle }) => (altDisabledStyle ? 'white' : theme.text3)};
|
||||
cursor: auto;
|
||||
box-shadow: none;
|
||||
border: 1px solid transparent;
|
||||
outline: none;
|
||||
opacity: ${({ altDisabledStyle }) => (altDisabledStyle ? '0.7' : '1')};
|
||||
}
|
||||
`
|
||||
|
||||
@@ -251,11 +254,15 @@ const ButtonErrorStyle = styled(Base)`
|
||||
}
|
||||
`
|
||||
|
||||
export function ButtonConfirmed({ confirmed, ...rest }: { confirmed?: boolean } & ButtonProps) {
|
||||
export function ButtonConfirmed({
|
||||
confirmed,
|
||||
altDisabledStyle,
|
||||
...rest
|
||||
}: { confirmed?: boolean; altDisabledStyle?: boolean } & ButtonProps) {
|
||||
if (confirmed) {
|
||||
return <ButtonConfirmedStyle {...rest} />
|
||||
} else {
|
||||
return <ButtonPrimary {...rest} />
|
||||
return <ButtonPrimary {...rest} altDisabledStyle={altDisabledStyle} />
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -19,11 +19,11 @@ export const LightCard = styled(Card)`
|
||||
`
|
||||
|
||||
export const GreyCard = styled(Card)`
|
||||
background-color: ${({ theme }) => theme.advancedBG};
|
||||
background-color: ${({ theme }) => theme.bg3};
|
||||
`
|
||||
|
||||
export const OutlineCard = styled(Card)`
|
||||
border: 1px solid ${({ theme }) => theme.advancedBG};
|
||||
border: 1px solid ${({ theme }) => theme.bg3};
|
||||
`
|
||||
|
||||
export const YellowCard = styled(Card)`
|
||||
|
||||
@@ -1,133 +0,0 @@
|
||||
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, Spinner } from '../../theme/components'
|
||||
import { RowBetween } from '../Row'
|
||||
import { ArrowUpCircle } from 'react-feather'
|
||||
import { ButtonPrimary } from '../Button'
|
||||
import { AutoColumn, ColumnCenter } from '../Column'
|
||||
import Circle from '../../assets/images/blue-loader.svg'
|
||||
|
||||
import { getEtherscanLink } from '../../utils'
|
||||
import { useActiveWeb3React } from '../../hooks'
|
||||
|
||||
const Wrapper = styled.div`
|
||||
width: 100%;
|
||||
`
|
||||
const Section = styled(AutoColumn)`
|
||||
padding: 24px;
|
||||
`
|
||||
|
||||
const BottomSection = styled(Section)`
|
||||
background-color: ${({ theme }) => theme.bg2};
|
||||
border-bottom-left-radius: 20px;
|
||||
border-bottom-right-radius: 20px;
|
||||
`
|
||||
|
||||
const ConfirmedIcon = styled(ColumnCenter)`
|
||||
padding: 60px 0;
|
||||
`
|
||||
|
||||
const CustomLightSpinner = styled(Spinner)<{ size: string }>`
|
||||
height: ${({ size }) => size};
|
||||
width: ${({ size }) => size};
|
||||
`
|
||||
|
||||
interface ConfirmationModalProps {
|
||||
isOpen: boolean
|
||||
onDismiss: () => void
|
||||
hash: string
|
||||
topContent: () => React.ReactChild
|
||||
bottomContent: () => React.ReactChild
|
||||
attemptingTxn: boolean
|
||||
pendingText: string
|
||||
title?: string
|
||||
}
|
||||
|
||||
export default function ConfirmationModal({
|
||||
isOpen,
|
||||
onDismiss,
|
||||
topContent,
|
||||
bottomContent,
|
||||
attemptingTxn,
|
||||
hash,
|
||||
pendingText,
|
||||
title = ''
|
||||
}: ConfirmationModalProps) {
|
||||
const { chainId } = useActiveWeb3React()
|
||||
const theme = useContext(ThemeContext)
|
||||
|
||||
const transactionBroadcast = !!hash
|
||||
|
||||
// waiting for user to confirm/reject tx _or_ showing info on a tx that has been broadcast
|
||||
if (attemptingTxn || transactionBroadcast) {
|
||||
return (
|
||||
<Modal isOpen={isOpen} onDismiss={onDismiss} maxHeight={90}>
|
||||
<Wrapper>
|
||||
<Section>
|
||||
<RowBetween>
|
||||
<div />
|
||||
<CloseIcon onClick={onDismiss} />
|
||||
</RowBetween>
|
||||
<ConfirmedIcon>
|
||||
{transactionBroadcast ? (
|
||||
<ArrowUpCircle strokeWidth={0.5} size={90} color={theme.primary1} />
|
||||
) : (
|
||||
<CustomLightSpinner src={Circle} alt="loader" size={'90px'} />
|
||||
)}
|
||||
</ConfirmedIcon>
|
||||
<AutoColumn gap="12px" justify={'center'}>
|
||||
<Text fontWeight={500} fontSize={20}>
|
||||
{transactionBroadcast ? 'Transaction Submitted' : 'Waiting For Confirmation'}
|
||||
</Text>
|
||||
<AutoColumn gap="12px" justify={'center'}>
|
||||
<Text fontWeight={600} fontSize={14} color="" textAlign="center">
|
||||
{pendingText}
|
||||
</Text>
|
||||
</AutoColumn>
|
||||
|
||||
{transactionBroadcast ? (
|
||||
<>
|
||||
<ExternalLink href={getEtherscanLink(chainId, hash, 'transaction')}>
|
||||
<Text fontWeight={500} fontSize={14} color={theme.primary1}>
|
||||
View on Etherscan
|
||||
</Text>
|
||||
</ExternalLink>
|
||||
<ButtonPrimary onClick={onDismiss} style={{ margin: '20px 0 0 0' }}>
|
||||
<Text fontWeight={500} fontSize={20}>
|
||||
Close
|
||||
</Text>
|
||||
</ButtonPrimary>
|
||||
</>
|
||||
) : (
|
||||
<Text fontSize={12} color="#565A69" textAlign="center">
|
||||
Confirm this transaction in your wallet
|
||||
</Text>
|
||||
)}
|
||||
</AutoColumn>
|
||||
</Section>
|
||||
</Wrapper>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
// confirmation screen
|
||||
return (
|
||||
<Modal isOpen={isOpen} onDismiss={onDismiss} maxHeight={90}>
|
||||
<Wrapper>
|
||||
<Section>
|
||||
<RowBetween>
|
||||
<Text fontWeight={500} fontSize={20}>
|
||||
{title}
|
||||
</Text>
|
||||
<CloseIcon onClick={onDismiss} />
|
||||
</RowBetween>
|
||||
{topContent()}
|
||||
</Section>
|
||||
<BottomSection gap="12px">{bottomContent()}</BottomSection>
|
||||
</Wrapper>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
@@ -7,7 +7,7 @@ import CurrencySearchModal from '../SearchModal/CurrencySearchModal'
|
||||
import CurrencyLogo from '../CurrencyLogo'
|
||||
import DoubleCurrencyLogo from '../DoubleLogo'
|
||||
import { RowBetween } from '../Row'
|
||||
import { TYPE, CursorPointer } from '../../theme'
|
||||
import { TYPE } from '../../theme'
|
||||
import { Input as NumericalInput } from '../NumericalInput'
|
||||
import { ReactComponent as DropDown } from '../../assets/images/dropdown.svg'
|
||||
|
||||
@@ -126,7 +126,6 @@ interface CurrencyInputPanelProps {
|
||||
hideBalance?: boolean
|
||||
pair?: Pair | null
|
||||
hideInput?: boolean
|
||||
showSendWithSwap?: boolean
|
||||
otherCurrency?: Currency | null
|
||||
id: string
|
||||
showCommonBases?: boolean
|
||||
@@ -138,14 +137,13 @@ export default function CurrencyInputPanel({
|
||||
onMax,
|
||||
showMaxButton,
|
||||
label = 'Input',
|
||||
onCurrencySelect = null,
|
||||
currency = null,
|
||||
onCurrencySelect,
|
||||
currency,
|
||||
disableCurrencySelect = false,
|
||||
hideBalance = false,
|
||||
pair = null, // used for double token logo
|
||||
hideInput = false,
|
||||
showSendWithSwap = false,
|
||||
otherCurrency = null,
|
||||
otherCurrency,
|
||||
id,
|
||||
showCommonBases
|
||||
}: CurrencyInputPanelProps) {
|
||||
@@ -153,7 +151,7 @@ export default function CurrencyInputPanel({
|
||||
|
||||
const [modalOpen, setModalOpen] = useState(false)
|
||||
const { account } = useActiveWeb3React()
|
||||
const selectedCurrencyBalance = useCurrencyBalance(account, currency)
|
||||
const selectedCurrencyBalance = useCurrencyBalance(account ?? undefined, currency ?? undefined)
|
||||
const theme = useContext(ThemeContext)
|
||||
|
||||
const handleDismissSearch = useCallback(() => {
|
||||
@@ -170,19 +168,17 @@ export default function CurrencyInputPanel({
|
||||
{label}
|
||||
</TYPE.body>
|
||||
{account && (
|
||||
<CursorPointer>
|
||||
<TYPE.body
|
||||
onClick={onMax}
|
||||
color={theme.text2}
|
||||
fontWeight={500}
|
||||
fontSize={14}
|
||||
style={{ display: 'inline' }}
|
||||
>
|
||||
{!hideBalance && !!currency && selectedCurrencyBalance
|
||||
? 'Balance: ' + selectedCurrencyBalance?.toSignificant(6)
|
||||
: ' -'}
|
||||
</TYPE.body>
|
||||
</CursorPointer>
|
||||
<TYPE.body
|
||||
onClick={onMax}
|
||||
color={theme.text2}
|
||||
fontWeight={500}
|
||||
fontSize={14}
|
||||
style={{ display: 'inline', cursor: 'pointer' }}
|
||||
>
|
||||
{!hideBalance && !!currency && selectedCurrencyBalance
|
||||
? 'Balance: ' + selectedCurrencyBalance?.toSignificant(6)
|
||||
: ' -'}
|
||||
</TYPE.body>
|
||||
)}
|
||||
</RowBetween>
|
||||
</LabelRow>
|
||||
@@ -235,13 +231,12 @@ export default function CurrencyInputPanel({
|
||||
</CurrencySelect>
|
||||
</InputRow>
|
||||
</Container>
|
||||
{!disableCurrencySelect && (
|
||||
{!disableCurrencySelect && onCurrencySelect && (
|
||||
<CurrencySearchModal
|
||||
isOpen={modalOpen}
|
||||
onDismiss={handleDismissSearch}
|
||||
onCurrencySelect={onCurrencySelect}
|
||||
showSendWithSwap={showSendWithSwap}
|
||||
hiddenCurrency={currency}
|
||||
selectedCurrency={currency}
|
||||
otherSelectedCurrency={otherCurrency}
|
||||
showCommonBases={showCommonBases}
|
||||
/>
|
||||
|
||||
@@ -1,32 +1,14 @@
|
||||
import { Currency, ETHER, Token } from '@uniswap/sdk'
|
||||
import React, { useState } from 'react'
|
||||
import React, { useMemo } from 'react'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import EthereumLogo from '../../assets/images/ethereum-logo.png'
|
||||
import useHttpLocations from '../../hooks/useHttpLocations'
|
||||
import { WrappedTokenInfo } from '../../state/lists/hooks'
|
||||
import uriToHttp from '../../utils/uriToHttp'
|
||||
import Logo from '../Logo'
|
||||
|
||||
const getTokenLogoURL = address =>
|
||||
const getTokenLogoURL = (address: string) =>
|
||||
`https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/${address}/logo.png`
|
||||
const BAD_URIS: { [tokenAddress: string]: true } = {}
|
||||
|
||||
const Image = styled.img<{ size: string }>`
|
||||
width: ${({ size }) => size};
|
||||
height: ${({ size }) => size};
|
||||
background-color: white;
|
||||
border-radius: 1rem;
|
||||
box-shadow: 0px 6px 10px rgba(0, 0, 0, 0.075);
|
||||
`
|
||||
|
||||
const Emoji = styled.span<{ size?: string }>`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: ${({ size }) => size};
|
||||
width: ${({ size }) => size};
|
||||
height: ${({ size }) => size};
|
||||
margin-bottom: -4px;
|
||||
`
|
||||
|
||||
const StyledEthereumLogo = styled.img<{ size: string }>`
|
||||
width: ${({ size }) => size};
|
||||
@@ -35,60 +17,38 @@ const StyledEthereumLogo = styled.img<{ size: string }>`
|
||||
border-radius: 24px;
|
||||
`
|
||||
|
||||
const StyledLogo = styled(Logo)<{ size: string }>`
|
||||
width: ${({ size }) => size};
|
||||
height: ${({ size }) => size};
|
||||
`
|
||||
|
||||
export default function CurrencyLogo({
|
||||
currency,
|
||||
size = '24px',
|
||||
...rest
|
||||
style
|
||||
}: {
|
||||
currency?: Currency
|
||||
size?: string
|
||||
style?: React.CSSProperties
|
||||
}) {
|
||||
const [, refresh] = useState<number>(0)
|
||||
const uriLocations = useHttpLocations(currency instanceof WrappedTokenInfo ? currency.logoURI : undefined)
|
||||
|
||||
const srcs: string[] = useMemo(() => {
|
||||
if (currency === ETHER) return []
|
||||
|
||||
if (currency instanceof Token) {
|
||||
if (currency instanceof WrappedTokenInfo) {
|
||||
return [...uriLocations, getTokenLogoURL(currency.address)]
|
||||
}
|
||||
|
||||
return [getTokenLogoURL(currency.address)]
|
||||
}
|
||||
return []
|
||||
}, [currency, uriLocations])
|
||||
|
||||
if (currency === ETHER) {
|
||||
return <StyledEthereumLogo src={EthereumLogo} size={size} {...rest} />
|
||||
return <StyledEthereumLogo src={EthereumLogo} size={size} style={style} />
|
||||
}
|
||||
|
||||
if (currency instanceof Token) {
|
||||
let uri: string | undefined
|
||||
|
||||
if (currency instanceof WrappedTokenInfo) {
|
||||
if (currency.logoURI && !BAD_URIS[currency.logoURI]) {
|
||||
uri = uriToHttp(currency.logoURI).filter(s => !BAD_URIS[s])[0]
|
||||
}
|
||||
}
|
||||
|
||||
if (!uri) {
|
||||
const defaultUri = getTokenLogoURL(currency.address)
|
||||
if (!BAD_URIS[defaultUri]) {
|
||||
uri = defaultUri
|
||||
}
|
||||
}
|
||||
|
||||
if (uri) {
|
||||
return (
|
||||
<Image
|
||||
{...rest}
|
||||
alt={`${currency.name} Logo`}
|
||||
src={uri}
|
||||
size={size}
|
||||
onError={() => {
|
||||
if (currency instanceof Token) {
|
||||
BAD_URIS[uri] = true
|
||||
}
|
||||
refresh(i => i + 1)
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Emoji {...rest} size={size}>
|
||||
<span role="img" aria-label="Thinking">
|
||||
🤔
|
||||
</span>
|
||||
</Emoji>
|
||||
)
|
||||
return <StyledLogo size={size} srcs={srcs} alt={`${currency?.symbol ?? 'token'} logo`} style={style} />
|
||||
}
|
||||
|
||||
@@ -137,7 +137,7 @@ const NETWORK_LABELS: { [chainId in ChainId]: string | null } = {
|
||||
export default function Header() {
|
||||
const { account, chainId } = useActiveWeb3React()
|
||||
|
||||
const userEthBalance = useETHBalances([account])[account]
|
||||
const userEthBalance = useETHBalances(account ? [account] : [])?.[account ?? '']
|
||||
const [isDark] = useDarkModeManager()
|
||||
|
||||
return (
|
||||
@@ -156,7 +156,7 @@ export default function Header() {
|
||||
<HeaderControls>
|
||||
<HeaderElement>
|
||||
<TestnetWrapper>
|
||||
{!isMobile && NETWORK_LABELS[chainId] && <NetworkCard>{NETWORK_LABELS[chainId]}</NetworkCard>}
|
||||
{!isMobile && chainId && NETWORK_LABELS[chainId] && <NetworkCard>{NETWORK_LABELS[chainId]}</NetworkCard>}
|
||||
</TestnetWrapper>
|
||||
<AccountElement active={!!account} style={{ pointerEvents: 'auto' }}>
|
||||
{account && userEthBalance ? (
|
||||
|
||||
@@ -5,7 +5,7 @@ import styled from 'styled-components'
|
||||
import { useActiveWeb3React } from '../../hooks'
|
||||
import Jazzicon from 'jazzicon'
|
||||
|
||||
const StyledIdenticon = styled.div`
|
||||
const StyledIdenticonContainer = styled.div`
|
||||
height: 1rem;
|
||||
width: 1rem;
|
||||
border-radius: 1.125rem;
|
||||
@@ -24,5 +24,6 @@ export default function Identicon() {
|
||||
}
|
||||
}, [account])
|
||||
|
||||
return <StyledIdenticon ref={ref} />
|
||||
// https://github.com/DefinitelyTyped/DefinitelyTyped/issues/30451
|
||||
return <StyledIdenticonContainer ref={ref as any} />
|
||||
}
|
||||
|
||||
26
src/components/ListLogo/index.tsx
Normal file
26
src/components/ListLogo/index.tsx
Normal file
@@ -0,0 +1,26 @@
|
||||
import React from 'react'
|
||||
import styled from 'styled-components'
|
||||
import useHttpLocations from '../../hooks/useHttpLocations'
|
||||
|
||||
import Logo from '../Logo'
|
||||
|
||||
const StyledListLogo = styled(Logo)<{ size: string }>`
|
||||
width: ${({ size }) => size};
|
||||
height: ${({ size }) => size};
|
||||
`
|
||||
|
||||
export default function ListLogo({
|
||||
logoURI,
|
||||
style,
|
||||
size = '24px',
|
||||
alt
|
||||
}: {
|
||||
logoURI: string
|
||||
size?: string
|
||||
style?: React.CSSProperties
|
||||
alt?: string
|
||||
}) {
|
||||
const srcs: string[] = useHttpLocations(logoURI)
|
||||
|
||||
return <StyledListLogo alt={alt} size={size} srcs={srcs} style={style} />
|
||||
}
|
||||
@@ -24,7 +24,7 @@ const StyledSVG = styled.svg<{ size: string; stroke?: string }>`
|
||||
* Takes in custom size and stroke for circle color, default to primary color as fill,
|
||||
* need ...rest for layered styles on top
|
||||
*/
|
||||
export default function Loader({ size = '16px', stroke = null, ...rest }: { size?: string; stroke?: string }) {
|
||||
export default function Loader({ size = '16px', stroke, ...rest }: { size?: string; stroke?: string }) {
|
||||
return (
|
||||
<StyledSVG viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" size={size} stroke={stroke} {...rest}>
|
||||
<path
|
||||
|
||||
34
src/components/Logo/index.tsx
Normal file
34
src/components/Logo/index.tsx
Normal file
@@ -0,0 +1,34 @@
|
||||
import React, { useState } from 'react'
|
||||
import { HelpCircle } from 'react-feather'
|
||||
import { ImageProps } from 'rebass'
|
||||
|
||||
const BAD_SRCS: { [tokenAddress: string]: true } = {}
|
||||
|
||||
export interface LogoProps extends Pick<ImageProps, 'style' | 'alt' | 'className'> {
|
||||
srcs: string[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders an image by sequentially trying a list of URIs, and then eventually a fallback triangle alert
|
||||
*/
|
||||
export default function Logo({ srcs, alt, ...rest }: LogoProps) {
|
||||
const [, refresh] = useState<number>(0)
|
||||
|
||||
const src: string | undefined = srcs.find(src => !BAD_SRCS[src])
|
||||
|
||||
if (src) {
|
||||
return (
|
||||
<img
|
||||
{...rest}
|
||||
alt={alt}
|
||||
src={src}
|
||||
onError={() => {
|
||||
if (src) BAD_SRCS[src] = true
|
||||
refresh(i => i + 1)
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return <HelpCircle {...rest} />
|
||||
}
|
||||
@@ -1,7 +1,8 @@
|
||||
import React, { useRef, useEffect } from 'react'
|
||||
import React, { useRef } from 'react'
|
||||
import { Info, BookOpen, Code, PieChart, MessageCircle } from 'react-feather'
|
||||
import styled from 'styled-components'
|
||||
import { ReactComponent as MenuIcon } from '../../assets/images/menu.svg'
|
||||
import { useOnClickOutside } from '../../hooks/useOnClickOutside'
|
||||
import useToggle from '../../hooks/useToggle'
|
||||
|
||||
import { ExternalLink } from '../../theme'
|
||||
@@ -83,27 +84,11 @@ export default function Menu() {
|
||||
const node = useRef<HTMLDivElement>()
|
||||
const [open, toggle] = useToggle(false)
|
||||
|
||||
useEffect(() => {
|
||||
const handleClickOutside = e => {
|
||||
if (node.current?.contains(e.target) ?? false) {
|
||||
return
|
||||
}
|
||||
toggle()
|
||||
}
|
||||
|
||||
if (open) {
|
||||
document.addEventListener('mousedown', handleClickOutside)
|
||||
} else {
|
||||
document.removeEventListener('mousedown', handleClickOutside)
|
||||
}
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('mousedown', handleClickOutside)
|
||||
}
|
||||
}, [open, toggle])
|
||||
useOnClickOutside(node, open ? toggle : undefined)
|
||||
|
||||
return (
|
||||
<StyledMenu ref={node}>
|
||||
// https://github.com/DefinitelyTyped/DefinitelyTyped/issues/30451
|
||||
<StyledMenu ref={node as any}>
|
||||
<StyledMenuButton onClick={toggle}>
|
||||
<StyledMenuIcon />
|
||||
</StyledMenuButton>
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import React from 'react'
|
||||
import styled, { css } from 'styled-components'
|
||||
import { animated, useTransition, useSpring } from 'react-spring'
|
||||
import { Spring } from 'react-spring/renderprops'
|
||||
|
||||
import { DialogOverlay, DialogContent } from '@reach/dialog'
|
||||
import { isMobile } from 'react-device-detect'
|
||||
import '@reach/dialog/styles.css'
|
||||
@@ -11,39 +9,25 @@ import { useGesture } from 'react-use-gesture'
|
||||
|
||||
const AnimatedDialogOverlay = animated(DialogOverlay)
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const StyledDialogOverlay = styled(({ mobile, ...rest }) => <AnimatedDialogOverlay {...rest} />)<{ mobile: boolean }>`
|
||||
const StyledDialogOverlay = styled(AnimatedDialogOverlay)`
|
||||
&[data-reach-dialog-overlay] {
|
||||
z-index: 2;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: transparent;
|
||||
overflow: hidden;
|
||||
|
||||
${({ mobile }) =>
|
||||
mobile &&
|
||||
css`
|
||||
align-items: flex-end;
|
||||
`}
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
background-color: ${({ theme }) => theme.modalBG};
|
||||
opacity: 0.5;
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
position: fixed;
|
||||
z-index: -1;
|
||||
}
|
||||
background-color: ${({ theme }) => theme.modalBG};
|
||||
}
|
||||
`
|
||||
|
||||
const AnimatedDialogContent = animated(DialogContent)
|
||||
// destructure to not pass custom props to Dialog DOM element
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const StyledDialogContent = styled(({ minHeight, maxHeight, mobile, isOpen, ...rest }) => (
|
||||
<DialogContent {...rest} />
|
||||
<AnimatedDialogContent {...rest} />
|
||||
)).attrs({
|
||||
'aria-label': 'dialog'
|
||||
})`
|
||||
@@ -54,6 +38,9 @@ const StyledDialogContent = styled(({ minHeight, maxHeight, mobile, isOpen, ...r
|
||||
box-shadow: 0 4px 8px 0 ${({ theme }) => transparentize(0.95, theme.shadow1)};
|
||||
padding: 0px;
|
||||
width: 50vw;
|
||||
overflow: hidden;
|
||||
|
||||
align-self: ${({ mobile }) => (mobile ? 'flex-end' : 'center')};
|
||||
|
||||
max-width: 420px;
|
||||
${({ maxHeight }) =>
|
||||
@@ -99,10 +86,10 @@ export default function Modal({
|
||||
onDismiss,
|
||||
minHeight = false,
|
||||
maxHeight = 50,
|
||||
initialFocusRef = null,
|
||||
initialFocusRef,
|
||||
children
|
||||
}: ModalProps) {
|
||||
const transitions = useTransition(isOpen, null, {
|
||||
const fadeTransition = useTransition(isOpen, null, {
|
||||
config: { duration: 200 },
|
||||
from: { opacity: 0 },
|
||||
enter: { opacity: 1 },
|
||||
@@ -115,80 +102,37 @@ export default function Modal({
|
||||
set({
|
||||
y: state.down ? state.movement[1] : 0
|
||||
})
|
||||
if (state.velocity > 3 && state.direction[1] > 0) {
|
||||
if (state.movement[1] > 300 || (state.velocity > 3 && state.direction[1] > 0)) {
|
||||
onDismiss()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
if (isMobile) {
|
||||
return (
|
||||
<>
|
||||
{transitions.map(
|
||||
({ item, key, props }) =>
|
||||
item && (
|
||||
<StyledDialogOverlay
|
||||
key={key}
|
||||
style={props}
|
||||
onDismiss={onDismiss}
|
||||
initialFocusRef={initialFocusRef}
|
||||
mobile={true}
|
||||
return (
|
||||
<>
|
||||
{fadeTransition.map(
|
||||
({ item, key, props }) =>
|
||||
item && (
|
||||
<StyledDialogOverlay key={key} style={props} onDismiss={onDismiss} initialFocusRef={initialFocusRef}>
|
||||
<StyledDialogContent
|
||||
{...(isMobile
|
||||
? {
|
||||
...bind(),
|
||||
style: { transform: y.interpolate(y => `translateY(${y > 0 ? y : 0}px)`) }
|
||||
}
|
||||
: {})}
|
||||
aria-label="dialog content"
|
||||
minHeight={minHeight}
|
||||
maxHeight={maxHeight}
|
||||
mobile={isMobile}
|
||||
>
|
||||
{/* prevents the automatic focusing of inputs on mobile by the reach dialog */}
|
||||
{initialFocusRef ? null : <div tabIndex={1} />}
|
||||
<Spring // animation for entrance and exit
|
||||
from={{
|
||||
transform: isOpen ? 'translateY(200px)' : 'translateY(100px)'
|
||||
}}
|
||||
to={{
|
||||
transform: isOpen ? 'translateY(0px)' : 'translateY(200px)'
|
||||
}}
|
||||
>
|
||||
{props => (
|
||||
<animated.div
|
||||
{...bind()}
|
||||
style={{
|
||||
transform: y.interpolate(y => `translateY(${y > 0 ? y : 0}px)`)
|
||||
}}
|
||||
>
|
||||
<StyledDialogContent
|
||||
aria-label="dialog content"
|
||||
style={props}
|
||||
hidden={true}
|
||||
minHeight={minHeight}
|
||||
maxHeight={maxHeight}
|
||||
mobile={isMobile}
|
||||
>
|
||||
{children}
|
||||
</StyledDialogContent>
|
||||
</animated.div>
|
||||
)}
|
||||
</Spring>
|
||||
</StyledDialogOverlay>
|
||||
)
|
||||
)}
|
||||
</>
|
||||
)
|
||||
} else {
|
||||
return (
|
||||
<>
|
||||
{transitions.map(
|
||||
({ item, key, props }) =>
|
||||
item && (
|
||||
<StyledDialogOverlay key={key} style={props} onDismiss={onDismiss} initialFocusRef={initialFocusRef}>
|
||||
<StyledDialogContent
|
||||
aria-label="dialog content"
|
||||
hidden={true}
|
||||
minHeight={minHeight}
|
||||
maxHeight={maxHeight}
|
||||
isOpen={isOpen}
|
||||
>
|
||||
{children}
|
||||
</StyledDialogContent>
|
||||
</StyledDialogOverlay>
|
||||
)
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
{!initialFocusRef && isMobile ? <div tabIndex={1} /> : null}
|
||||
{children}
|
||||
</StyledDialogContent>
|
||||
</StyledDialogOverlay>
|
||||
)
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -46,7 +46,7 @@ export const Input = React.memo(function InnerInput({
|
||||
...rest
|
||||
}: {
|
||||
value: string | number
|
||||
onUserInput: (string) => void
|
||||
onUserInput: (input: string) => void
|
||||
error?: boolean
|
||||
fontSize?: string
|
||||
align?: 'right' | 'left'
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Placement } from '@popperjs/core'
|
||||
import { transparentize } from 'polished'
|
||||
import React, { useState } from 'react'
|
||||
import React, { useCallback, useState } from 'react'
|
||||
import { usePopper } from 'react-popper'
|
||||
import styled from 'styled-components'
|
||||
import useInterval from '../../hooks/useInterval'
|
||||
@@ -83,9 +83,9 @@ export interface PopoverProps {
|
||||
}
|
||||
|
||||
export default function Popover({ content, show, children, placement = 'auto' }: PopoverProps) {
|
||||
const [referenceElement, setReferenceElement] = useState<HTMLDivElement>(null)
|
||||
const [popperElement, setPopperElement] = useState<HTMLDivElement>(null)
|
||||
const [arrowElement, setArrowElement] = useState<HTMLDivElement>(null)
|
||||
const [referenceElement, setReferenceElement] = useState<HTMLDivElement | null>(null)
|
||||
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null)
|
||||
const [arrowElement, setArrowElement] = useState<HTMLDivElement | null>(null)
|
||||
const { styles, update, attributes } = usePopper(referenceElement, popperElement, {
|
||||
placement,
|
||||
strategy: 'fixed',
|
||||
@@ -94,17 +94,20 @@ export default function Popover({ content, show, children, placement = 'auto' }:
|
||||
{ name: 'arrow', options: { element: arrowElement } }
|
||||
]
|
||||
})
|
||||
useInterval(update, show ? 100 : null)
|
||||
const updateCallback = useCallback(() => {
|
||||
update && update()
|
||||
}, [update])
|
||||
useInterval(updateCallback, show ? 100 : null)
|
||||
|
||||
return (
|
||||
<>
|
||||
<ReferenceElement ref={setReferenceElement}>{children}</ReferenceElement>
|
||||
<ReferenceElement ref={setReferenceElement as any}>{children}</ReferenceElement>
|
||||
<Portal>
|
||||
<PopoverContainer show={show} ref={setPopperElement} style={styles.popper} {...attributes.popper}>
|
||||
<PopoverContainer show={show} ref={setPopperElement as any} style={styles.popper} {...attributes.popper}>
|
||||
{content}
|
||||
<Arrow
|
||||
className={`arrow-${attributes.popper?.['data-popper-placement'] ?? ''}`}
|
||||
ref={setArrowElement}
|
||||
ref={setArrowElement as any}
|
||||
style={styles.arrow}
|
||||
{...attributes.arrow}
|
||||
/>
|
||||
|
||||
@@ -1,20 +1,17 @@
|
||||
import { TokenList, Version } from '@uniswap/token-lists'
|
||||
import React, { useCallback, useContext } from 'react'
|
||||
import { AlertCircle, Info } from 'react-feather'
|
||||
import { diffTokenLists, TokenList } from '@uniswap/token-lists'
|
||||
import React, { useCallback, useMemo } from 'react'
|
||||
import ReactGA from 'react-ga'
|
||||
import { useDispatch } from 'react-redux'
|
||||
import { ThemeContext } from 'styled-components'
|
||||
import { Text } from 'rebass'
|
||||
import { AppDispatch } from '../../state'
|
||||
import { useRemovePopup } from '../../state/application/hooks'
|
||||
import { acceptListUpdate } from '../../state/lists/actions'
|
||||
import { TYPE } from '../../theme'
|
||||
import { ButtonPrimary, ButtonSecondary } from '../Button'
|
||||
import listVersionLabel from '../../utils/listVersionLabel'
|
||||
import { ButtonSecondary } from '../Button'
|
||||
import { AutoColumn } from '../Column'
|
||||
import { AutoRow } from '../Row'
|
||||
|
||||
function versionLabel(version: Version): string {
|
||||
return `v${version.major}.${version.minor}.${version.patch}`
|
||||
}
|
||||
|
||||
export default function ListUpdatePopup({
|
||||
popKey,
|
||||
listUrl,
|
||||
@@ -31,34 +28,69 @@ export default function ListUpdatePopup({
|
||||
const removePopup = useRemovePopup()
|
||||
const removeThisPopup = useCallback(() => removePopup(popKey), [popKey, removePopup])
|
||||
const dispatch = useDispatch<AppDispatch>()
|
||||
const theme = useContext(ThemeContext)
|
||||
|
||||
const updateList = useCallback(() => {
|
||||
const handleAcceptUpdate = useCallback(() => {
|
||||
if (auto) return
|
||||
ReactGA.event({
|
||||
category: 'Lists',
|
||||
action: 'Update List from Popup',
|
||||
label: listUrl
|
||||
})
|
||||
dispatch(acceptListUpdate(listUrl))
|
||||
removeThisPopup()
|
||||
}, [auto, dispatch, listUrl, removeThisPopup])
|
||||
|
||||
const { added: tokensAdded, changed: tokensChanged, removed: tokensRemoved } = useMemo(() => {
|
||||
return diffTokenLists(oldList.tokens, newList.tokens)
|
||||
}, [newList.tokens, oldList.tokens])
|
||||
const numTokensChanged = useMemo(
|
||||
() =>
|
||||
Object.keys(tokensChanged).reduce((memo, chainId: any) => memo + Object.keys(tokensChanged[chainId]).length, 0),
|
||||
[tokensChanged]
|
||||
)
|
||||
|
||||
return (
|
||||
<AutoRow>
|
||||
<div style={{ paddingRight: 16 }}>
|
||||
{auto ? <Info color={theme.text2} size={24} /> : <AlertCircle color={theme.red1} size={24} />}{' '}
|
||||
</div>
|
||||
<AutoColumn style={{ flex: '1' }} gap="8px">
|
||||
{auto ? (
|
||||
<TYPE.body fontWeight={500}>
|
||||
The token list "{oldList.name}" has been updated to{' '}
|
||||
<strong>{versionLabel(newList.version)}</strong>.
|
||||
<strong>{listVersionLabel(newList.version)}</strong>.
|
||||
</TYPE.body>
|
||||
) : (
|
||||
<>
|
||||
<div>
|
||||
A token list update is available for the list "{oldList.name}" ({versionLabel(oldList.version)}{' '}
|
||||
to {versionLabel(newList.version)}).
|
||||
<Text>
|
||||
An update is available for the token list "{oldList.name}" (
|
||||
{listVersionLabel(oldList.version)} to {listVersionLabel(newList.version)}).
|
||||
</Text>
|
||||
<ul>
|
||||
{tokensAdded.length > 0 ? (
|
||||
<li>
|
||||
{tokensAdded.map(token => (
|
||||
<strong key={`${token.chainId}-${token.address}`} title={token.address}>
|
||||
{token.symbol}
|
||||
</strong>
|
||||
))}{' '}
|
||||
added
|
||||
</li>
|
||||
) : null}
|
||||
{tokensRemoved.length > 0 ? (
|
||||
<li>
|
||||
{tokensRemoved.map(token => (
|
||||
<strong key={`${token.chainId}-${token.address}`} title={token.address}>
|
||||
{token.symbol}
|
||||
</strong>
|
||||
))}{' '}
|
||||
removed
|
||||
</li>
|
||||
) : null}
|
||||
{numTokensChanged > 0 ? <li>{numTokensChanged} tokens updated</li> : null}
|
||||
</ul>
|
||||
</div>
|
||||
<AutoRow>
|
||||
<div style={{ flexGrow: 1, marginRight: 6 }}>
|
||||
<ButtonPrimary onClick={updateList}>Update list</ButtonPrimary>
|
||||
<div style={{ flexGrow: 1, marginRight: 12 }}>
|
||||
<ButtonSecondary onClick={handleAcceptUpdate}>Accept update</ButtonSecondary>
|
||||
</div>
|
||||
<div style={{ flexGrow: 1 }}>
|
||||
<ButtonSecondary onClick={removeThisPopup}>Dismiss</ButtonSecondary>
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import React, { useCallback, useContext, useState } from 'react'
|
||||
import React, { useCallback, useContext, useEffect } from 'react'
|
||||
import { X } from 'react-feather'
|
||||
import { useSpring } from 'react-spring/web'
|
||||
import styled, { ThemeContext } from 'styled-components'
|
||||
import useInterval from '../../hooks/useInterval'
|
||||
import { animated } from 'react-spring'
|
||||
import { PopupContent } from '../../state/application/actions'
|
||||
import { useRemovePopup } from '../../state/application/hooks'
|
||||
import ListUpdatePopup from './ListUpdatePopup'
|
||||
import TxnPopup from './TxnPopup'
|
||||
import TransactionPopup from './TransactionPopup'
|
||||
|
||||
export const StyledClose = styled(X)`
|
||||
position: absolute;
|
||||
@@ -25,50 +26,54 @@ export const Popup = styled.div`
|
||||
border-radius: 10px;
|
||||
padding: 20px;
|
||||
padding-right: 35px;
|
||||
z-index: 2;
|
||||
overflow: hidden;
|
||||
|
||||
${({ theme }) => theme.mediaWidth.upToSmall`
|
||||
min-width: 290px;
|
||||
`}
|
||||
`
|
||||
const DELAY = 100
|
||||
const Fader = styled.div<{ count: number }>`
|
||||
const Fader = styled.div`
|
||||
position: absolute;
|
||||
bottom: 0px;
|
||||
left: 0px;
|
||||
width: ${({ count }) => `calc(100% - (100% / ${150 / count}))`};
|
||||
width: 100%;
|
||||
height: 2px;
|
||||
background-color: ${({ theme }) => theme.bg3};
|
||||
transition: width 100ms linear;
|
||||
`
|
||||
|
||||
export default function PopupItem({ content, popKey }: { content: PopupContent; popKey: string }) {
|
||||
const [count, setCount] = useState(1)
|
||||
const AnimatedFader = animated(Fader)
|
||||
|
||||
const [isRunning, setIsRunning] = useState(true)
|
||||
export default function PopupItem({
|
||||
removeAfterMs,
|
||||
content,
|
||||
popKey
|
||||
}: {
|
||||
removeAfterMs: number | null
|
||||
content: PopupContent
|
||||
popKey: string
|
||||
}) {
|
||||
const removePopup = useRemovePopup()
|
||||
|
||||
const removeThisPopup = useCallback(() => removePopup(popKey), [popKey, removePopup])
|
||||
useEffect(() => {
|
||||
if (removeAfterMs === null) return undefined
|
||||
|
||||
useInterval(
|
||||
() => {
|
||||
count > 150 ? removeThisPopup() : setCount(count + 1)
|
||||
},
|
||||
isRunning ? DELAY : null
|
||||
)
|
||||
const timeout = setTimeout(() => {
|
||||
removeThisPopup()
|
||||
}, removeAfterMs)
|
||||
|
||||
return () => {
|
||||
clearTimeout(timeout)
|
||||
}
|
||||
}, [removeAfterMs, removeThisPopup])
|
||||
|
||||
const theme = useContext(ThemeContext)
|
||||
|
||||
const handleMouseEnter = useCallback(() => setIsRunning(false), [])
|
||||
const handleMouseLeave = useCallback(() => setIsRunning(true), [])
|
||||
|
||||
let popupContent
|
||||
if ('txn' in content) {
|
||||
const {
|
||||
txn: { hash, success, summary }
|
||||
} = content
|
||||
popupContent = <TxnPopup hash={hash} success={success} summary={summary} />
|
||||
popupContent = <TransactionPopup hash={hash} success={success} summary={summary} />
|
||||
} else if ('listUpdate' in content) {
|
||||
const {
|
||||
listUpdate: { listUrl, oldList, newList, auto }
|
||||
@@ -76,11 +81,17 @@ export default function PopupItem({ content, popKey }: { content: PopupContent;
|
||||
popupContent = <ListUpdatePopup popKey={popKey} listUrl={listUrl} oldList={oldList} newList={newList} auto={auto} />
|
||||
}
|
||||
|
||||
const faderStyle = useSpring({
|
||||
from: { width: '100%' },
|
||||
to: { width: '0%' },
|
||||
config: { duration: removeAfterMs ?? undefined }
|
||||
})
|
||||
|
||||
return (
|
||||
<Popup onMouseEnter={handleMouseEnter} onMouseLeave={handleMouseLeave}>
|
||||
<StyledClose color={theme.text2} onClick={() => removePopup(popKey)} />
|
||||
<Popup>
|
||||
<StyledClose color={theme.text2} onClick={removeThisPopup} />
|
||||
{popupContent}
|
||||
<Fader count={count} />
|
||||
{removeAfterMs !== null ? <AnimatedFader style={faderStyle} /> : null}
|
||||
</Popup>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useContext } from 'react'
|
||||
import { AlertCircle, CheckCircle } from 'react-feather'
|
||||
import { ThemeContext } from 'styled-components'
|
||||
import styled, { ThemeContext } from 'styled-components'
|
||||
import { useActiveWeb3React } from '../../hooks'
|
||||
import { TYPE } from '../../theme'
|
||||
import { ExternalLink } from '../../theme/components'
|
||||
@@ -8,20 +8,34 @@ import { getEtherscanLink } from '../../utils'
|
||||
import { AutoColumn } from '../Column'
|
||||
import { AutoRow } from '../Row'
|
||||
|
||||
export default function TxnPopup({ hash, success, summary }: { hash: string; success?: boolean; summary?: string }) {
|
||||
const RowNoFlex = styled(AutoRow)`
|
||||
flex-wrap: nowrap;
|
||||
`
|
||||
|
||||
export default function TransactionPopup({
|
||||
hash,
|
||||
success,
|
||||
summary
|
||||
}: {
|
||||
hash: string
|
||||
success?: boolean
|
||||
summary?: string
|
||||
}) {
|
||||
const { chainId } = useActiveWeb3React()
|
||||
|
||||
const theme = useContext(ThemeContext)
|
||||
|
||||
return (
|
||||
<AutoRow>
|
||||
<RowNoFlex>
|
||||
<div style={{ paddingRight: 16 }}>
|
||||
{success ? <CheckCircle color={theme.green1} size={24} /> : <AlertCircle color={theme.red1} size={24} />}
|
||||
</div>
|
||||
<AutoColumn gap="8px">
|
||||
<TYPE.body fontWeight={500}>{summary ?? 'Hash: ' + hash.slice(0, 8) + '...' + hash.slice(58, 65)}</TYPE.body>
|
||||
<ExternalLink href={getEtherscanLink(chainId, hash, 'transaction')}>View on Etherscan</ExternalLink>
|
||||
{chainId && (
|
||||
<ExternalLink href={getEtherscanLink(chainId, hash, 'transaction')}>View on Etherscan</ExternalLink>
|
||||
)}
|
||||
</AutoColumn>
|
||||
</AutoRow>
|
||||
</RowNoFlex>
|
||||
)
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
import React from 'react'
|
||||
import styled from 'styled-components'
|
||||
import { useMediaLayout } from 'use-media'
|
||||
import { useActivePopups } from '../../state/application/hooks'
|
||||
import { AutoColumn } from '../Column'
|
||||
import PopupItem from './PopupItem'
|
||||
@@ -11,6 +10,11 @@ const MobilePopupWrapper = styled.div<{ height: string | number }>`
|
||||
height: ${({ height }) => height};
|
||||
margin: ${({ height }) => (height ? '0 auto;' : 0)};
|
||||
margin-bottom: ${({ height }) => (height ? '20px' : 0)}};
|
||||
|
||||
display: none;
|
||||
${({ theme }) => theme.mediaWidth.upToSmall`
|
||||
display: block;
|
||||
`};
|
||||
`
|
||||
|
||||
const MobilePopupInner = styled.div`
|
||||
@@ -26,11 +30,12 @@ const MobilePopupInner = styled.div`
|
||||
`
|
||||
|
||||
const FixedPopupColumn = styled(AutoColumn)`
|
||||
position: absolute;
|
||||
top: 112px;
|
||||
position: fixed;
|
||||
top: 64px;
|
||||
right: 1rem;
|
||||
max-width: 355px !important;
|
||||
width: 100%;
|
||||
z-index: 2;
|
||||
|
||||
${({ theme }) => theme.mediaWidth.upToSmall`
|
||||
display: none;
|
||||
@@ -41,30 +46,23 @@ export default function Popups() {
|
||||
// get all popups
|
||||
const activePopups = useActivePopups()
|
||||
|
||||
// switch view settings on mobile
|
||||
const isMobile = useMediaLayout({ maxWidth: '600px' })
|
||||
|
||||
if (!isMobile) {
|
||||
return (
|
||||
return (
|
||||
<>
|
||||
<FixedPopupColumn gap="20px">
|
||||
{activePopups.map(item => (
|
||||
<PopupItem key={item.key} content={item.content} popKey={item.key} />
|
||||
<PopupItem key={item.key} content={item.content} popKey={item.key} removeAfterMs={item.removeAfterMs} />
|
||||
))}
|
||||
</FixedPopupColumn>
|
||||
)
|
||||
}
|
||||
//mobile
|
||||
else
|
||||
return (
|
||||
<MobilePopupWrapper height={activePopups?.length > 0 ? 'fit-content' : 0}>
|
||||
<MobilePopupInner>
|
||||
{activePopups // reverse so new items up front
|
||||
.slice(0)
|
||||
.reverse()
|
||||
.map(item => (
|
||||
<PopupItem key={item.key} content={item.content} popKey={item.key} />
|
||||
<PopupItem key={item.key} content={item.content} popKey={item.key} removeAfterMs={item.removeAfterMs} />
|
||||
))}
|
||||
</MobilePopupInner>
|
||||
</MobilePopupWrapper>
|
||||
)
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ function V1PositionCard({ token, V1LiquidityBalance }: PositionCardProps) {
|
||||
<RowFixed>
|
||||
<DoubleCurrencyLogo currency0={token} margin={true} size={20} />
|
||||
<Text fontWeight={500} fontSize={20} style={{ marginLeft: '' }}>
|
||||
{`${token.equals(WETH[chainId]) ? 'WETH' : token.symbol}/ETH`}
|
||||
{`${chainId && token.equals(WETH[chainId]) ? 'WETH' : token.symbol}/ETH`}
|
||||
</Text>
|
||||
<Text
|
||||
fontSize={12}
|
||||
|
||||
@@ -46,7 +46,7 @@ export function MinimalPositionCard({ pair, showUnwrapped = false, border }: Pos
|
||||
|
||||
const [showMore, setShowMore] = useState(false)
|
||||
|
||||
const userPoolBalance = useTokenBalance(account, pair.liquidityToken)
|
||||
const userPoolBalance = useTokenBalance(account ?? undefined, pair.liquidityToken)
|
||||
const totalPoolTokens = useTotalSupply(pair.liquidityToken)
|
||||
|
||||
const [token0Deposited, token1Deposited] =
|
||||
@@ -131,7 +131,7 @@ export default function FullPositionCard({ pair, border }: PositionCardProps) {
|
||||
|
||||
const [showMore, setShowMore] = useState(false)
|
||||
|
||||
const userPoolBalance = useTokenBalance(account, pair.liquidityToken)
|
||||
const userPoolBalance = useTokenBalance(account ?? undefined, pair.liquidityToken)
|
||||
const totalPoolTokens = useTotalSupply(pair.liquidityToken)
|
||||
|
||||
const poolTokenPercentage =
|
||||
|
||||
79
src/components/ProgressSteps/index.tsx
Normal file
79
src/components/ProgressSteps/index.tsx
Normal file
@@ -0,0 +1,79 @@
|
||||
import React from 'react'
|
||||
import styled from 'styled-components'
|
||||
import { RowBetween } from '../Row'
|
||||
import { AutoColumn } from '../Column'
|
||||
import { transparentize } from 'polished'
|
||||
|
||||
const Wrapper = styled(AutoColumn)`
|
||||
margin-top: 1.25rem;
|
||||
`
|
||||
|
||||
const Grouping = styled(RowBetween)`
|
||||
width: 50%;
|
||||
`
|
||||
|
||||
const Circle = styled.div<{ confirmed?: boolean; disabled?: boolean }>`
|
||||
min-width: 20px;
|
||||
min-height: 20px;
|
||||
background-color: ${({ theme, confirmed, disabled }) =>
|
||||
disabled ? theme.bg4 : confirmed ? theme.green1 : theme.primary1};
|
||||
border-radius: 50%;
|
||||
color: ${({ theme }) => theme.white};
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
line-height: 8px;
|
||||
font-size: 12px;
|
||||
`
|
||||
|
||||
const CircleRow = styled.div`
|
||||
width: calc(100% - 20px);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
`
|
||||
|
||||
const Connector = styled.div<{ prevConfirmed?: boolean }>`
|
||||
width: 100%;
|
||||
height: 2px;
|
||||
background-color: ;
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
${({ theme, prevConfirmed }) => transparentize(0.5, prevConfirmed ? theme.green1 : theme.primary1)} 0%,
|
||||
${({ theme, prevConfirmed }) => (prevConfirmed ? theme.primary1 : theme.bg4)} 80%
|
||||
);
|
||||
opacity: 0.6;
|
||||
`
|
||||
|
||||
interface ProgressCirclesProps {
|
||||
steps: boolean[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Based on array of steps, create a step counter of circles.
|
||||
* A circle can be enabled, disabled, or confirmed. States are derived
|
||||
* from previous step.
|
||||
*
|
||||
* An extra circle is added to represent the ability to swap, add, or remove.
|
||||
* This step will never be marked as complete (because no 'txn done' state in body ui).
|
||||
*
|
||||
* @param steps array of booleans where true means step is complete
|
||||
*/
|
||||
export default function ProgressCircles({ steps }: ProgressCirclesProps) {
|
||||
return (
|
||||
<Wrapper justify={'center'}>
|
||||
<Grouping>
|
||||
{steps.map((step, i) => {
|
||||
return (
|
||||
<CircleRow key={i}>
|
||||
<Circle confirmed={step} disabled={!steps[i - 1] && i !== 0}>
|
||||
{step ? '✓' : i + 1}
|
||||
</Circle>
|
||||
<Connector prevConfirmed={step} />
|
||||
</CircleRow>
|
||||
)
|
||||
})}
|
||||
<Circle disabled={!steps[steps.length - 1]}>{steps.length + 1}</Circle>
|
||||
</Grouping>
|
||||
</Wrapper>
|
||||
)
|
||||
}
|
||||
@@ -22,7 +22,7 @@ const QuestionWrapper = styled.div`
|
||||
}
|
||||
`
|
||||
|
||||
export default function QuestionHelper({ text, disabled }: { text: string; disabled?: boolean }) {
|
||||
export default function QuestionHelper({ text }: { text: string }) {
|
||||
const [show, setShow] = useState<boolean>(false)
|
||||
|
||||
const open = useCallback(() => setShow(true), [setShow])
|
||||
@@ -30,7 +30,7 @@ export default function QuestionHelper({ text, disabled }: { text: string; disab
|
||||
|
||||
return (
|
||||
<span style={{ marginLeft: 4 }}>
|
||||
<Tooltip text={text} show={show && !disabled}>
|
||||
<Tooltip text={text} show={show}>
|
||||
<QuestionWrapper onClick={open} onMouseEnter={open} onMouseLeave={close}>
|
||||
<Question size={16} />
|
||||
</QuestionWrapper>
|
||||
|
||||
@@ -31,7 +31,7 @@ export default function CommonBases({
|
||||
selectedCurrency
|
||||
}: {
|
||||
chainId?: ChainId
|
||||
selectedCurrency?: Currency
|
||||
selectedCurrency?: Currency | null
|
||||
onSelect: (currency: Currency) => void
|
||||
}) {
|
||||
return (
|
||||
@@ -44,7 +44,11 @@ export default function CommonBases({
|
||||
</AutoRow>
|
||||
<AutoRow gap="4px">
|
||||
<BaseWrapper
|
||||
onClick={() => !currencyEquals(selectedCurrency, ETHER) && onSelect(ETHER)}
|
||||
onClick={() => {
|
||||
if (!selectedCurrency || !currencyEquals(selectedCurrency, ETHER)) {
|
||||
onSelect(ETHER)
|
||||
}
|
||||
}}
|
||||
disable={selectedCurrency === ETHER}
|
||||
>
|
||||
<CurrencyLogo currency={ETHER} style={{ marginRight: 8 }} />
|
||||
|
||||
@@ -1,155 +1,210 @@
|
||||
import { Currency, CurrencyAmount, currencyEquals, ETHER, JSBI, Token } from '@uniswap/sdk'
|
||||
import React, { CSSProperties, memo, useContext, useMemo } from 'react'
|
||||
import { Currency, CurrencyAmount, currencyEquals, ETHER, Token } from '@uniswap/sdk'
|
||||
import React, { CSSProperties, MutableRefObject, useCallback, useMemo } from 'react'
|
||||
import { FixedSizeList } from 'react-window'
|
||||
import { Text } from 'rebass'
|
||||
import { ThemeContext } from 'styled-components'
|
||||
import styled from 'styled-components'
|
||||
import { useActiveWeb3React } from '../../hooks'
|
||||
import { useAllTokens } from '../../hooks/Tokens'
|
||||
import { useDefaultTokenList } from '../../state/lists/hooks'
|
||||
import { useSelectedTokenList, WrappedTokenInfo } from '../../state/lists/hooks'
|
||||
import { useAddUserToken, useRemoveUserAddedToken } from '../../state/user/hooks'
|
||||
import { useETHBalances } from '../../state/wallet/hooks'
|
||||
import { useCurrencyBalance } from '../../state/wallet/hooks'
|
||||
import { LinkStyledButton, TYPE } from '../../theme'
|
||||
import { ButtonSecondary } from '../Button'
|
||||
import Column, { AutoColumn } from '../Column'
|
||||
import { useIsUserAddedToken } from '../../hooks/Tokens'
|
||||
import Column from '../Column'
|
||||
import { RowFixed } from '../Row'
|
||||
import CurrencyLogo from '../CurrencyLogo'
|
||||
import { MouseoverTooltip } from '../Tooltip'
|
||||
import { FadedSpan, MenuItem } from './styleds'
|
||||
import Loader from '../Loader'
|
||||
import { isDefaultToken } from '../../utils'
|
||||
import { isTokenOnList } from '../../utils'
|
||||
|
||||
function currencyKey(currency: Currency): string {
|
||||
return currency instanceof Token ? currency.address : currency === ETHER ? 'ETHER' : ''
|
||||
}
|
||||
|
||||
const StyledBalanceText = styled(Text)`
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
max-width: 5rem;
|
||||
text-overflow: ellipsis;
|
||||
`
|
||||
|
||||
const Tag = styled.div`
|
||||
background-color: ${({ theme }) => theme.bg3};
|
||||
color: ${({ theme }) => theme.text2};
|
||||
font-size: 14px;
|
||||
border-radius: 4px;
|
||||
padding: 0.25rem 0.3rem 0.25rem 0.3rem;
|
||||
max-width: 6rem;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
justify-self: flex-end;
|
||||
margin-right: 4px;
|
||||
`
|
||||
|
||||
function Balance({ balance }: { balance: CurrencyAmount }) {
|
||||
return <StyledBalanceText title={balance.toExact()}>{balance.toSignificant(4)}</StyledBalanceText>
|
||||
}
|
||||
|
||||
const TagContainer = styled.div`
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
`
|
||||
|
||||
function TokenTags({ currency }: { currency: Currency }) {
|
||||
if (!(currency instanceof WrappedTokenInfo)) {
|
||||
return <span />
|
||||
}
|
||||
|
||||
const tags = currency.tags
|
||||
if (!tags || tags.length === 0) return <span />
|
||||
|
||||
const tag = tags[0]
|
||||
|
||||
return (
|
||||
<TagContainer>
|
||||
<MouseoverTooltip text={tag.description}>
|
||||
<Tag key={tag.id}>{tag.name}</Tag>
|
||||
</MouseoverTooltip>
|
||||
{tags.length > 1 ? (
|
||||
<MouseoverTooltip
|
||||
text={tags
|
||||
.slice(1)
|
||||
.map(({ name, description }) => `${name}: ${description}`)
|
||||
.join('; \n')}
|
||||
>
|
||||
<Tag>...</Tag>
|
||||
</MouseoverTooltip>
|
||||
) : null}
|
||||
</TagContainer>
|
||||
)
|
||||
}
|
||||
|
||||
function CurrencyRow({
|
||||
currency,
|
||||
onSelect,
|
||||
isSelected,
|
||||
otherSelected,
|
||||
style
|
||||
}: {
|
||||
currency: Currency
|
||||
onSelect: () => void
|
||||
isSelected: boolean
|
||||
otherSelected: boolean
|
||||
style: CSSProperties
|
||||
}) {
|
||||
const { account, chainId } = useActiveWeb3React()
|
||||
const key = currencyKey(currency)
|
||||
const selectedTokenList = useSelectedTokenList()
|
||||
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
|
||||
style={style}
|
||||
className={`token-item-${key}`}
|
||||
onClick={() => (isSelected ? null : onSelect())}
|
||||
disabled={isSelected}
|
||||
selected={otherSelected}
|
||||
>
|
||||
<CurrencyLogo currency={currency} size={'24px'} />
|
||||
<Column>
|
||||
<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>
|
||||
</Column>
|
||||
<TokenTags currency={currency} />
|
||||
<RowFixed style={{ justifySelf: 'flex-end' }}>
|
||||
{balance ? <Balance balance={balance} /> : account ? <Loader /> : null}
|
||||
</RowFixed>
|
||||
</MenuItem>
|
||||
)
|
||||
}
|
||||
|
||||
export default function CurrencyList({
|
||||
height,
|
||||
currencies,
|
||||
allBalances,
|
||||
selectedCurrency,
|
||||
onCurrencySelect,
|
||||
otherCurrency,
|
||||
showSendWithSwap
|
||||
fixedListRef,
|
||||
showETH
|
||||
}: {
|
||||
height: number
|
||||
currencies: Currency[]
|
||||
selectedCurrency: Currency
|
||||
allBalances: { [tokenAddress: string]: CurrencyAmount }
|
||||
selectedCurrency?: Currency | null
|
||||
onCurrencySelect: (currency: Currency) => void
|
||||
otherCurrency: Currency
|
||||
showSendWithSwap?: boolean
|
||||
otherCurrency?: Currency | null
|
||||
fixedListRef?: MutableRefObject<FixedSizeList | undefined>
|
||||
showETH: boolean
|
||||
}) {
|
||||
const { account, chainId } = useActiveWeb3React()
|
||||
const theme = useContext(ThemeContext)
|
||||
const allTokens = useAllTokens()
|
||||
const defaultTokens = useDefaultTokenList()
|
||||
const addToken = useAddUserToken()
|
||||
const removeToken = useRemoveUserAddedToken()
|
||||
const ETHBalance = useETHBalances([account])[account]
|
||||
const itemData = useMemo(() => (showETH ? [Currency.ETHER, ...currencies] : currencies), [currencies, showETH])
|
||||
|
||||
const CurrencyRow = useMemo(() => {
|
||||
return memo(function CurrencyRow({ index, style }: { index: number; style: CSSProperties }) {
|
||||
const currency = index === 0 ? Currency.ETHER : currencies[index - 1]
|
||||
const key = currencyKey(currency)
|
||||
const isDefault = isDefaultToken(defaultTokens, currency)
|
||||
const customAdded = Boolean(!isDefault && currency instanceof Token && allTokens[currency.address])
|
||||
const balance = currency === ETHER ? ETHBalance : allBalances[key]
|
||||
|
||||
const zeroBalance = balance && JSBI.equal(JSBI.BigInt(0), balance.raw)
|
||||
|
||||
const isSelected = Boolean(selectedCurrency && currencyEquals(currency, selectedCurrency))
|
||||
const Row = useCallback(
|
||||
({ data, index, style }) => {
|
||||
const currency: Currency = data[index]
|
||||
const isSelected = Boolean(selectedCurrency && currencyEquals(selectedCurrency, currency))
|
||||
const otherSelected = Boolean(otherCurrency && currencyEquals(otherCurrency, currency))
|
||||
|
||||
const handleSelect = () => onCurrencySelect(currency)
|
||||
return (
|
||||
<MenuItem
|
||||
<CurrencyRow
|
||||
style={style}
|
||||
className={`token-item-${key}`}
|
||||
onClick={() => (isSelected ? null : onCurrencySelect(currency))}
|
||||
disabled={isSelected}
|
||||
selected={otherSelected}
|
||||
>
|
||||
<RowFixed>
|
||||
<CurrencyLogo currency={currency} size={'24px'} style={{ marginRight: '14px' }} />
|
||||
<Column>
|
||||
<Text fontWeight={500}>{currency.symbol}</Text>
|
||||
<FadedSpan>
|
||||
{customAdded ? (
|
||||
<TYPE.main fontWeight={500}>
|
||||
Added by user
|
||||
<LinkStyledButton
|
||||
onClick={event => {
|
||||
event.stopPropagation()
|
||||
if (currency instanceof Token) removeToken(chainId, currency.address)
|
||||
}}
|
||||
>
|
||||
(Remove)
|
||||
</LinkStyledButton>
|
||||
</TYPE.main>
|
||||
) : null}
|
||||
{!isDefault && !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>
|
||||
</Column>
|
||||
</RowFixed>
|
||||
<AutoColumn>
|
||||
{balance ? (
|
||||
<Text>
|
||||
{zeroBalance && showSendWithSwap ? (
|
||||
<ButtonSecondary padding={'4px 8px'}>
|
||||
<Text textAlign="center" fontWeight={500} fontSize={14} color={theme.primary1}>
|
||||
Send With Swap
|
||||
</Text>
|
||||
</ButtonSecondary>
|
||||
) : balance ? (
|
||||
balance.toSignificant(6)
|
||||
) : (
|
||||
'-'
|
||||
)}
|
||||
</Text>
|
||||
) : account ? (
|
||||
<Loader />
|
||||
) : (
|
||||
'-'
|
||||
)}
|
||||
</AutoColumn>
|
||||
</MenuItem>
|
||||
currency={currency}
|
||||
isSelected={isSelected}
|
||||
onSelect={handleSelect}
|
||||
otherSelected={otherSelected}
|
||||
/>
|
||||
)
|
||||
})
|
||||
}, [
|
||||
ETHBalance,
|
||||
account,
|
||||
addToken,
|
||||
allBalances,
|
||||
allTokens,
|
||||
chainId,
|
||||
currencies,
|
||||
defaultTokens,
|
||||
onCurrencySelect,
|
||||
otherCurrency,
|
||||
removeToken,
|
||||
selectedCurrency,
|
||||
showSendWithSwap,
|
||||
theme.primary1
|
||||
])
|
||||
},
|
||||
[onCurrencySelect, otherCurrency, selectedCurrency]
|
||||
)
|
||||
|
||||
const itemKey = useCallback((index: number, data: any) => currencyKey(data[index]), [])
|
||||
|
||||
return (
|
||||
<FixedSizeList
|
||||
height={height}
|
||||
ref={fixedListRef as any}
|
||||
width="100%"
|
||||
height={500}
|
||||
itemCount={currencies.length + 1}
|
||||
itemData={itemData}
|
||||
itemCount={itemData.length}
|
||||
itemSize={56}
|
||||
style={{ flex: '1' }}
|
||||
itemKey={index => currencyKey(currencies[index])}
|
||||
itemKey={itemKey}
|
||||
>
|
||||
{CurrencyRow}
|
||||
{Row}
|
||||
</FixedSizeList>
|
||||
)
|
||||
}
|
||||
|
||||
214
src/components/SearchModal/CurrencySearch.tsx
Normal file
214
src/components/SearchModal/CurrencySearch.tsx
Normal file
@@ -0,0 +1,214 @@
|
||||
import { Currency, ETHER, Token } from '@uniswap/sdk'
|
||||
import React, { KeyboardEvent, RefObject, useCallback, useContext, 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 { 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 CommonBases from './CommonBases'
|
||||
import CurrencyList from './CurrencyList'
|
||||
import { filterTokens } from './filtering'
|
||||
import SortButton from './SortButton'
|
||||
import { useTokenComparator } from './sorting'
|
||||
import { PaddedColumn, SearchInput, Separator } from './styleds'
|
||||
import AutoSizer from 'react-virtualized-auto-sizer'
|
||||
|
||||
interface CurrencySearchProps {
|
||||
isOpen: boolean
|
||||
onDismiss: () => void
|
||||
selectedCurrency?: Currency | null
|
||||
onCurrencySelect: (currency: Currency) => void
|
||||
otherSelectedCurrency?: Currency | null
|
||||
showCommonBases?: boolean
|
||||
onChangeList: () => void
|
||||
}
|
||||
|
||||
export function CurrencySearch({
|
||||
selectedCurrency,
|
||||
onCurrencySelect,
|
||||
otherSelectedCurrency,
|
||||
showCommonBases,
|
||||
onDismiss,
|
||||
isOpen,
|
||||
onChangeList
|
||||
}: CurrencySearchProps) {
|
||||
const { t } = useTranslation()
|
||||
const { chainId } = useActiveWeb3React()
|
||||
const theme = useContext(ThemeContext)
|
||||
|
||||
const fixedList = useRef<FixedSizeList>()
|
||||
const [searchQuery, setSearchQuery] = useState<string>('')
|
||||
const [invertSearchOrder, setInvertSearchOrder] = useState<boolean>(false)
|
||||
const allTokens = useAllTokens()
|
||||
|
||||
// if they input an address, use it
|
||||
const isAddressSearch = isAddress(searchQuery)
|
||||
const searchToken = useToken(searchQuery)
|
||||
|
||||
useEffect(() => {
|
||||
if (isAddressSearch) {
|
||||
ReactGA.event({
|
||||
category: 'Currency Select',
|
||||
action: 'Search by address',
|
||||
label: isAddressSearch
|
||||
})
|
||||
}
|
||||
}, [isAddressSearch])
|
||||
|
||||
const showETH: boolean = useMemo(() => {
|
||||
const s = searchQuery.toLowerCase().trim()
|
||||
return s === '' || s === 'e' || s === 'et' || s === 'eth'
|
||||
}, [searchQuery])
|
||||
|
||||
const tokenComparator = useTokenComparator(invertSearchOrder)
|
||||
|
||||
const filteredTokens: Token[] = useMemo(() => {
|
||||
if (isAddressSearch) return searchToken ? [searchToken] : []
|
||||
return filterTokens(Object.values(allTokens), searchQuery)
|
||||
}, [isAddressSearch, searchToken, allTokens, searchQuery])
|
||||
|
||||
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
|
||||
|
||||
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 handleCurrencySelect = useCallback(
|
||||
(currency: Currency) => {
|
||||
onCurrencySelect(currency)
|
||||
onDismiss()
|
||||
},
|
||||
[onDismiss, onCurrencySelect]
|
||||
)
|
||||
|
||||
// clear the input on open
|
||||
useEffect(() => {
|
||||
if (isOpen) setSearchQuery('')
|
||||
}, [isOpen])
|
||||
|
||||
// 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)
|
||||
fixedList.current?.scrollTo(0)
|
||||
}, [])
|
||||
|
||||
const handleEnter = useCallback(
|
||||
(e: KeyboardEvent<HTMLInputElement>) => {
|
||||
if (e.key === 'Enter') {
|
||||
const s = searchQuery.toLowerCase().trim()
|
||||
if (s === 'eth') {
|
||||
handleCurrencySelect(ETHER)
|
||||
} else if (filteredSortedTokens.length > 0) {
|
||||
if (
|
||||
filteredSortedTokens[0].symbol?.toLowerCase() === searchQuery.trim().toLowerCase() ||
|
||||
filteredSortedTokens.length === 1
|
||||
) {
|
||||
handleCurrencySelect(filteredSortedTokens[0])
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
[filteredSortedTokens, handleCurrencySelect, searchQuery]
|
||||
)
|
||||
|
||||
const selectedListInfo = useSelectedListInfo()
|
||||
|
||||
return (
|
||||
<Column style={{ width: '100%', flex: '1 1' }}>
|
||||
<PaddedColumn gap="14px">
|
||||
<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}
|
||||
/>
|
||||
{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>
|
||||
)
|
||||
}
|
||||
@@ -1,35 +1,19 @@
|
||||
import { Currency, Token } from '@uniswap/sdk'
|
||||
import React, { KeyboardEvent, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { isMobile } from 'react-device-detect'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Text } from 'rebass'
|
||||
import { ThemeContext } from 'styled-components'
|
||||
import Card from '../../components/Card'
|
||||
import { useActiveWeb3React } from '../../hooks'
|
||||
import { useAllTokens, useToken } from '../../hooks/Tokens'
|
||||
import useInterval from '../../hooks/useInterval'
|
||||
import { useAllTokenBalances, useTokenBalance } from '../../state/wallet/hooks'
|
||||
import { CloseIcon, LinkStyledButton } from '../../theme'
|
||||
import { isAddress } from '../../utils'
|
||||
import Column from '../Column'
|
||||
import { Currency } from '@uniswap/sdk'
|
||||
import React, { useCallback, useEffect, useState } from 'react'
|
||||
import ReactGA from 'react-ga'
|
||||
import useLast from '../../hooks/useLast'
|
||||
import { useSelectedListUrl } from '../../state/lists/hooks'
|
||||
import Modal from '../Modal'
|
||||
import QuestionHelper from '../QuestionHelper'
|
||||
import { AutoRow, RowBetween } from '../Row'
|
||||
import Tooltip from '../Tooltip'
|
||||
import CommonBases from './CommonBases'
|
||||
import { filterTokens } from './filtering'
|
||||
import { useTokenComparator } from './sorting'
|
||||
import { PaddedColumn, SearchInput } from './styleds'
|
||||
import CurrencyList from './CurrencyList'
|
||||
import SortButton from './SortButton'
|
||||
import { CurrencySearch } from './CurrencySearch'
|
||||
import ListIntroduction from './ListIntroduction'
|
||||
import { ListSelect } from './ListSelect'
|
||||
|
||||
interface CurrencySearchModalProps {
|
||||
isOpen?: boolean
|
||||
onDismiss?: () => void
|
||||
hiddenCurrency?: Currency
|
||||
showSendWithSwap?: boolean
|
||||
onCurrencySelect?: (currency: Currency) => void
|
||||
otherSelectedCurrency?: Currency
|
||||
isOpen: boolean
|
||||
onDismiss: () => void
|
||||
selectedCurrency?: Currency | null
|
||||
onCurrencySelect: (currency: Currency) => void
|
||||
otherSelectedCurrency?: Currency | null
|
||||
showCommonBases?: boolean
|
||||
}
|
||||
|
||||
@@ -37,53 +21,18 @@ export default function CurrencySearchModal({
|
||||
isOpen,
|
||||
onDismiss,
|
||||
onCurrencySelect,
|
||||
hiddenCurrency,
|
||||
showSendWithSwap,
|
||||
selectedCurrency,
|
||||
otherSelectedCurrency,
|
||||
showCommonBases = false
|
||||
}: CurrencySearchModalProps) {
|
||||
const { t } = useTranslation()
|
||||
const { account, chainId } = useActiveWeb3React()
|
||||
const theme = useContext(ThemeContext)
|
||||
const [listView, setListView] = useState<boolean>(false)
|
||||
const lastOpen = useLast(isOpen)
|
||||
|
||||
const [searchQuery, setSearchQuery] = useState<string>('')
|
||||
const [tooltipOpen, setTooltipOpen] = useState<boolean>(false)
|
||||
const [invertSearchOrder, setInvertSearchOrder] = useState<boolean>(false)
|
||||
const allTokens = useAllTokens()
|
||||
|
||||
// if the current input is an address, and we don't have the token in context, try to fetch it and import
|
||||
const searchToken = useToken(searchQuery)
|
||||
const searchTokenBalance = useTokenBalance(account, searchToken)
|
||||
const allTokenBalances_ = useAllTokenBalances()
|
||||
const allTokenBalances = searchToken
|
||||
? {
|
||||
[searchToken.address]: searchTokenBalance
|
||||
}
|
||||
: allTokenBalances_ ?? {}
|
||||
|
||||
const tokenComparator = useTokenComparator(invertSearchOrder)
|
||||
|
||||
const filteredTokens: Token[] = useMemo(() => {
|
||||
if (searchToken) return [searchToken]
|
||||
return filterTokens(Object.values(allTokens), searchQuery)
|
||||
}, [searchToken, allTokens, searchQuery])
|
||||
|
||||
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
|
||||
|
||||
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])
|
||||
useEffect(() => {
|
||||
if (isOpen && !lastOpen) {
|
||||
setListView(false)
|
||||
}
|
||||
}, [isOpen, lastOpen])
|
||||
|
||||
const handleCurrencySelect = useCallback(
|
||||
(currency: Currency) => {
|
||||
@@ -93,114 +42,44 @@ export default function CurrencySearchModal({
|
||||
[onDismiss, onCurrencySelect]
|
||||
)
|
||||
|
||||
// clear the input on open
|
||||
useEffect(() => {
|
||||
if (isOpen) setSearchQuery('')
|
||||
}, [isOpen, setSearchQuery])
|
||||
|
||||
// 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)
|
||||
setTooltipOpen(false)
|
||||
const handleClickChangeList = useCallback(() => {
|
||||
ReactGA.event({
|
||||
category: 'Lists',
|
||||
action: 'Change Lists'
|
||||
})
|
||||
setListView(true)
|
||||
}, [])
|
||||
const handleClickBack = useCallback(() => {
|
||||
ReactGA.event({
|
||||
category: 'Lists',
|
||||
action: 'Back'
|
||||
})
|
||||
setListView(false)
|
||||
}, [])
|
||||
const handleSelectListIntroduction = useCallback(() => {
|
||||
setListView(true)
|
||||
}, [])
|
||||
|
||||
const openTooltip = useCallback(() => {
|
||||
setTooltipOpen(true)
|
||||
}, [setTooltipOpen])
|
||||
const closeTooltip = useCallback(() => setTooltipOpen(false), [setTooltipOpen])
|
||||
|
||||
useInterval(
|
||||
() => {
|
||||
setTooltipOpen(false)
|
||||
},
|
||||
tooltipOpen ? 4000 : null,
|
||||
false
|
||||
)
|
||||
|
||||
const handleEnter = useCallback(
|
||||
(e: KeyboardEvent<HTMLInputElement>) => {
|
||||
if (e.key === 'Enter' && filteredSortedTokens.length > 0) {
|
||||
if (
|
||||
filteredSortedTokens[0].symbol.toLowerCase() === searchQuery.trim().toLowerCase() ||
|
||||
filteredSortedTokens.length === 1
|
||||
) {
|
||||
handleCurrencySelect(filteredSortedTokens[0])
|
||||
}
|
||||
}
|
||||
},
|
||||
[filteredSortedTokens, handleCurrencySelect, searchQuery]
|
||||
)
|
||||
const selectedListUrl = useSelectedListUrl()
|
||||
const noListSelected = !selectedListUrl
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
onDismiss={onDismiss}
|
||||
maxHeight={70}
|
||||
initialFocusRef={isMobile ? undefined : inputRef}
|
||||
minHeight={70}
|
||||
>
|
||||
<Column style={{ width: '100%' }}>
|
||||
<PaddedColumn gap="14px">
|
||||
<RowBetween>
|
||||
<Text fontWeight={500} fontSize={16}>
|
||||
Select a token
|
||||
<QuestionHelper
|
||||
disabled={tooltipOpen}
|
||||
text="Find a token by searching for its name or symbol or by pasting its address below."
|
||||
/>
|
||||
</Text>
|
||||
<CloseIcon onClick={onDismiss} />
|
||||
</RowBetween>
|
||||
<Tooltip
|
||||
text="Import any token into your list by pasting the token address into the search field."
|
||||
show={tooltipOpen}
|
||||
placement="bottom"
|
||||
>
|
||||
<SearchInput
|
||||
type="text"
|
||||
id="token-search-input"
|
||||
placeholder={t('tokenSearchPlaceholder')}
|
||||
value={searchQuery}
|
||||
ref={inputRef}
|
||||
onChange={handleInput}
|
||||
onFocus={closeTooltip}
|
||||
onBlur={closeTooltip}
|
||||
onKeyDown={handleEnter}
|
||||
/>
|
||||
</Tooltip>
|
||||
{showCommonBases && (
|
||||
<CommonBases chainId={chainId} onSelect={handleCurrencySelect} selectedCurrency={hiddenCurrency} />
|
||||
)}
|
||||
<RowBetween>
|
||||
<Text fontSize={14} fontWeight={500}>
|
||||
Token Name
|
||||
</Text>
|
||||
<SortButton ascending={invertSearchOrder} toggleSortOrder={() => setInvertSearchOrder(iso => !iso)} />
|
||||
</RowBetween>
|
||||
</PaddedColumn>
|
||||
<div style={{ width: '100%', height: '1px', backgroundColor: theme.bg2 }} />
|
||||
<CurrencyList
|
||||
currencies={filteredSortedTokens}
|
||||
allBalances={allTokenBalances}
|
||||
<Modal isOpen={isOpen} onDismiss={onDismiss} maxHeight={90} minHeight={listView ? 40 : noListSelected ? 0 : 80}>
|
||||
{listView ? (
|
||||
<ListSelect onDismiss={onDismiss} onBack={handleClickBack} />
|
||||
) : noListSelected ? (
|
||||
<ListIntroduction onSelectList={handleSelectListIntroduction} />
|
||||
) : (
|
||||
<CurrencySearch
|
||||
isOpen={isOpen}
|
||||
onDismiss={onDismiss}
|
||||
onCurrencySelect={handleCurrencySelect}
|
||||
otherCurrency={otherSelectedCurrency}
|
||||
selectedCurrency={hiddenCurrency}
|
||||
showSendWithSwap={showSendWithSwap}
|
||||
onChangeList={handleClickChangeList}
|
||||
selectedCurrency={selectedCurrency}
|
||||
otherSelectedCurrency={otherSelectedCurrency}
|
||||
showCommonBases={showCommonBases}
|
||||
/>
|
||||
<div style={{ width: '100%', height: '1px', backgroundColor: theme.bg2 }} />
|
||||
<Card>
|
||||
<AutoRow justify={'center'}>
|
||||
<div>
|
||||
<LinkStyledButton style={{ fontWeight: 500, color: theme.text2, fontSize: 16 }} onClick={openTooltip}>
|
||||
Having trouble finding a token?
|
||||
</LinkStyledButton>
|
||||
</div>
|
||||
</AutoRow>
|
||||
</Card>
|
||||
</Column>
|
||||
)}
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
47
src/components/SearchModal/ListIntroduction.tsx
Normal file
47
src/components/SearchModal/ListIntroduction.tsx
Normal file
@@ -0,0 +1,47 @@
|
||||
import React from 'react'
|
||||
import { Text } from 'rebass'
|
||||
import { ExternalLink } from '../../theme'
|
||||
import { ButtonPrimary } from '../Button'
|
||||
import { OutlineCard } from '../Card'
|
||||
import Column, { AutoColumn } from '../Column'
|
||||
import { PaddedColumn } from './styleds'
|
||||
import { useDarkModeManager } from '../../state/user/hooks'
|
||||
|
||||
import listLight from '../../assets/images/token-list/lists-light.png'
|
||||
import listDark from '../../assets/images/token-list/lists-dark.png'
|
||||
|
||||
export default function ListIntroduction({ onSelectList }: { onSelectList: () => void }) {
|
||||
const [isDark] = useDarkModeManager()
|
||||
|
||||
return (
|
||||
<Column style={{ width: '100%', flex: '1 1' }}>
|
||||
<PaddedColumn>
|
||||
<AutoColumn gap="14px">
|
||||
<img
|
||||
style={{ width: '120px', margin: '0 auto' }}
|
||||
src={isDark ? listDark : listLight}
|
||||
alt="token-list-preview"
|
||||
/>
|
||||
<img
|
||||
style={{ width: '100%', borderRadius: '12px' }}
|
||||
src="https://cloudflare-ipfs.com/ipfs/QmRf1rAJcZjV3pwKTHfPdJh4RxR8yvRHkdLjZCsmp7T6hA"
|
||||
alt="token-list-preview"
|
||||
/>
|
||||
<Text style={{ marginBottom: '8px', textAlign: 'center' }}>
|
||||
Uniswap now supports token lists. You can add your own custom lists via IPFS, HTTPS and ENS.{' '}
|
||||
</Text>
|
||||
<ButtonPrimary onClick={onSelectList} id="list-introduction-choose-a-list">
|
||||
Choose a list
|
||||
</ButtonPrimary>
|
||||
<OutlineCard style={{ marginBottom: '8px', padding: '1rem' }}>
|
||||
<Text fontWeight={400} fontSize={14} style={{ textAlign: 'center' }}>
|
||||
Token lists are an{' '}
|
||||
<ExternalLink href="https://github.com/uniswap/token-lists">open specification</ExternalLink>. Check out{' '}
|
||||
<ExternalLink href="https://tokenlists.org">tokenlists.org</ExternalLink> to learn more.
|
||||
</Text>
|
||||
</OutlineCard>
|
||||
</AutoColumn>
|
||||
</PaddedColumn>
|
||||
</Column>
|
||||
)
|
||||
}
|
||||
379
src/components/SearchModal/ListSelect.tsx
Normal file
379
src/components/SearchModal/ListSelect.tsx
Normal file
@@ -0,0 +1,379 @@
|
||||
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)`
|
||||
/* height: 1.8rem; */
|
||||
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>
|
||||
)
|
||||
}
|
||||
@@ -31,6 +31,6 @@ export function filterTokens(tokens: Token[], search: string): Token[] {
|
||||
return tokens.filter(token => {
|
||||
const { symbol, name } = token
|
||||
|
||||
return matchesSearch(symbol) || matchesSearch(name)
|
||||
return (symbol && matchesSearch(symbol)) || (name && matchesSearch(name))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { Token, TokenAmount, WETH } from '@uniswap/sdk'
|
||||
import { Token, TokenAmount } from '@uniswap/sdk'
|
||||
import { useMemo } from 'react'
|
||||
import { useActiveWeb3React } from '../../hooks'
|
||||
import { useAllTokenBalances } from '../../state/wallet/hooks'
|
||||
|
||||
// compare two token amounts with highest one coming first
|
||||
@@ -15,20 +14,13 @@ function balanceComparator(balanceA?: TokenAmount, balanceB?: TokenAmount) {
|
||||
return 0
|
||||
}
|
||||
|
||||
function getTokenComparator(
|
||||
weth: Token | undefined,
|
||||
balances: { [tokenAddress: string]: TokenAmount }
|
||||
): (tokenA: Token, tokenB: Token) => number {
|
||||
function getTokenComparator(balances: {
|
||||
[tokenAddress: string]: TokenAmount | undefined
|
||||
}): (tokenA: Token, tokenB: Token) => number {
|
||||
return function sortTokens(tokenA: Token, tokenB: Token): number {
|
||||
// -1 = a is first
|
||||
// 1 = b is first
|
||||
|
||||
// sort ETH first
|
||||
if (weth) {
|
||||
if (tokenA.equals(weth)) return -1
|
||||
if (tokenB.equals(weth)) return 1
|
||||
}
|
||||
|
||||
// sort by balances
|
||||
const balanceA = balances[tokenA.address]
|
||||
const balanceB = balances[tokenB.address]
|
||||
@@ -36,16 +28,18 @@ function getTokenComparator(
|
||||
const balanceComp = balanceComparator(balanceA, balanceB)
|
||||
if (balanceComp !== 0) return balanceComp
|
||||
|
||||
// sort by symbol
|
||||
return tokenA.symbol.toLowerCase() < tokenB.symbol.toLowerCase() ? -1 : 1
|
||||
if (tokenA.symbol && tokenB.symbol) {
|
||||
// sort by symbol
|
||||
return tokenA.symbol.toLowerCase() < tokenB.symbol.toLowerCase() ? -1 : 1
|
||||
} else {
|
||||
return tokenA.symbol ? -1 : tokenB.symbol ? -1 : 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function useTokenComparator(inverted: boolean): (tokenA: Token, tokenB: Token) => number {
|
||||
const { chainId } = useActiveWeb3React()
|
||||
const weth = WETH[chainId]
|
||||
const balances = useAllTokenBalances()
|
||||
const comparator = useMemo(() => getTokenComparator(weth, balances ?? {}), [balances, weth])
|
||||
const comparator = useMemo(() => getTokenComparator(balances ?? {}), [balances])
|
||||
return useMemo(() => {
|
||||
if (inverted) {
|
||||
return (tokenA: Token, tokenB: Token) => comparator(tokenA, tokenB) * -1
|
||||
|
||||
@@ -17,12 +17,26 @@ export const FadedSpan = styled(RowFixed)`
|
||||
font-size: 14px;
|
||||
`
|
||||
|
||||
export const GreySpan = styled.span`
|
||||
color: ${({ theme }) => theme.text3};
|
||||
font-weight: 400;
|
||||
export const PaddedColumn = styled(AutoColumn)`
|
||||
padding: 20px;
|
||||
padding-bottom: 12px;
|
||||
`
|
||||
|
||||
export const Input = styled.input`
|
||||
export const MenuItem = styled(RowBetween)`
|
||||
padding: 4px 20px;
|
||||
height: 56px;
|
||||
display: grid;
|
||||
grid-template-columns: auto minmax(auto, 1fr) auto minmax(0, 72px);
|
||||
grid-gap: 16px;
|
||||
cursor: ${({ disabled }) => !disabled && 'pointer'};
|
||||
pointer-events: ${({ disabled }) => disabled && 'none'};
|
||||
:hover {
|
||||
background-color: ${({ theme, disabled }) => !disabled && theme.bg2};
|
||||
}
|
||||
opacity: ${({ disabled, selected }) => (disabled || selected ? 0.5 : 1)};
|
||||
`
|
||||
|
||||
export const SearchInput = styled.input`
|
||||
position: relative;
|
||||
display: flex;
|
||||
padding: 16px;
|
||||
@@ -43,28 +57,20 @@ export const Input = styled.input`
|
||||
::placeholder {
|
||||
color: ${({ theme }) => theme.text3};
|
||||
}
|
||||
`
|
||||
|
||||
export const PaddedColumn = styled(AutoColumn)`
|
||||
padding: 20px;
|
||||
padding-bottom: 12px;
|
||||
`
|
||||
|
||||
export const MenuItem = styled(RowBetween)`
|
||||
padding: 4px 20px;
|
||||
height: 56px;
|
||||
cursor: ${({ disabled }) => !disabled && 'pointer'};
|
||||
pointer-events: ${({ disabled }) => disabled && 'none'};
|
||||
:hover {
|
||||
background-color: ${({ theme, disabled }) => !disabled && theme.bg2};
|
||||
}
|
||||
opacity: ${({ disabled, selected }) => (disabled || selected ? 0.5 : 1)};
|
||||
`
|
||||
|
||||
export const SearchInput = styled(Input)`
|
||||
transition: border 100ms;
|
||||
:focus {
|
||||
border: 1px solid ${({ theme }) => theme.primary1};
|
||||
outline: none;
|
||||
}
|
||||
`
|
||||
export const Separator = styled.div`
|
||||
width: 100%;
|
||||
height: 1px;
|
||||
background-color: ${({ theme }) => theme.bg2};
|
||||
`
|
||||
|
||||
export const SeparatorDark = styled.div`
|
||||
width: 100%;
|
||||
height: 1px;
|
||||
background-color: ${({ theme }) => theme.bg3};
|
||||
`
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import React, { useRef, useEffect, useContext, useState } from 'react'
|
||||
import React, { useRef, useContext, useState } from 'react'
|
||||
import { Settings, X } from 'react-feather'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import { useOnClickOutside } from '../../hooks/useOnClickOutside'
|
||||
import {
|
||||
useUserSlippageTolerance,
|
||||
useExpertModeManager,
|
||||
useUserDeadline,
|
||||
useDarkModeManager
|
||||
} from '../../state/user/hooks'
|
||||
import SlippageTabs from '../SlippageTabs'
|
||||
import TransactionSettings from '../TransactionSettings'
|
||||
import { RowFixed, RowBetween } from '../Row'
|
||||
import { TYPE } from '../../theme'
|
||||
import QuestionHelper from '../QuestionHelper'
|
||||
@@ -138,27 +138,11 @@ export default function SettingsTab() {
|
||||
// show confirmation view before turning on
|
||||
const [showConfirmation, setShowConfirmation] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
const handleClickOutside = e => {
|
||||
if (node.current?.contains(e.target) ?? false) {
|
||||
return
|
||||
}
|
||||
toggle()
|
||||
}
|
||||
|
||||
if (open) {
|
||||
document.addEventListener('mousedown', handleClickOutside)
|
||||
} else {
|
||||
document.removeEventListener('mousedown', handleClickOutside)
|
||||
}
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('mousedown', handleClickOutside)
|
||||
}
|
||||
}, [open, toggle])
|
||||
useOnClickOutside(node, open ? toggle : undefined)
|
||||
|
||||
return (
|
||||
<StyledMenu ref={node}>
|
||||
// https://github.com/DefinitelyTyped/DefinitelyTyped/issues/30451
|
||||
<StyledMenu ref={node as any}>
|
||||
<Modal isOpen={showConfirmation} onDismiss={() => setShowConfirmation(false)} maxHeight={100}>
|
||||
<ModalContentWrapper>
|
||||
<AutoColumn gap="lg">
|
||||
@@ -188,7 +172,7 @@ export default function SettingsTab() {
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Text fontSize={20} fontWeight={500}>
|
||||
<Text fontSize={20} fontWeight={500} id="confirm-expert-mode">
|
||||
Turn On Expert Mode
|
||||
</Text>
|
||||
</ButtonError>
|
||||
@@ -196,7 +180,7 @@ export default function SettingsTab() {
|
||||
</AutoColumn>
|
||||
</ModalContentWrapper>
|
||||
</Modal>
|
||||
<StyledMenuButton onClick={toggle}>
|
||||
<StyledMenuButton onClick={toggle} id="open-settings-dialog-button">
|
||||
<StyledMenuIcon />
|
||||
{expertMode && (
|
||||
<EmojiWrapper>
|
||||
@@ -212,7 +196,7 @@ export default function SettingsTab() {
|
||||
<Text fontWeight={600} fontSize={14}>
|
||||
Transaction Settings
|
||||
</Text>
|
||||
<SlippageTabs
|
||||
<TransactionSettings
|
||||
rawSlippage={userSlippageTolerance}
|
||||
setRawSlippage={setUserslippageTolerance}
|
||||
deadline={deadline}
|
||||
@@ -229,6 +213,7 @@ export default function SettingsTab() {
|
||||
<QuestionHelper text="Bypasses confirmation modals and allows high slippage trades. Use at your own risk." />
|
||||
</RowFixed>
|
||||
<Toggle
|
||||
id="toggle-expert-mode-button"
|
||||
isActive={expertMode}
|
||||
toggle={
|
||||
expertMode
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { useCallback } from 'react'
|
||||
import styled from 'styled-components'
|
||||
|
||||
const StyledRangeInput = styled.input<{ value: number }>`
|
||||
const StyledRangeInput = styled.input<{ size: number }>`
|
||||
-webkit-appearance: none; /* Hides the slider so that custom slider can be made */
|
||||
width: 100%; /* Specific width is required for Firefox. */
|
||||
background: transparent; /* Otherwise white in Chrome */
|
||||
@@ -17,8 +17,8 @@ const StyledRangeInput = styled.input<{ value: number }>`
|
||||
|
||||
&::-webkit-slider-thumb {
|
||||
-webkit-appearance: none;
|
||||
height: 28px;
|
||||
width: 28px;
|
||||
height: ${({ size }) => size}px;
|
||||
width: ${({ size }) => size}px;
|
||||
background-color: #565a69;
|
||||
border-radius: 100%;
|
||||
border: none;
|
||||
@@ -33,8 +33,8 @@ const StyledRangeInput = styled.input<{ value: number }>`
|
||||
}
|
||||
|
||||
&::-moz-range-thumb {
|
||||
height: 28px;
|
||||
width: 28px;
|
||||
height: ${({ size }) => size}px;
|
||||
width: ${({ size }) => size}px;
|
||||
background-color: #565a69;
|
||||
border-radius: 100%;
|
||||
border: none;
|
||||
@@ -48,8 +48,8 @@ const StyledRangeInput = styled.input<{ value: number }>`
|
||||
}
|
||||
|
||||
&::-ms-thumb {
|
||||
height: 28px;
|
||||
width: 28px;
|
||||
height: ${({ size }) => size}px;
|
||||
width: ${({ size }) => size}px;
|
||||
background-color: #565a69;
|
||||
border-radius: 100%;
|
||||
color: ${({ theme }) => theme.bg1};
|
||||
@@ -62,24 +62,12 @@ const StyledRangeInput = styled.input<{ value: number }>`
|
||||
}
|
||||
|
||||
&::-webkit-slider-runnable-track {
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
${({ theme }) => theme.bg5},
|
||||
${({ theme }) => theme.bg5} ${({ value }) => value}%,
|
||||
${({ theme }) => theme.bg3} ${({ value }) => value}%,
|
||||
${({ theme }) => theme.bg3}
|
||||
);
|
||||
background: linear-gradient(90deg, ${({ theme }) => theme.bg5}, ${({ theme }) => theme.bg3});
|
||||
height: 2px;
|
||||
}
|
||||
|
||||
&::-moz-range-track {
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
${({ theme }) => theme.bg5},
|
||||
${({ theme }) => theme.bg5} ${({ value }) => value}%,
|
||||
${({ theme }) => theme.bg3} ${({ value }) => value}%,
|
||||
${({ theme }) => theme.bg3}
|
||||
);
|
||||
background: linear-gradient(90deg, ${({ theme }) => theme.bg5}, ${({ theme }) => theme.bg3});
|
||||
height: 2px;
|
||||
}
|
||||
|
||||
@@ -102,26 +90,31 @@ const StyledRangeInput = styled.input<{ value: number }>`
|
||||
interface InputSliderProps {
|
||||
value: number
|
||||
onChange: (value: number) => void
|
||||
step?: number
|
||||
min?: number
|
||||
max?: number
|
||||
size?: number
|
||||
}
|
||||
|
||||
export default function InputSlider({ value, onChange }: InputSliderProps) {
|
||||
export default function Slider({ value, onChange, min = 0, step = 1, max = 100, size = 28 }: InputSliderProps) {
|
||||
const changeCallback = useCallback(
|
||||
e => {
|
||||
onChange(e.target.value)
|
||||
onChange(parseInt(e.target.value))
|
||||
},
|
||||
[onChange]
|
||||
)
|
||||
|
||||
return (
|
||||
<StyledRangeInput
|
||||
size={size}
|
||||
type="range"
|
||||
value={value}
|
||||
style={{ width: '90%', marginLeft: 15, marginRight: 15, padding: '15px 0' }}
|
||||
onChange={changeCallback}
|
||||
aria-labelledby="input-slider"
|
||||
step={1}
|
||||
min={0}
|
||||
max={100}
|
||||
aria-labelledby="input slider"
|
||||
step={step}
|
||||
min={min}
|
||||
max={max}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -10,26 +10,26 @@ const ToggleElement = styled.span<{ isActive?: boolean; isOnSwitch?: boolean }>`
|
||||
font-weight: 400;
|
||||
`
|
||||
|
||||
const StyledToggle = styled.a<{ isActive?: boolean; activeElement?: boolean }>`
|
||||
const StyledToggle = styled.button<{ isActive?: boolean; activeElement?: boolean }>`
|
||||
border-radius: 16px;
|
||||
border: 1px solid ${({ theme, isActive }) => (isActive ? theme.primary5 : theme.text4)};
|
||||
display: flex;
|
||||
width: fit-content;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
outline: none;
|
||||
padding: 0;
|
||||
background-color: transparent;
|
||||
`
|
||||
|
||||
export interface ToggleProps {
|
||||
id?: string
|
||||
isActive: boolean
|
||||
toggle: () => void
|
||||
}
|
||||
|
||||
export default function Toggle({ isActive, toggle }: ToggleProps) {
|
||||
export default function Toggle({ id, isActive, toggle }: ToggleProps) {
|
||||
return (
|
||||
<StyledToggle isActive={isActive} target="_self" onClick={toggle}>
|
||||
<StyledToggle id={id} isActive={isActive} onClick={toggle}>
|
||||
<ToggleElement isActive={isActive} isOnSwitch={true}>
|
||||
On
|
||||
</ToggleElement>
|
||||
|
||||
@@ -1,139 +0,0 @@
|
||||
import { Currency, Token } from '@uniswap/sdk'
|
||||
import { transparentize } from 'polished'
|
||||
import React, { useMemo } from 'react'
|
||||
import styled from 'styled-components'
|
||||
import { useActiveWeb3React } from '../../hooks'
|
||||
import { useAllTokens } from '../../hooks/Tokens'
|
||||
import { useDefaultTokenList } from '../../state/lists/hooks'
|
||||
import { Field } from '../../state/swap/actions'
|
||||
import { ExternalLink, TYPE } from '../../theme'
|
||||
import { getEtherscanLink, isDefaultToken } from '../../utils'
|
||||
import PropsOfExcluding from '../../utils/props-of-excluding'
|
||||
import CurrencyLogo from '../CurrencyLogo'
|
||||
import { AutoRow, RowBetween } from '../Row'
|
||||
import { AutoColumn } from '../Column'
|
||||
import { AlertTriangle } from 'react-feather'
|
||||
import { ButtonError } from '../Button'
|
||||
import { useTokenWarningDismissal } from '../../state/user/hooks'
|
||||
|
||||
const Wrapper = styled.div<{ error: boolean }>`
|
||||
background: ${({ theme }) => transparentize(0.6, theme.white)};
|
||||
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;
|
||||
box-sizing: border-box;
|
||||
border-radius: 20px;
|
||||
margin-bottom: 2rem;
|
||||
`
|
||||
|
||||
const StyledWarningIcon = styled(AlertTriangle)`
|
||||
stroke: ${({ theme }) => theme.red2};
|
||||
`
|
||||
|
||||
interface TokenWarningCardProps extends PropsOfExcluding<typeof Wrapper, 'error'> {
|
||||
token?: Token
|
||||
}
|
||||
|
||||
export default function TokenWarningCard({ token, ...rest }: TokenWarningCardProps) {
|
||||
const { chainId } = useActiveWeb3React()
|
||||
const defaultTokens = useDefaultTokenList()
|
||||
const isDefault = isDefaultToken(defaultTokens, token)
|
||||
|
||||
const tokenSymbol = token?.symbol?.toLowerCase() ?? ''
|
||||
const tokenName = token?.name?.toLowerCase() ?? ''
|
||||
|
||||
const allTokens = useAllTokens()
|
||||
|
||||
const duplicateNameOrSymbol = useMemo(() => {
|
||||
if (isDefault || !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
|
||||
})
|
||||
}, [isDefault, token, chainId, allTokens, tokenSymbol, tokenName])
|
||||
|
||||
if (isDefault || !token) return null
|
||||
|
||||
return (
|
||||
<Wrapper error={duplicateNameOrSymbol} {...rest}>
|
||||
<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>
|
||||
<ExternalLink style={{ fontWeight: 400 }} href={getEtherscanLink(chainId, token.address, 'token')}>
|
||||
<TYPE.blue> (View on Etherscan)</TYPE.blue>
|
||||
</ExternalLink>
|
||||
</AutoColumn>
|
||||
</AutoRow>
|
||||
</Wrapper>
|
||||
)
|
||||
}
|
||||
|
||||
export function TokenWarningCards({ currencies }: { currencies: { [field in Field]?: Currency } }) {
|
||||
const { chainId } = useActiveWeb3React()
|
||||
const [dismissedToken0, dismissToken0] = useTokenWarningDismissal(chainId, currencies[Field.INPUT])
|
||||
const [dismissedToken1, dismissToken1] = useTokenWarningDismissal(chainId, currencies[Field.OUTPUT])
|
||||
|
||||
return (
|
||||
<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 and name any ERC20 token on Ethereum, 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'}>
|
||||
Similar to Etherscan, this site can load arbitrary tokens via token addresses. Please do your own research
|
||||
before interacting with any ERC20 token.
|
||||
</TYPE.body>
|
||||
{Object.keys(currencies).map(field => {
|
||||
const dismissed = field === Field.INPUT ? dismissedToken0 : dismissedToken1
|
||||
return currencies[field] instanceof Token && !dismissed ? (
|
||||
<TokenWarningCard key={field} token={currencies[field]} />
|
||||
) : null
|
||||
})}
|
||||
<RowBetween>
|
||||
<div />
|
||||
<ButtonError
|
||||
error={true}
|
||||
width={'140px'}
|
||||
padding="0.5rem 1rem"
|
||||
style={{
|
||||
borderRadius: '10px'
|
||||
}}
|
||||
onClick={() => {
|
||||
dismissToken0 && dismissToken0()
|
||||
dismissToken1 && dismissToken1()
|
||||
}}
|
||||
>
|
||||
<TYPE.body color="white" className="token-dismiss-button">
|
||||
I understand
|
||||
</TYPE.body>
|
||||
</ButtonError>
|
||||
<div />
|
||||
</RowBetween>
|
||||
</AutoColumn>
|
||||
</WarningContainer>
|
||||
)
|
||||
}
|
||||
153
src/components/TokenWarningModal/index.tsx
Normal file
153
src/components/TokenWarningModal/index.tsx
Normal file
@@ -0,0 +1,153 @@
|
||||
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 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>
|
||||
)
|
||||
}
|
||||
|
||||
export default function TokenWarningModal({
|
||||
isOpen,
|
||||
tokens,
|
||||
onConfirm
|
||||
}: {
|
||||
isOpen: boolean
|
||||
tokens: Token[]
|
||||
onConfirm: () => 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>
|
||||
)
|
||||
}
|
||||
197
src/components/TransactionConfirmationModal/index.tsx
Normal file
197
src/components/TransactionConfirmationModal/index.tsx
Normal file
@@ -0,0 +1,197 @@
|
||||
import { ChainId } 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, Spinner } from '../../theme/components'
|
||||
import { RowBetween } from '../Row'
|
||||
import { AlertTriangle, ArrowUpCircle } from 'react-feather'
|
||||
import { ButtonPrimary } from '../Button'
|
||||
import { AutoColumn, ColumnCenter } from '../Column'
|
||||
import Circle from '../../assets/images/blue-loader.svg'
|
||||
|
||||
import { getEtherscanLink } from '../../utils'
|
||||
import { useActiveWeb3React } from '../../hooks'
|
||||
|
||||
const Wrapper = styled.div`
|
||||
width: 100%;
|
||||
`
|
||||
const Section = styled(AutoColumn)`
|
||||
padding: 24px;
|
||||
`
|
||||
|
||||
const BottomSection = styled(Section)`
|
||||
background-color: ${({ theme }) => theme.bg2};
|
||||
border-bottom-left-radius: 20px;
|
||||
border-bottom-right-radius: 20px;
|
||||
`
|
||||
|
||||
const ConfirmedIcon = styled(ColumnCenter)`
|
||||
padding: 60px 0;
|
||||
`
|
||||
|
||||
const CustomLightSpinner = styled(Spinner)<{ size: string }>`
|
||||
height: ${({ size }) => size};
|
||||
width: ${({ size }) => size};
|
||||
`
|
||||
|
||||
function ConfirmationPendingContent({ onDismiss, pendingText }: { onDismiss: () => void; pendingText: string }) {
|
||||
return (
|
||||
<Wrapper>
|
||||
<Section>
|
||||
<RowBetween>
|
||||
<div />
|
||||
<CloseIcon onClick={onDismiss} />
|
||||
</RowBetween>
|
||||
<ConfirmedIcon>
|
||||
<CustomLightSpinner src={Circle} alt="loader" size={'90px'} />
|
||||
</ConfirmedIcon>
|
||||
<AutoColumn gap="12px" justify={'center'}>
|
||||
<Text fontWeight={500} fontSize={20}>
|
||||
Waiting For Confirmation
|
||||
</Text>
|
||||
<AutoColumn gap="12px" justify={'center'}>
|
||||
<Text fontWeight={600} fontSize={14} color="" textAlign="center">
|
||||
{pendingText}
|
||||
</Text>
|
||||
</AutoColumn>
|
||||
<Text fontSize={12} color="#565A69" textAlign="center">
|
||||
Confirm this transaction in your wallet
|
||||
</Text>
|
||||
</AutoColumn>
|
||||
</Section>
|
||||
</Wrapper>
|
||||
)
|
||||
}
|
||||
|
||||
function TransactionSubmittedContent({
|
||||
onDismiss,
|
||||
chainId,
|
||||
hash
|
||||
}: {
|
||||
onDismiss: () => void
|
||||
hash: string | undefined
|
||||
chainId: ChainId
|
||||
}) {
|
||||
const theme = useContext(ThemeContext)
|
||||
|
||||
return (
|
||||
<Wrapper>
|
||||
<Section>
|
||||
<RowBetween>
|
||||
<div />
|
||||
<CloseIcon onClick={onDismiss} />
|
||||
</RowBetween>
|
||||
<ConfirmedIcon>
|
||||
<ArrowUpCircle strokeWidth={0.5} size={90} color={theme.primary1} />
|
||||
</ConfirmedIcon>
|
||||
<AutoColumn gap="12px" justify={'center'}>
|
||||
<Text fontWeight={500} fontSize={20}>
|
||||
Transaction Submitted
|
||||
</Text>
|
||||
|
||||
{chainId && hash && (
|
||||
<ExternalLink href={getEtherscanLink(chainId, hash, 'transaction')}>
|
||||
<Text fontWeight={500} fontSize={14} color={theme.primary1}>
|
||||
View on Etherscan
|
||||
</Text>
|
||||
</ExternalLink>
|
||||
)}
|
||||
<ButtonPrimary onClick={onDismiss} style={{ margin: '20px 0 0 0' }}>
|
||||
<Text fontWeight={500} fontSize={20}>
|
||||
Close
|
||||
</Text>
|
||||
</ButtonPrimary>
|
||||
</AutoColumn>
|
||||
</Section>
|
||||
</Wrapper>
|
||||
)
|
||||
}
|
||||
|
||||
export function ConfirmationModalContent({
|
||||
title,
|
||||
bottomContent,
|
||||
onDismiss,
|
||||
topContent
|
||||
}: {
|
||||
title: string
|
||||
onDismiss: () => void
|
||||
topContent: () => React.ReactNode
|
||||
bottomContent: () => React.ReactNode
|
||||
}) {
|
||||
return (
|
||||
<Wrapper>
|
||||
<Section>
|
||||
<RowBetween>
|
||||
<Text fontWeight={500} fontSize={20}>
|
||||
{title}
|
||||
</Text>
|
||||
<CloseIcon onClick={onDismiss} />
|
||||
</RowBetween>
|
||||
{topContent()}
|
||||
</Section>
|
||||
<BottomSection gap="12px">{bottomContent()}</BottomSection>
|
||||
</Wrapper>
|
||||
)
|
||||
}
|
||||
|
||||
export function TransactionErrorContent({ message, onDismiss }: { message: string; onDismiss: () => void }) {
|
||||
const theme = useContext(ThemeContext)
|
||||
return (
|
||||
<Wrapper>
|
||||
<Section>
|
||||
<RowBetween>
|
||||
<Text fontWeight={500} fontSize={20}>
|
||||
Error
|
||||
</Text>
|
||||
<CloseIcon onClick={onDismiss} />
|
||||
</RowBetween>
|
||||
<AutoColumn style={{ marginTop: 20, padding: '2rem 0' }} gap="24px" justify="center">
|
||||
<AlertTriangle color={theme.red1} style={{ strokeWidth: 1.5 }} size={64} />
|
||||
<Text fontWeight={500} fontSize={16} color={theme.red1} style={{ textAlign: 'center', width: '85%' }}>
|
||||
{message}
|
||||
</Text>
|
||||
</AutoColumn>
|
||||
</Section>
|
||||
<BottomSection gap="12px">
|
||||
<ButtonPrimary onClick={onDismiss}>Dismiss</ButtonPrimary>
|
||||
</BottomSection>
|
||||
</Wrapper>
|
||||
)
|
||||
}
|
||||
|
||||
interface ConfirmationModalProps {
|
||||
isOpen: boolean
|
||||
onDismiss: () => void
|
||||
hash: string | undefined
|
||||
content: () => React.ReactNode
|
||||
attemptingTxn: boolean
|
||||
pendingText: string
|
||||
}
|
||||
|
||||
export default function TransactionConfirmationModal({
|
||||
isOpen,
|
||||
onDismiss,
|
||||
attemptingTxn,
|
||||
hash,
|
||||
pendingText,
|
||||
content
|
||||
}: ConfirmationModalProps) {
|
||||
const { chainId } = useActiveWeb3React()
|
||||
|
||||
if (!chainId) return null
|
||||
|
||||
// confirmation screen
|
||||
return (
|
||||
<Modal isOpen={isOpen} onDismiss={onDismiss} maxHeight={90}>
|
||||
{attemptingTxn ? (
|
||||
<ConfirmationPendingContent onDismiss={onDismiss} pendingText={pendingText} />
|
||||
) : hash ? (
|
||||
<TransactionSubmittedContent chainId={chainId} hash={hash} onDismiss={onDismiss} />
|
||||
) : (
|
||||
content()
|
||||
)}
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
@@ -104,48 +104,44 @@ export default function SlippageTabs({ rawSlippage, setRawSlippage, deadline, se
|
||||
slippageInput === '' || (rawSlippage / 100).toFixed(2) === Number.parseFloat(slippageInput).toFixed(2)
|
||||
const deadlineInputIsValid = deadlineInput === '' || (deadline / 60).toString() === deadlineInput
|
||||
|
||||
let slippageError: SlippageError
|
||||
let slippageError: SlippageError | undefined
|
||||
if (slippageInput !== '' && !slippageInputIsValid) {
|
||||
slippageError = SlippageError.InvalidInput
|
||||
} else if (slippageInputIsValid && rawSlippage < 50) {
|
||||
slippageError = SlippageError.RiskyLow
|
||||
} else if (slippageInputIsValid && rawSlippage > 500) {
|
||||
slippageError = SlippageError.RiskyHigh
|
||||
} else {
|
||||
slippageError = undefined
|
||||
}
|
||||
|
||||
let deadlineError: DeadlineError
|
||||
let deadlineError: DeadlineError | undefined
|
||||
if (deadlineInput !== '' && !deadlineInputIsValid) {
|
||||
deadlineError = DeadlineError.InvalidInput
|
||||
} else {
|
||||
deadlineError = undefined
|
||||
}
|
||||
|
||||
function parseCustomSlippage(event) {
|
||||
setSlippageInput(event.target.value)
|
||||
function parseCustomSlippage(value: string) {
|
||||
setSlippageInput(value)
|
||||
|
||||
let valueAsIntFromRoundedFloat: number
|
||||
try {
|
||||
valueAsIntFromRoundedFloat = Number.parseInt((Number.parseFloat(event.target.value) * 100).toString())
|
||||
const valueAsIntFromRoundedFloat = Number.parseInt((Number.parseFloat(value) * 100).toString())
|
||||
if (!Number.isNaN(valueAsIntFromRoundedFloat) && valueAsIntFromRoundedFloat < 5000) {
|
||||
setRawSlippage(valueAsIntFromRoundedFloat)
|
||||
}
|
||||
} catch {}
|
||||
|
||||
if (
|
||||
typeof valueAsIntFromRoundedFloat === 'number' &&
|
||||
!Number.isNaN(valueAsIntFromRoundedFloat) &&
|
||||
valueAsIntFromRoundedFloat < 5000
|
||||
) {
|
||||
setRawSlippage(valueAsIntFromRoundedFloat)
|
||||
}
|
||||
}
|
||||
|
||||
function parseCustomDeadline(event) {
|
||||
setDeadlineInput(event.target.value)
|
||||
function parseCustomDeadline(value: string) {
|
||||
setDeadlineInput(value)
|
||||
|
||||
let valueAsInt: number
|
||||
try {
|
||||
valueAsInt = Number.parseInt(event.target.value) * 60
|
||||
const valueAsInt: number = Number.parseInt(value) * 60
|
||||
if (!Number.isNaN(valueAsInt) && valueAsInt > 0) {
|
||||
setDeadline(valueAsInt)
|
||||
}
|
||||
} catch {}
|
||||
|
||||
if (typeof valueAsInt === 'number' && !Number.isNaN(valueAsInt) && valueAsInt > 0) {
|
||||
setDeadline(valueAsInt)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -195,14 +191,15 @@ export default function SlippageTabs({ rawSlippage, setRawSlippage, deadline, se
|
||||
</span>
|
||||
</SlippageEmojiContainer>
|
||||
) : null}
|
||||
{/* https://github.com/DefinitelyTyped/DefinitelyTyped/issues/30451 */}
|
||||
<Input
|
||||
ref={inputRef}
|
||||
ref={inputRef as any}
|
||||
placeholder={(rawSlippage / 100).toFixed(2)}
|
||||
value={slippageInput}
|
||||
onBlur={() => {
|
||||
parseCustomSlippage({ target: { value: (rawSlippage / 100).toFixed(2) } })
|
||||
parseCustomSlippage((rawSlippage / 100).toFixed(2))
|
||||
}}
|
||||
onChange={parseCustomSlippage}
|
||||
onChange={e => parseCustomSlippage(e.target.value)}
|
||||
color={!slippageInputIsValid ? 'red' : ''}
|
||||
/>
|
||||
%
|
||||
@@ -238,11 +235,11 @@ export default function SlippageTabs({ rawSlippage, setRawSlippage, deadline, se
|
||||
<Input
|
||||
color={!!deadlineError ? 'red' : undefined}
|
||||
onBlur={() => {
|
||||
parseCustomDeadline({ target: { value: (deadline / 60).toString() } })
|
||||
parseCustomDeadline((deadline / 60).toString())
|
||||
}}
|
||||
placeholder={(deadline / 60).toString()}
|
||||
value={deadlineInput}
|
||||
onChange={parseCustomDeadline}
|
||||
onChange={e => parseCustomDeadline(e.target.value)}
|
||||
/>
|
||||
</OptionCustom>
|
||||
<TYPE.body style={{ paddingLeft: '8px' }} fontSize={14}>
|
||||
@@ -73,7 +73,7 @@ const SubHeader = styled.div`
|
||||
font-size: 12px;
|
||||
`
|
||||
|
||||
const IconWrapper = styled.div<{ size?: number }>`
|
||||
const IconWrapper = styled.div<{ size?: number | null }>`
|
||||
${({ theme }) => theme.flexColumnNoWrap};
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@@ -90,7 +90,7 @@ const IconWrapper = styled.div<{ size?: number }>`
|
||||
export default function Option({
|
||||
link = null,
|
||||
clickable = true,
|
||||
size = null,
|
||||
size,
|
||||
onClick = null,
|
||||
color,
|
||||
header,
|
||||
|
||||
@@ -86,7 +86,7 @@ export default function PendingView({
|
||||
<ErrorButton
|
||||
onClick={() => {
|
||||
setPendingError(false)
|
||||
tryActivation(connector)
|
||||
connector && tryActivation(connector)
|
||||
}}
|
||||
>
|
||||
Try Again
|
||||
|
||||
@@ -17,6 +17,7 @@ import { ReactComponent as Close } from '../../assets/images/x.svg'
|
||||
import { injected, fortmatic, portis } from '../../connectors'
|
||||
import { OVERLAY_READY } from '../../connectors/Fortmatic'
|
||||
import { WalletConnectConnector } from '@web3-react/walletconnect-connector'
|
||||
import { AbstractConnector } from '@web3-react/abstract-connector'
|
||||
|
||||
const CloseIcon = styled.div`
|
||||
position: absolute;
|
||||
@@ -128,7 +129,7 @@ export default function WalletModal({
|
||||
|
||||
const [walletView, setWalletView] = useState(WALLET_VIEWS.ACCOUNT)
|
||||
|
||||
const [pendingWallet, setPendingWallet] = useState()
|
||||
const [pendingWallet, setPendingWallet] = useState<AbstractConnector | undefined>()
|
||||
|
||||
const [pendingError, setPendingError] = useState<boolean>()
|
||||
|
||||
@@ -161,7 +162,7 @@ export default function WalletModal({
|
||||
}
|
||||
}, [setWalletView, active, error, connector, walletModalOpen, activePrevious, connectorPrevious])
|
||||
|
||||
const tryActivation = async connector => {
|
||||
const tryActivation = async (connector: AbstractConnector | undefined) => {
|
||||
let name = ''
|
||||
Object.keys(SUPPORTED_WALLETS).map(key => {
|
||||
if (connector === SUPPORTED_WALLETS[key].connector) {
|
||||
@@ -183,13 +184,14 @@ export default function WalletModal({
|
||||
connector.walletConnectProvider = undefined
|
||||
}
|
||||
|
||||
activate(connector, undefined, true).catch(error => {
|
||||
if (error instanceof UnsupportedChainIdError) {
|
||||
activate(connector) // a little janky...can't use setError because the connector isn't set
|
||||
} else {
|
||||
setPendingError(true)
|
||||
}
|
||||
})
|
||||
connector &&
|
||||
activate(connector, undefined, true).catch(error => {
|
||||
if (error instanceof UnsupportedChainIdError) {
|
||||
activate(connector) // a little janky...can't use setError because the connector isn't set
|
||||
} else {
|
||||
setPendingError(true)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// close wallet modal if fortmatic modal is active
|
||||
@@ -349,9 +351,7 @@ export default function WalletModal({
|
||||
{walletView !== WALLET_VIEWS.PENDING && (
|
||||
<Blurb>
|
||||
<span>New to Ethereum? </span>{' '}
|
||||
<ExternalLink href="https://ethereum.org/use/#3-what-is-a-wallet-and-which-one-should-i-use">
|
||||
Learn more about wallets
|
||||
</ExternalLink>
|
||||
<ExternalLink href="https://ethereum.org/wallets/">Learn more about wallets</ExternalLink>
|
||||
</Blurb>
|
||||
)}
|
||||
</ContentWrapper>
|
||||
@@ -360,7 +360,7 @@ export default function WalletModal({
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal isOpen={walletModalOpen} onDismiss={toggleWalletModal} minHeight={null} maxHeight={90}>
|
||||
<Modal isOpen={walletModalOpen} onDismiss={toggleWalletModal} minHeight={false} maxHeight={90}>
|
||||
<Wrapper>{getModalContent()}</Wrapper>
|
||||
</Modal>
|
||||
)
|
||||
|
||||
@@ -19,7 +19,7 @@ const Message = styled.h2`
|
||||
color: ${({ theme }) => theme.secondary1};
|
||||
`
|
||||
|
||||
export default function Web3ReactManager({ children }) {
|
||||
export default function Web3ReactManager({ children }: { children: JSX.Element }) {
|
||||
const { t } = useTranslation()
|
||||
const { active } = useWeb3React()
|
||||
const { active: networkActive, error: networkError, activate: activateNetwork } = useWeb3React(NetworkContextName)
|
||||
|
||||
@@ -1,28 +1,29 @@
|
||||
import React, { useMemo } from 'react'
|
||||
import styled, { css } from 'styled-components'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useWeb3React, UnsupportedChainIdError } from '@web3-react/core'
|
||||
import { AbstractConnector } from '@web3-react/abstract-connector'
|
||||
import { UnsupportedChainIdError, useWeb3React } from '@web3-react/core'
|
||||
import { darken, lighten } from 'polished'
|
||||
import React, { useMemo } from 'react'
|
||||
import { Activity } from 'react-feather'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled, { css } from 'styled-components'
|
||||
import CoinbaseWalletIcon from '../../assets/images/coinbaseWalletIcon.svg'
|
||||
import FortmaticIcon from '../../assets/images/fortmaticIcon.png'
|
||||
import PortisIcon from '../../assets/images/portisIcon.png'
|
||||
import WalletConnectIcon from '../../assets/images/walletConnectIcon.svg'
|
||||
import { fortmatic, injected, portis, walletconnect, walletlink } from '../../connectors'
|
||||
import { NetworkContextName } from '../../constants'
|
||||
import useENSName from '../../hooks/useENSName'
|
||||
import { useHasSocks } from '../../hooks/useSocksBalance'
|
||||
import { useWalletModalToggle } from '../../state/application/hooks'
|
||||
import { isTransactionRecent, useAllTransactions } from '../../state/transactions/hooks'
|
||||
import { TransactionDetails } from '../../state/transactions/reducer'
|
||||
import { shortenAddress } from '../../utils'
|
||||
import { ButtonSecondary } from '../Button'
|
||||
|
||||
import Identicon from '../Identicon'
|
||||
import PortisIcon from '../../assets/images/portisIcon.png'
|
||||
import WalletModal from '../WalletModal'
|
||||
import { ButtonSecondary } from '../Button'
|
||||
import FortmaticIcon from '../../assets/images/fortmaticIcon.png'
|
||||
import WalletConnectIcon from '../../assets/images/walletConnectIcon.svg'
|
||||
import CoinbaseWalletIcon from '../../assets/images/coinbaseWalletIcon.svg'
|
||||
import Loader from '../Loader'
|
||||
|
||||
import { RowBetween } from '../Row'
|
||||
import { shortenAddress } from '../../utils'
|
||||
import { useAllTransactions } from '../../state/transactions/hooks'
|
||||
import { NetworkContextName } from '../../constants'
|
||||
import { injected, walletconnect, walletlink, fortmatic, portis } from '../../connectors'
|
||||
import Loader from '../Loader'
|
||||
import WalletModal from '../WalletModal'
|
||||
|
||||
const IconWrapper = styled.div<{ size?: number }>`
|
||||
${({ theme }) => theme.flexColumnNoWrap};
|
||||
@@ -118,104 +119,114 @@ const NetworkIcon = styled(Activity)`
|
||||
`
|
||||
|
||||
// we want the latest one to come first, so return negative if a is after b
|
||||
function newTranscationsFirst(a: TransactionDetails, b: TransactionDetails) {
|
||||
function newTransactionsFirst(a: TransactionDetails, b: TransactionDetails) {
|
||||
return b.addedTime - a.addedTime
|
||||
}
|
||||
|
||||
function recentTransactionsOnly(a: TransactionDetails) {
|
||||
return new Date().getTime() - a.addedTime < 86_400_000
|
||||
}
|
||||
|
||||
const SOCK = (
|
||||
<span role="img" aria-label="has socks emoji" style={{ marginTop: -4, marginBottom: -4 }}>
|
||||
🧦
|
||||
</span>
|
||||
)
|
||||
|
||||
export default function Web3Status() {
|
||||
const { t } = useTranslation()
|
||||
const { active, account, connector, error } = useWeb3React()
|
||||
const contextNetwork = useWeb3React(NetworkContextName)
|
||||
// eslint-disable-next-line react/prop-types
|
||||
function StatusIcon({ connector }: { connector: AbstractConnector }) {
|
||||
if (connector === injected) {
|
||||
return <Identicon />
|
||||
} else if (connector === walletconnect) {
|
||||
return (
|
||||
<IconWrapper size={16}>
|
||||
<img src={WalletConnectIcon} alt={''} />
|
||||
</IconWrapper>
|
||||
)
|
||||
} else if (connector === walletlink) {
|
||||
return (
|
||||
<IconWrapper size={16}>
|
||||
<img src={CoinbaseWalletIcon} alt={''} />
|
||||
</IconWrapper>
|
||||
)
|
||||
} else if (connector === fortmatic) {
|
||||
return (
|
||||
<IconWrapper size={16}>
|
||||
<img src={FortmaticIcon} alt={''} />
|
||||
</IconWrapper>
|
||||
)
|
||||
} else if (connector === portis) {
|
||||
return (
|
||||
<IconWrapper size={16}>
|
||||
<img src={PortisIcon} alt={''} />
|
||||
</IconWrapper>
|
||||
)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
const { ENSName } = useENSName(account)
|
||||
function Web3StatusInner() {
|
||||
const { t } = useTranslation()
|
||||
const { account, connector, error } = useWeb3React()
|
||||
|
||||
const { ENSName } = useENSName(account ?? undefined)
|
||||
|
||||
const allTransactions = useAllTransactions()
|
||||
|
||||
const sortedRecentTransactions = useMemo(() => {
|
||||
const txs = Object.values(allTransactions)
|
||||
return txs.filter(recentTransactionsOnly).sort(newTranscationsFirst)
|
||||
return txs.filter(isTransactionRecent).sort(newTransactionsFirst)
|
||||
}, [allTransactions])
|
||||
|
||||
const pending = sortedRecentTransactions.filter(tx => !tx.receipt).map(tx => tx.hash)
|
||||
const confirmed = sortedRecentTransactions.filter(tx => tx.receipt).map(tx => tx.hash)
|
||||
|
||||
const hasPendingTransactions = !!pending.length
|
||||
const hasSocks = useHasSocks()
|
||||
const toggleWalletModal = useWalletModalToggle()
|
||||
|
||||
// handle the logo we want to show with the account
|
||||
function getStatusIcon() {
|
||||
if (connector === injected) {
|
||||
return <Identicon />
|
||||
} else if (connector === walletconnect) {
|
||||
return (
|
||||
<IconWrapper size={16}>
|
||||
<img src={WalletConnectIcon} alt={''} />
|
||||
</IconWrapper>
|
||||
)
|
||||
} else if (connector === walletlink) {
|
||||
return (
|
||||
<IconWrapper size={16}>
|
||||
<img src={CoinbaseWalletIcon} alt={''} />
|
||||
</IconWrapper>
|
||||
)
|
||||
} else if (connector === fortmatic) {
|
||||
return (
|
||||
<IconWrapper size={16}>
|
||||
<img src={FortmaticIcon} alt={''} />
|
||||
</IconWrapper>
|
||||
)
|
||||
} else if (connector === portis) {
|
||||
return (
|
||||
<IconWrapper size={16}>
|
||||
<img src={PortisIcon} alt={''} />
|
||||
</IconWrapper>
|
||||
)
|
||||
}
|
||||
if (account) {
|
||||
return (
|
||||
<Web3StatusConnected id="web3-status-connected" onClick={toggleWalletModal} pending={hasPendingTransactions}>
|
||||
{hasPendingTransactions ? (
|
||||
<RowBetween>
|
||||
<Text>{pending?.length} Pending</Text> <Loader stroke="white" />
|
||||
</RowBetween>
|
||||
) : (
|
||||
<>
|
||||
{hasSocks ? SOCK : null}
|
||||
<Text>{ENSName || shortenAddress(account)}</Text>
|
||||
</>
|
||||
)}
|
||||
{!hasPendingTransactions && connector && <StatusIcon connector={connector} />}
|
||||
</Web3StatusConnected>
|
||||
)
|
||||
} else if (error) {
|
||||
return (
|
||||
<Web3StatusError onClick={toggleWalletModal}>
|
||||
<NetworkIcon />
|
||||
<Text>{error instanceof UnsupportedChainIdError ? 'Wrong Network' : 'Error'}</Text>
|
||||
</Web3StatusError>
|
||||
)
|
||||
} else {
|
||||
return (
|
||||
<Web3StatusConnect id="connect-wallet" onClick={toggleWalletModal} faded={!account}>
|
||||
<Text>{t('Connect to a wallet')}</Text>
|
||||
</Web3StatusConnect>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
function getWeb3Status() {
|
||||
if (account) {
|
||||
return (
|
||||
<Web3StatusConnected id="web3-status-connected" onClick={toggleWalletModal} pending={hasPendingTransactions}>
|
||||
{hasPendingTransactions ? (
|
||||
<RowBetween>
|
||||
<Text>{pending?.length} Pending</Text> <Loader stroke="white" />
|
||||
</RowBetween>
|
||||
) : (
|
||||
<>
|
||||
{hasSocks ? SOCK : null}
|
||||
<Text>{ENSName || shortenAddress(account)}</Text>
|
||||
</>
|
||||
)}
|
||||
{!hasPendingTransactions && getStatusIcon()}
|
||||
</Web3StatusConnected>
|
||||
)
|
||||
} else if (error) {
|
||||
return (
|
||||
<Web3StatusError onClick={toggleWalletModal}>
|
||||
<NetworkIcon />
|
||||
<Text>{error instanceof UnsupportedChainIdError ? 'Wrong Network' : 'Error'}</Text>
|
||||
</Web3StatusError>
|
||||
)
|
||||
} else {
|
||||
return (
|
||||
<Web3StatusConnect id="connect-wallet" onClick={toggleWalletModal} faded={!account}>
|
||||
<Text>{t('Connect to a wallet')}</Text>
|
||||
</Web3StatusConnect>
|
||||
)
|
||||
}
|
||||
}
|
||||
export default function Web3Status() {
|
||||
const { active, account } = useWeb3React()
|
||||
const contextNetwork = useWeb3React(NetworkContextName)
|
||||
|
||||
const { ENSName } = useENSName(account ?? undefined)
|
||||
|
||||
const allTransactions = useAllTransactions()
|
||||
|
||||
const sortedRecentTransactions = useMemo(() => {
|
||||
const txs = Object.values(allTransactions)
|
||||
return txs.filter(isTransactionRecent).sort(newTransactionsFirst)
|
||||
}, [allTransactions])
|
||||
|
||||
const pending = sortedRecentTransactions.filter(tx => !tx.receipt).map(tx => tx.hash)
|
||||
const confirmed = sortedRecentTransactions.filter(tx => tx.receipt).map(tx => tx.hash)
|
||||
|
||||
if (!contextNetwork.active && !active) {
|
||||
return null
|
||||
@@ -223,8 +234,8 @@ export default function Web3Status() {
|
||||
|
||||
return (
|
||||
<>
|
||||
{getWeb3Status()}
|
||||
<WalletModal ENSName={ENSName} pendingTransactions={pending} confirmedTransactions={confirmed} />
|
||||
<Web3StatusInner />
|
||||
<WalletModal ENSName={ENSName ?? undefined} pendingTransactions={pending} confirmedTransactions={confirmed} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import ReactGA from 'react-ga'
|
||||
import { RouteComponentProps } from 'react-router-dom'
|
||||
|
||||
// fires a GA pageview every time the route changes
|
||||
export default function GoogleAnalyticsReporter({ location: { pathname, search } }: RouteComponentProps) {
|
||||
export default function GoogleAnalyticsReporter({ location: { pathname, search } }: RouteComponentProps): null {
|
||||
useEffect(() => {
|
||||
ReactGA.pageview(`${pathname}${search}`)
|
||||
}, [pathname, search])
|
||||
|
||||
@@ -73,23 +73,27 @@ export function AdvancedSwapDetails({ trade }: AdvancedSwapDetailsProps) {
|
||||
|
||||
const [allowedSlippage] = useUserSlippageTolerance()
|
||||
|
||||
const showRoute = trade?.route?.path?.length > 2
|
||||
const showRoute = Boolean(trade && trade.route.path.length > 2)
|
||||
|
||||
return (
|
||||
<AutoColumn gap="md">
|
||||
{trade && <TradeSummary trade={trade} allowedSlippage={allowedSlippage} />}
|
||||
{showRoute && (
|
||||
{trade && (
|
||||
<>
|
||||
<SectionBreak />
|
||||
<AutoColumn style={{ padding: '0 24px' }}>
|
||||
<RowFixed>
|
||||
<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>
|
||||
<SwapRoute trade={trade} />
|
||||
</AutoColumn>
|
||||
<TradeSummary trade={trade} allowedSlippage={allowedSlippage} />
|
||||
{showRoute && (
|
||||
<>
|
||||
<SectionBreak />
|
||||
<AutoColumn style={{ padding: '0 24px' }}>
|
||||
<RowFixed>
|
||||
<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>
|
||||
<SwapRoute trade={trade} />
|
||||
</AutoColumn>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</AutoColumn>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react'
|
||||
import styled from 'styled-components'
|
||||
import useLast from '../../hooks/useLast'
|
||||
import { useLastTruthy } from '../../hooks/useLast'
|
||||
import { AdvancedSwapDetails, AdvancedSwapDetailsProps } from './AdvancedSwapDetails'
|
||||
|
||||
const AdvancedDetailsFooter = styled.div<{ show: boolean }>`
|
||||
@@ -20,11 +20,11 @@ const AdvancedDetailsFooter = styled.div<{ show: boolean }>`
|
||||
`
|
||||
|
||||
export default function AdvancedSwapDetailsDropdown({ trade, ...rest }: AdvancedSwapDetailsProps) {
|
||||
const lastTrade = useLast(trade)
|
||||
const lastTrade = useLastTruthy(trade)
|
||||
|
||||
return (
|
||||
<AdvancedDetailsFooter show={Boolean(trade)}>
|
||||
<AdvancedSwapDetails {...rest} trade={trade ?? lastTrade} />
|
||||
<AdvancedSwapDetails {...rest} trade={trade ?? lastTrade ?? undefined} />
|
||||
</AdvancedDetailsFooter>
|
||||
)
|
||||
}
|
||||
|
||||
109
src/components/swap/ConfirmSwapModal.tsx
Normal file
109
src/components/swap/ConfirmSwapModal.tsx
Normal file
@@ -0,0 +1,109 @@
|
||||
import { currencyEquals, Trade } from '@uniswap/sdk'
|
||||
import React, { useCallback, useMemo } from 'react'
|
||||
import TransactionConfirmationModal, {
|
||||
ConfirmationModalContent,
|
||||
TransactionErrorContent
|
||||
} from '../TransactionConfirmationModal'
|
||||
import SwapModalFooter from './SwapModalFooter'
|
||||
import SwapModalHeader from './SwapModalHeader'
|
||||
|
||||
/**
|
||||
* Returns true if the trade requires a confirmation of details before we can submit it
|
||||
* @param tradeA trade A
|
||||
* @param tradeB trade B
|
||||
*/
|
||||
function tradeMeaningfullyDiffers(tradeA: Trade, tradeB: Trade): boolean {
|
||||
return (
|
||||
tradeA.tradeType !== tradeB.tradeType ||
|
||||
!currencyEquals(tradeA.inputAmount.currency, tradeB.inputAmount.currency) ||
|
||||
!tradeA.inputAmount.equalTo(tradeB.inputAmount) ||
|
||||
!currencyEquals(tradeA.outputAmount.currency, tradeB.outputAmount.currency) ||
|
||||
!tradeA.outputAmount.equalTo(tradeB.outputAmount)
|
||||
)
|
||||
}
|
||||
|
||||
export default function ConfirmSwapModal({
|
||||
trade,
|
||||
originalTrade,
|
||||
onAcceptChanges,
|
||||
allowedSlippage,
|
||||
onConfirm,
|
||||
onDismiss,
|
||||
recipient,
|
||||
swapErrorMessage,
|
||||
isOpen,
|
||||
attemptingTxn,
|
||||
txHash
|
||||
}: {
|
||||
isOpen: boolean
|
||||
trade: Trade | undefined
|
||||
originalTrade: Trade | undefined
|
||||
attemptingTxn: boolean
|
||||
txHash: string | undefined
|
||||
recipient: string | null
|
||||
allowedSlippage: number
|
||||
onAcceptChanges: () => void
|
||||
onConfirm: () => void
|
||||
swapErrorMessage: string | undefined
|
||||
onDismiss: () => void
|
||||
}) {
|
||||
const showAcceptChanges = useMemo(
|
||||
() => Boolean(trade && originalTrade && tradeMeaningfullyDiffers(trade, originalTrade)),
|
||||
[originalTrade, trade]
|
||||
)
|
||||
|
||||
const modalHeader = useCallback(() => {
|
||||
return trade ? (
|
||||
<SwapModalHeader
|
||||
trade={trade}
|
||||
allowedSlippage={allowedSlippage}
|
||||
recipient={recipient}
|
||||
showAcceptChanges={showAcceptChanges}
|
||||
onAcceptChanges={onAcceptChanges}
|
||||
/>
|
||||
) : null
|
||||
}, [allowedSlippage, onAcceptChanges, recipient, showAcceptChanges, trade])
|
||||
|
||||
const modalBottom = useCallback(() => {
|
||||
return trade ? (
|
||||
<SwapModalFooter
|
||||
onConfirm={onConfirm}
|
||||
trade={trade}
|
||||
disabledConfirm={showAcceptChanges}
|
||||
swapErrorMessage={swapErrorMessage}
|
||||
allowedSlippage={allowedSlippage}
|
||||
/>
|
||||
) : null
|
||||
}, [allowedSlippage, onConfirm, showAcceptChanges, swapErrorMessage, trade])
|
||||
|
||||
// text to show while loading
|
||||
const pendingText = `Swapping ${trade?.inputAmount?.toSignificant(6)} ${
|
||||
trade?.inputAmount?.currency?.symbol
|
||||
} for ${trade?.outputAmount?.toSignificant(6)} ${trade?.outputAmount?.currency?.symbol}`
|
||||
|
||||
const confirmationContent = useCallback(
|
||||
() =>
|
||||
swapErrorMessage ? (
|
||||
<TransactionErrorContent onDismiss={onDismiss} message={swapErrorMessage} />
|
||||
) : (
|
||||
<ConfirmationModalContent
|
||||
title="Confirm Swap"
|
||||
onDismiss={onDismiss}
|
||||
topContent={modalHeader}
|
||||
bottomContent={modalBottom}
|
||||
/>
|
||||
),
|
||||
[onDismiss, modalBottom, modalHeader, swapErrorMessage]
|
||||
)
|
||||
|
||||
return (
|
||||
<TransactionConfirmationModal
|
||||
isOpen={isOpen}
|
||||
onDismiss={onDismiss}
|
||||
attemptingTxn={attemptingTxn}
|
||||
hash={txHash}
|
||||
content={confirmationContent}
|
||||
pendingText={pendingText}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -4,10 +4,13 @@ import { ONE_BIPS } from '../../constants'
|
||||
import { warningSeverity } from '../../utils/prices'
|
||||
import { ErrorText } from './styleds'
|
||||
|
||||
/**
|
||||
* Formatted version of price impact text with warning colors
|
||||
*/
|
||||
export default function FormattedPriceImpact({ priceImpact }: { priceImpact?: Percent }) {
|
||||
return (
|
||||
<ErrorText fontWeight={500} fontSize={14} severity={warningSeverity(priceImpact)}>
|
||||
{priceImpact?.lessThan(ONE_BIPS) ? '<0.01%' : `${priceImpact?.toFixed(2)}%` ?? '-'}
|
||||
{priceImpact ? (priceImpact.lessThan(ONE_BIPS) ? '<0.01%' : `${priceImpact.toFixed(2)}%`) : '-'}
|
||||
</ErrorText>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,46 +1,44 @@
|
||||
import { CurrencyAmount, Percent, Trade, TradeType } from '@uniswap/sdk'
|
||||
import React, { useContext } from 'react'
|
||||
import { Trade, TradeType } from '@uniswap/sdk'
|
||||
import React, { useContext, useMemo, useState } from 'react'
|
||||
import { Repeat } from 'react-feather'
|
||||
import { Text } from 'rebass'
|
||||
import { ThemeContext } from 'styled-components'
|
||||
import { Field } from '../../state/swap/actions'
|
||||
import { TYPE } from '../../theme'
|
||||
import { formatExecutionPrice } from '../../utils/prices'
|
||||
import {
|
||||
computeSlippageAdjustedAmounts,
|
||||
computeTradePriceBreakdown,
|
||||
formatExecutionPrice,
|
||||
warningSeverity
|
||||
} from '../../utils/prices'
|
||||
import { ButtonError } from '../Button'
|
||||
import { AutoColumn } from '../Column'
|
||||
import QuestionHelper from '../QuestionHelper'
|
||||
import { AutoRow, RowBetween, RowFixed } from '../Row'
|
||||
import FormattedPriceImpact from './FormattedPriceImpact'
|
||||
import { StyledBalanceMaxMini } from './styleds'
|
||||
import { StyledBalanceMaxMini, SwapCallbackError } from './styleds'
|
||||
|
||||
export default function SwapModalFooter({
|
||||
trade,
|
||||
showInverted,
|
||||
setShowInverted,
|
||||
severity,
|
||||
slippageAdjustedAmounts,
|
||||
onSwap,
|
||||
parsedAmounts,
|
||||
realizedLPFee,
|
||||
priceImpactWithoutFee,
|
||||
confirmText
|
||||
onConfirm,
|
||||
allowedSlippage,
|
||||
swapErrorMessage,
|
||||
disabledConfirm
|
||||
}: {
|
||||
trade?: Trade
|
||||
showInverted: boolean
|
||||
setShowInverted: (inverted: boolean) => void
|
||||
severity: number
|
||||
slippageAdjustedAmounts?: { [field in Field]?: CurrencyAmount }
|
||||
onSwap: () => any
|
||||
parsedAmounts?: { [field in Field]?: CurrencyAmount }
|
||||
realizedLPFee?: CurrencyAmount
|
||||
priceImpactWithoutFee?: Percent
|
||||
confirmText: string
|
||||
trade: Trade
|
||||
allowedSlippage: number
|
||||
onConfirm: () => void
|
||||
swapErrorMessage: string | undefined
|
||||
disabledConfirm: boolean
|
||||
}) {
|
||||
const [showInverted, setShowInverted] = useState<boolean>(false)
|
||||
const theme = useContext(ThemeContext)
|
||||
|
||||
if (!trade) {
|
||||
return null
|
||||
}
|
||||
const slippageAdjustedAmounts = useMemo(() => computeSlippageAdjustedAmounts(trade, allowedSlippage), [
|
||||
allowedSlippage,
|
||||
trade
|
||||
])
|
||||
const { priceImpactWithoutFee, realizedLPFee } = useMemo(() => computeTradePriceBreakdown(trade), [trade])
|
||||
const severity = warningSeverity(priceImpactWithoutFee)
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -71,23 +69,21 @@ export default function SwapModalFooter({
|
||||
<RowBetween>
|
||||
<RowFixed>
|
||||
<TYPE.black fontSize={14} fontWeight={400} color={theme.text2}>
|
||||
{trade?.tradeType === TradeType.EXACT_INPUT ? 'Minimum received' : 'Maximum sold'}
|
||||
{trade.tradeType === TradeType.EXACT_INPUT ? 'Minimum received' : 'Maximum sold'}
|
||||
</TYPE.black>
|
||||
<QuestionHelper text="Your transaction will revert if there is a large, unfavorable price movement before it is confirmed." />
|
||||
</RowFixed>
|
||||
<RowFixed>
|
||||
<TYPE.black fontSize={14}>
|
||||
{trade?.tradeType === TradeType.EXACT_INPUT
|
||||
{trade.tradeType === TradeType.EXACT_INPUT
|
||||
? slippageAdjustedAmounts[Field.OUTPUT]?.toSignificant(4) ?? '-'
|
||||
: slippageAdjustedAmounts[Field.INPUT]?.toSignificant(4) ?? '-'}
|
||||
</TYPE.black>
|
||||
{parsedAmounts[Field.OUTPUT] && parsedAmounts[Field.INPUT] && (
|
||||
<TYPE.black fontSize={14} marginLeft={'4px'}>
|
||||
{trade?.tradeType === TradeType.EXACT_INPUT
|
||||
? parsedAmounts[Field.OUTPUT]?.currency?.symbol
|
||||
: parsedAmounts[Field.INPUT]?.currency?.symbol}
|
||||
</TYPE.black>
|
||||
)}
|
||||
<TYPE.black fontSize={14} marginLeft={'4px'}>
|
||||
{trade.tradeType === TradeType.EXACT_INPUT
|
||||
? trade.outputAmount.currency.symbol
|
||||
: trade.inputAmount.currency.symbol}
|
||||
</TYPE.black>
|
||||
</RowFixed>
|
||||
</RowBetween>
|
||||
<RowBetween>
|
||||
@@ -107,17 +103,25 @@ export default function SwapModalFooter({
|
||||
<QuestionHelper text="A portion of each trade (0.30%) goes to liquidity providers as a protocol incentive." />
|
||||
</RowFixed>
|
||||
<TYPE.black fontSize={14}>
|
||||
{realizedLPFee ? realizedLPFee?.toSignificant(6) + ' ' + trade?.inputAmount?.currency?.symbol : '-'}
|
||||
{realizedLPFee ? realizedLPFee?.toSignificant(6) + ' ' + trade.inputAmount.currency.symbol : '-'}
|
||||
</TYPE.black>
|
||||
</RowBetween>
|
||||
</AutoColumn>
|
||||
|
||||
<AutoRow>
|
||||
<ButtonError onClick={onSwap} error={severity > 2} style={{ margin: '10px 0 0 0' }} id="confirm-swap-or-send">
|
||||
<ButtonError
|
||||
onClick={onConfirm}
|
||||
disabled={disabledConfirm}
|
||||
error={severity > 2}
|
||||
style={{ margin: '10px 0 0 0' }}
|
||||
id="confirm-swap-or-send"
|
||||
>
|
||||
<Text fontSize={20} fontWeight={500}>
|
||||
{confirmText}
|
||||
{severity > 2 ? 'Swap Anyway' : 'Confirm Swap'}
|
||||
</Text>
|
||||
</ButtonError>
|
||||
|
||||
{swapErrorMessage ? <SwapCallbackError error={swapErrorMessage} /> : null}
|
||||
</AutoRow>
|
||||
</>
|
||||
)
|
||||
|
||||
@@ -1,66 +1,107 @@
|
||||
import { Currency, CurrencyAmount } from '@uniswap/sdk'
|
||||
import React, { useContext } from 'react'
|
||||
import { ArrowDown } from 'react-feather'
|
||||
import { Trade, TradeType } from '@uniswap/sdk'
|
||||
import React, { useContext, useMemo } from 'react'
|
||||
import { ArrowDown, AlertTriangle } from 'react-feather'
|
||||
import { Text } from 'rebass'
|
||||
import { ThemeContext } from 'styled-components'
|
||||
import { Field } from '../../state/swap/actions'
|
||||
import { TYPE } from '../../theme'
|
||||
import { ButtonPrimary } from '../Button'
|
||||
import { isAddress, shortenAddress } from '../../utils'
|
||||
import { computeSlippageAdjustedAmounts, computeTradePriceBreakdown, warningSeverity } from '../../utils/prices'
|
||||
import { AutoColumn } from '../Column'
|
||||
import { RowBetween, RowFixed } from '../Row'
|
||||
import CurrencyLogo from '../CurrencyLogo'
|
||||
import { TruncatedText } from './styleds'
|
||||
import { RowBetween, RowFixed } from '../Row'
|
||||
import { TruncatedText, SwapShowAcceptChanges } from './styleds'
|
||||
|
||||
export default function SwapModalHeader({
|
||||
currencies,
|
||||
formattedAmounts,
|
||||
slippageAdjustedAmounts,
|
||||
priceImpactSeverity,
|
||||
independentField,
|
||||
recipient
|
||||
trade,
|
||||
allowedSlippage,
|
||||
recipient,
|
||||
showAcceptChanges,
|
||||
onAcceptChanges
|
||||
}: {
|
||||
currencies: { [field in Field]?: Currency }
|
||||
formattedAmounts: { [field in Field]?: string }
|
||||
slippageAdjustedAmounts: { [field in Field]?: CurrencyAmount }
|
||||
priceImpactSeverity: number
|
||||
independentField: Field
|
||||
trade: Trade
|
||||
allowedSlippage: number
|
||||
recipient: string | null
|
||||
showAcceptChanges: boolean
|
||||
onAcceptChanges: () => void
|
||||
}) {
|
||||
const slippageAdjustedAmounts = useMemo(() => computeSlippageAdjustedAmounts(trade, allowedSlippage), [
|
||||
trade,
|
||||
allowedSlippage
|
||||
])
|
||||
const { priceImpactWithoutFee } = useMemo(() => computeTradePriceBreakdown(trade), [trade])
|
||||
const priceImpactSeverity = warningSeverity(priceImpactWithoutFee)
|
||||
|
||||
const theme = useContext(ThemeContext)
|
||||
|
||||
return (
|
||||
<AutoColumn gap={'md'} style={{ marginTop: '20px' }}>
|
||||
<RowBetween align="flex-end">
|
||||
<TruncatedText fontSize={24} fontWeight={500}>
|
||||
{formattedAmounts[Field.INPUT]}
|
||||
</TruncatedText>
|
||||
<RowFixed gap="4px">
|
||||
<CurrencyLogo currency={currencies[Field.INPUT]} size={'24px'} />
|
||||
<RowFixed gap={'0px'}>
|
||||
<CurrencyLogo currency={trade.inputAmount.currency} size={'24px'} style={{ marginRight: '12px' }} />
|
||||
<TruncatedText
|
||||
fontSize={24}
|
||||
fontWeight={500}
|
||||
color={showAcceptChanges && trade.tradeType === TradeType.EXACT_OUTPUT ? theme.primary1 : ''}
|
||||
>
|
||||
{trade.inputAmount.toSignificant(6)}
|
||||
</TruncatedText>
|
||||
</RowFixed>
|
||||
<RowFixed gap={'0px'}>
|
||||
<Text fontSize={24} fontWeight={500} style={{ marginLeft: '10px' }}>
|
||||
{currencies[Field.INPUT]?.symbol}
|
||||
{trade.inputAmount.currency.symbol}
|
||||
</Text>
|
||||
</RowFixed>
|
||||
</RowBetween>
|
||||
<RowFixed>
|
||||
<ArrowDown size="16" color={theme.text2} />
|
||||
<ArrowDown size="16" color={theme.text2} style={{ marginLeft: '4px', minWidth: '16px' }} />
|
||||
</RowFixed>
|
||||
<RowBetween align="flex-end">
|
||||
<TruncatedText fontSize={24} fontWeight={500} color={priceImpactSeverity > 2 ? theme.red1 : ''}>
|
||||
{formattedAmounts[Field.OUTPUT]}
|
||||
</TruncatedText>
|
||||
<RowFixed gap="4px">
|
||||
<CurrencyLogo currency={currencies[Field.OUTPUT]} size={'24px'} />
|
||||
<RowFixed gap={'0px'}>
|
||||
<CurrencyLogo currency={trade.outputAmount.currency} size={'24px'} style={{ marginRight: '12px' }} />
|
||||
<TruncatedText
|
||||
fontSize={24}
|
||||
fontWeight={500}
|
||||
color={
|
||||
priceImpactSeverity > 2
|
||||
? theme.red1
|
||||
: showAcceptChanges && trade.tradeType === TradeType.EXACT_INPUT
|
||||
? theme.primary1
|
||||
: ''
|
||||
}
|
||||
>
|
||||
{trade.outputAmount.toSignificant(6)}
|
||||
</TruncatedText>
|
||||
</RowFixed>
|
||||
<RowFixed gap={'0px'}>
|
||||
<Text fontSize={24} fontWeight={500} style={{ marginLeft: '10px' }}>
|
||||
{currencies[Field.OUTPUT]?.symbol}
|
||||
{trade.outputAmount.currency.symbol}
|
||||
</Text>
|
||||
</RowFixed>
|
||||
</RowBetween>
|
||||
{showAcceptChanges ? (
|
||||
<SwapShowAcceptChanges justify="flex-start" gap={'0px'}>
|
||||
<RowBetween>
|
||||
<RowFixed>
|
||||
<AlertTriangle size={20} style={{ marginRight: '8px', minWidth: 24 }} />
|
||||
<TYPE.main color={theme.primary1}> Price Updated</TYPE.main>
|
||||
</RowFixed>
|
||||
<ButtonPrimary
|
||||
style={{ padding: '.5rem', width: 'fit-content', fontSize: '0.825rem', borderRadius: '12px' }}
|
||||
onClick={onAcceptChanges}
|
||||
>
|
||||
Accept
|
||||
</ButtonPrimary>
|
||||
</RowBetween>
|
||||
</SwapShowAcceptChanges>
|
||||
) : null}
|
||||
<AutoColumn justify="flex-start" gap="sm" style={{ padding: '12px 0 0 0px' }}>
|
||||
{independentField === Field.INPUT ? (
|
||||
{trade.tradeType === TradeType.EXACT_INPUT ? (
|
||||
<TYPE.italic textAlign="left" style={{ width: '100%' }}>
|
||||
{`Output is estimated. You will receive at least `}
|
||||
<b>
|
||||
{slippageAdjustedAmounts[Field.OUTPUT]?.toSignificant(6)} {currencies[Field.OUTPUT]?.symbol}
|
||||
{slippageAdjustedAmounts[Field.OUTPUT]?.toSignificant(6)} {trade.outputAmount.currency.symbol}
|
||||
</b>
|
||||
{' or the transaction will revert.'}
|
||||
</TYPE.italic>
|
||||
@@ -68,7 +109,7 @@ export default function SwapModalHeader({
|
||||
<TYPE.italic textAlign="left" style={{ width: '100%' }}>
|
||||
{`Input is estimated. You will sell at most `}
|
||||
<b>
|
||||
{slippageAdjustedAmounts[Field.INPUT]?.toSignificant(6)} {currencies[Field.INPUT]?.symbol}
|
||||
{slippageAdjustedAmounts[Field.INPUT]?.toSignificant(6)} {trade.inputAmount.currency.symbol}
|
||||
</b>
|
||||
{' or the transaction will revert.'}
|
||||
</TYPE.italic>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react'
|
||||
import { Currency, Price } from '@uniswap/sdk'
|
||||
import { Price } from '@uniswap/sdk'
|
||||
import { useContext } from 'react'
|
||||
import { Repeat } from 'react-feather'
|
||||
import { Text } from 'rebass'
|
||||
@@ -8,27 +8,19 @@ import { StyledBalanceMaxMini } from './styleds'
|
||||
|
||||
interface TradePriceProps {
|
||||
price?: Price
|
||||
inputCurrency?: Currency
|
||||
outputCurrency?: Currency
|
||||
showInverted: boolean
|
||||
setShowInverted: (showInverted: boolean) => void
|
||||
}
|
||||
|
||||
export default function TradePrice({
|
||||
price,
|
||||
inputCurrency,
|
||||
outputCurrency,
|
||||
showInverted,
|
||||
setShowInverted
|
||||
}: TradePriceProps) {
|
||||
export default function TradePrice({ price, showInverted, setShowInverted }: TradePriceProps) {
|
||||
const theme = useContext(ThemeContext)
|
||||
|
||||
const formattedPrice = showInverted ? price?.toSignificant(6) : price?.invert()?.toSignificant(6)
|
||||
|
||||
const show = Boolean(inputCurrency && outputCurrency)
|
||||
const show = Boolean(price?.baseCurrency && price?.quoteCurrency)
|
||||
const label = showInverted
|
||||
? `${outputCurrency?.symbol} per ${inputCurrency?.symbol}`
|
||||
: `${inputCurrency?.symbol} per ${outputCurrency?.symbol}`
|
||||
? `${price?.quoteCurrency?.symbol} per ${price?.baseCurrency?.symbol}`
|
||||
: `${price?.baseCurrency?.symbol} per ${price?.quoteCurrency?.symbol}`
|
||||
|
||||
return (
|
||||
<Text
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
// gathers additional user consent for a high price impact
|
||||
import { Percent } from '@uniswap/sdk'
|
||||
import { ALLOWED_PRICE_IMPACT_HIGH, PRICE_IMPACT_WITHOUT_FEE_CONFIRM_MIN } from '../../constants'
|
||||
|
||||
/**
|
||||
* Given the price impact, get user confirmation.
|
||||
*
|
||||
* @param priceImpactWithoutFee price impact of the trade without the fee.
|
||||
*/
|
||||
export default function confirmPriceImpactWithoutFee(priceImpactWithoutFee: Percent): boolean {
|
||||
if (!priceImpactWithoutFee.lessThan(PRICE_IMPACT_WITHOUT_FEE_CONFIRM_MIN)) {
|
||||
return (
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { transparentize } from 'polished'
|
||||
import React from 'react'
|
||||
import { AlertTriangle } from 'react-feather'
|
||||
import styled, { css } from 'styled-components'
|
||||
import { AutoColumn } from '../Column'
|
||||
import { Text } from 'rebass'
|
||||
|
||||
import NumericalInput from '../NumericalInput'
|
||||
import { AutoColumn } from '../Column'
|
||||
|
||||
export const Wrapper = styled.div`
|
||||
position: relative;
|
||||
@@ -29,8 +30,7 @@ export const SectionBreak = styled.div`
|
||||
`
|
||||
|
||||
export const BottomGrouping = styled.div`
|
||||
margin-top: 12px;
|
||||
position: relative;
|
||||
margin-top: 1rem;
|
||||
`
|
||||
|
||||
export const ErrorText = styled(Text)<{ severity?: 0 | 1 | 2 | 3 | 4 }>`
|
||||
@@ -44,21 +44,6 @@ export const ErrorText = styled(Text)<{ severity?: 0 | 1 | 2 | 3 | 4 }>`
|
||||
: theme.green1};
|
||||
`
|
||||
|
||||
export const InputGroup = styled(AutoColumn)`
|
||||
position: relative;
|
||||
padding: 40px 0 20px 0;
|
||||
`
|
||||
|
||||
export const StyledNumerical = styled(NumericalInput)`
|
||||
text-align: center;
|
||||
font-size: 48px;
|
||||
font-weight: 500px;
|
||||
width: 100%;
|
||||
|
||||
::placeholder {
|
||||
color: ${({ theme }) => theme.text4};
|
||||
}
|
||||
`
|
||||
export const StyledBalanceMaxMini = styled.button`
|
||||
height: 22px;
|
||||
width: 22px;
|
||||
@@ -112,3 +97,51 @@ export const Dots = styled.span`
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const SwapCallbackErrorInner = styled.div`
|
||||
background-color: ${({ theme }) => transparentize(0.9, theme.red1)};
|
||||
border-radius: 1rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 0.825rem;
|
||||
width: 100%;
|
||||
padding: 3rem 1.25rem 1rem 1rem;
|
||||
margin-top: -2rem;
|
||||
color: ${({ theme }) => theme.red1};
|
||||
z-index: -1;
|
||||
p {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
font-weight: 500;
|
||||
}
|
||||
`
|
||||
|
||||
const SwapCallbackErrorInnerAlertTriangle = styled.div`
|
||||
background-color: ${({ theme }) => transparentize(0.9, theme.red1)};
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 12px;
|
||||
border-radius: 12px;
|
||||
min-width: 48px;
|
||||
height: 48px;
|
||||
`
|
||||
|
||||
export function SwapCallbackError({ error }: { error: string }) {
|
||||
return (
|
||||
<SwapCallbackErrorInner>
|
||||
<SwapCallbackErrorInnerAlertTriangle>
|
||||
<AlertTriangle size={24} />
|
||||
</SwapCallbackErrorInnerAlertTriangle>
|
||||
<p>{error}</p>
|
||||
</SwapCallbackErrorInner>
|
||||
)
|
||||
}
|
||||
|
||||
export const SwapShowAcceptChanges = styled(AutoColumn)`
|
||||
background-color: ${({ theme }) => transparentize(0.9, theme.primary1)};
|
||||
color: ${({ theme }) => theme.primary1};
|
||||
padding: 0.5rem;
|
||||
border-radius: 12px;
|
||||
margin-top: 8px;
|
||||
`
|
||||
@@ -16,6 +16,7 @@ export class FortmaticConnector extends FortmaticConnectorCore {
|
||||
async activate() {
|
||||
if (!this.fortmatic) {
|
||||
const { default: Fortmatic } = await import('fortmatic')
|
||||
|
||||
const { apiKey, chainId } = this as any
|
||||
if (chainId in CHAIN_ID_NETWORK_ARGUMENT) {
|
||||
this.fortmatic = new Fortmatic(apiKey, CHAIN_ID_NETWORK_ARGUMENT[chainId as FormaticSupportedChains])
|
||||
|
||||
@@ -22,19 +22,83 @@ class RequestError extends Error {
|
||||
}
|
||||
}
|
||||
|
||||
interface BatchItem {
|
||||
request: { jsonrpc: '2.0'; id: number; method: string; params: unknown }
|
||||
resolve: (result: any) => void
|
||||
reject: (error: Error) => void
|
||||
}
|
||||
|
||||
class MiniRpcProvider implements AsyncSendable {
|
||||
public readonly isMetaMask: false = false
|
||||
public readonly chainId: number
|
||||
public readonly url: string
|
||||
public readonly host: string
|
||||
public readonly path: string
|
||||
public readonly batchWaitTimeMs: number
|
||||
|
||||
constructor(chainId: number, url: string) {
|
||||
private nextId = 1
|
||||
private batchTimeoutId: ReturnType<typeof setTimeout> | null = null
|
||||
private batch: BatchItem[] = []
|
||||
|
||||
constructor(chainId: number, url: string, batchWaitTimeMs?: number) {
|
||||
this.chainId = chainId
|
||||
this.url = url
|
||||
const parsed = new URL(url)
|
||||
this.host = parsed.host
|
||||
this.path = parsed.pathname
|
||||
// how long to wait to batch calls
|
||||
this.batchWaitTimeMs = batchWaitTimeMs ?? 50
|
||||
}
|
||||
|
||||
public readonly clearBatch = async () => {
|
||||
console.debug('Clearing batch', this.batch)
|
||||
const batch = this.batch
|
||||
this.batch = []
|
||||
this.batchTimeoutId = null
|
||||
let response: Response
|
||||
try {
|
||||
response = await fetch(this.url, {
|
||||
method: 'POST',
|
||||
headers: { 'content-type': 'application/json', accept: 'application/json' },
|
||||
body: JSON.stringify(batch.map(item => item.request))
|
||||
})
|
||||
} catch (error) {
|
||||
batch.forEach(({ reject }) => reject(new Error('Failed to send batch call')))
|
||||
return
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
batch.forEach(({ reject }) => reject(new RequestError(`${response.status}: ${response.statusText}`, -32000)))
|
||||
return
|
||||
}
|
||||
|
||||
let json
|
||||
try {
|
||||
json = await response.json()
|
||||
} catch (error) {
|
||||
batch.forEach(({ reject }) => reject(new Error('Failed to parse JSON response')))
|
||||
return
|
||||
}
|
||||
const byKey = batch.reduce<{ [id: number]: BatchItem }>((memo, current) => {
|
||||
memo[current.request.id] = current
|
||||
return memo
|
||||
}, {})
|
||||
for (const result of json) {
|
||||
const {
|
||||
resolve,
|
||||
reject,
|
||||
request: { method }
|
||||
} = byKey[result.id]
|
||||
if (resolve && reject) {
|
||||
if ('error' in result) {
|
||||
reject(new RequestError(result?.error?.message, result?.error?.code, result?.error?.data))
|
||||
} else if ('result' in result) {
|
||||
resolve(result.result)
|
||||
} else {
|
||||
reject(new RequestError(`Received unexpected JSON-RPC response to ${method} request.`, -32000, result))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public readonly sendAsync = (
|
||||
@@ -56,24 +120,20 @@ class MiniRpcProvider implements AsyncSendable {
|
||||
if (method === 'eth_chainId') {
|
||||
return `0x${this.chainId.toString(16)}`
|
||||
}
|
||||
const response = await fetch(this.url, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
jsonrpc: '2.0',
|
||||
id: 1,
|
||||
method,
|
||||
params
|
||||
const promise = new Promise((resolve, reject) => {
|
||||
this.batch.push({
|
||||
request: {
|
||||
jsonrpc: '2.0',
|
||||
id: this.nextId++,
|
||||
method,
|
||||
params
|
||||
},
|
||||
resolve,
|
||||
reject
|
||||
})
|
||||
})
|
||||
if (!response.ok) throw new RequestError(`${response.status}: ${response.statusText}`, -32000)
|
||||
const body = await response.json()
|
||||
if ('error' in body) {
|
||||
throw new RequestError(body?.error?.message, body?.error?.code, body?.error?.data)
|
||||
} else if ('result' in body) {
|
||||
return body.result
|
||||
} else {
|
||||
throw new RequestError(`Received unexpected JSON-RPC response to ${method} request.`, -32000, body)
|
||||
}
|
||||
this.batchTimeoutId = this.batchTimeoutId ?? setTimeout(this.clearBatch, this.batchWaitTimeMs)
|
||||
return promise
|
||||
}
|
||||
}
|
||||
|
||||
@@ -92,6 +152,10 @@ export class NetworkConnector extends AbstractConnector {
|
||||
}, {})
|
||||
}
|
||||
|
||||
public get provider(): MiniRpcProvider {
|
||||
return this.providers[this.currentChainId]
|
||||
}
|
||||
|
||||
public async activate(): Promise<ConnectorUpdate> {
|
||||
return { provider: this.providers[this.currentChainId], chainId: this.currentChainId, account: null }
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { Web3Provider } from '@ethersproject/providers'
|
||||
import { InjectedConnector } from '@web3-react/injected-connector'
|
||||
import { WalletConnectConnector } from '@web3-react/walletconnect-connector'
|
||||
import { WalletLinkConnector } from '@web3-react/walletlink-connector'
|
||||
@@ -10,14 +11,21 @@ const NETWORK_URL = process.env.REACT_APP_NETWORK_URL
|
||||
const FORMATIC_KEY = process.env.REACT_APP_FORTMATIC_KEY
|
||||
const PORTIS_ID = process.env.REACT_APP_PORTIS_ID
|
||||
|
||||
export const NETWORK_CHAIN_ID: number = parseInt(process.env.REACT_APP_CHAIN_ID ?? '1')
|
||||
|
||||
if (typeof NETWORK_URL === 'undefined') {
|
||||
throw new Error(`REACT_APP_NETWORK_URL must be a defined environment variable`)
|
||||
}
|
||||
|
||||
export const network = new NetworkConnector({
|
||||
urls: { [Number(process.env.REACT_APP_CHAIN_ID)]: NETWORK_URL }
|
||||
urls: { [NETWORK_CHAIN_ID]: NETWORK_URL }
|
||||
})
|
||||
|
||||
let networkLibrary: Web3Provider | undefined
|
||||
export function getNetworkLibrary(): Web3Provider {
|
||||
return (networkLibrary = networkLibrary ?? new Web3Provider(network.provider as any))
|
||||
}
|
||||
|
||||
export const injected = new InjectedConnector({
|
||||
supportedChainIds: [1, 3, 4, 5, 42]
|
||||
})
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
{
|
||||
"extends": "../../tsconfig.strict.json",
|
||||
"include": ["**/*"]
|
||||
}
|
||||
816
src/constants/abis/ens-public-resolver.json
Normal file
816
src/constants/abis/ens-public-resolver.json
Normal file
@@ -0,0 +1,816 @@
|
||||
[
|
||||
{
|
||||
"inputs": [
|
||||
{
|
||||
"internalType": "contract ENS",
|
||||
"name": "_ens",
|
||||
"type": "address"
|
||||
}
|
||||
],
|
||||
"payable": false,
|
||||
"stateMutability": "nonpayable",
|
||||
"type": "constructor"
|
||||
},
|
||||
{
|
||||
"anonymous": false,
|
||||
"inputs": [
|
||||
{
|
||||
"indexed": true,
|
||||
"internalType": "bytes32",
|
||||
"name": "node",
|
||||
"type": "bytes32"
|
||||
},
|
||||
{
|
||||
"indexed": true,
|
||||
"internalType": "uint256",
|
||||
"name": "contentType",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"name": "ABIChanged",
|
||||
"type": "event"
|
||||
},
|
||||
{
|
||||
"anonymous": false,
|
||||
"inputs": [
|
||||
{
|
||||
"indexed": true,
|
||||
"internalType": "bytes32",
|
||||
"name": "node",
|
||||
"type": "bytes32"
|
||||
},
|
||||
{
|
||||
"indexed": false,
|
||||
"internalType": "address",
|
||||
"name": "a",
|
||||
"type": "address"
|
||||
}
|
||||
],
|
||||
"name": "AddrChanged",
|
||||
"type": "event"
|
||||
},
|
||||
{
|
||||
"anonymous": false,
|
||||
"inputs": [
|
||||
{
|
||||
"indexed": true,
|
||||
"internalType": "bytes32",
|
||||
"name": "node",
|
||||
"type": "bytes32"
|
||||
},
|
||||
{
|
||||
"indexed": false,
|
||||
"internalType": "uint256",
|
||||
"name": "coinType",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"indexed": false,
|
||||
"internalType": "bytes",
|
||||
"name": "newAddress",
|
||||
"type": "bytes"
|
||||
}
|
||||
],
|
||||
"name": "AddressChanged",
|
||||
"type": "event"
|
||||
},
|
||||
{
|
||||
"anonymous": false,
|
||||
"inputs": [
|
||||
{
|
||||
"indexed": true,
|
||||
"internalType": "bytes32",
|
||||
"name": "node",
|
||||
"type": "bytes32"
|
||||
},
|
||||
{
|
||||
"indexed": true,
|
||||
"internalType": "address",
|
||||
"name": "owner",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"indexed": true,
|
||||
"internalType": "address",
|
||||
"name": "target",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"indexed": false,
|
||||
"internalType": "bool",
|
||||
"name": "isAuthorised",
|
||||
"type": "bool"
|
||||
}
|
||||
],
|
||||
"name": "AuthorisationChanged",
|
||||
"type": "event"
|
||||
},
|
||||
{
|
||||
"anonymous": false,
|
||||
"inputs": [
|
||||
{
|
||||
"indexed": true,
|
||||
"internalType": "bytes32",
|
||||
"name": "node",
|
||||
"type": "bytes32"
|
||||
},
|
||||
{
|
||||
"indexed": false,
|
||||
"internalType": "bytes",
|
||||
"name": "hash",
|
||||
"type": "bytes"
|
||||
}
|
||||
],
|
||||
"name": "ContenthashChanged",
|
||||
"type": "event"
|
||||
},
|
||||
{
|
||||
"anonymous": false,
|
||||
"inputs": [
|
||||
{
|
||||
"indexed": true,
|
||||
"internalType": "bytes32",
|
||||
"name": "node",
|
||||
"type": "bytes32"
|
||||
},
|
||||
{
|
||||
"indexed": false,
|
||||
"internalType": "bytes",
|
||||
"name": "name",
|
||||
"type": "bytes"
|
||||
},
|
||||
{
|
||||
"indexed": false,
|
||||
"internalType": "uint16",
|
||||
"name": "resource",
|
||||
"type": "uint16"
|
||||
},
|
||||
{
|
||||
"indexed": false,
|
||||
"internalType": "bytes",
|
||||
"name": "record",
|
||||
"type": "bytes"
|
||||
}
|
||||
],
|
||||
"name": "DNSRecordChanged",
|
||||
"type": "event"
|
||||
},
|
||||
{
|
||||
"anonymous": false,
|
||||
"inputs": [
|
||||
{
|
||||
"indexed": true,
|
||||
"internalType": "bytes32",
|
||||
"name": "node",
|
||||
"type": "bytes32"
|
||||
},
|
||||
{
|
||||
"indexed": false,
|
||||
"internalType": "bytes",
|
||||
"name": "name",
|
||||
"type": "bytes"
|
||||
},
|
||||
{
|
||||
"indexed": false,
|
||||
"internalType": "uint16",
|
||||
"name": "resource",
|
||||
"type": "uint16"
|
||||
}
|
||||
],
|
||||
"name": "DNSRecordDeleted",
|
||||
"type": "event"
|
||||
},
|
||||
{
|
||||
"anonymous": false,
|
||||
"inputs": [
|
||||
{
|
||||
"indexed": true,
|
||||
"internalType": "bytes32",
|
||||
"name": "node",
|
||||
"type": "bytes32"
|
||||
}
|
||||
],
|
||||
"name": "DNSZoneCleared",
|
||||
"type": "event"
|
||||
},
|
||||
{
|
||||
"anonymous": false,
|
||||
"inputs": [
|
||||
{
|
||||
"indexed": true,
|
||||
"internalType": "bytes32",
|
||||
"name": "node",
|
||||
"type": "bytes32"
|
||||
},
|
||||
{
|
||||
"indexed": true,
|
||||
"internalType": "bytes4",
|
||||
"name": "interfaceID",
|
||||
"type": "bytes4"
|
||||
},
|
||||
{
|
||||
"indexed": false,
|
||||
"internalType": "address",
|
||||
"name": "implementer",
|
||||
"type": "address"
|
||||
}
|
||||
],
|
||||
"name": "InterfaceChanged",
|
||||
"type": "event"
|
||||
},
|
||||
{
|
||||
"anonymous": false,
|
||||
"inputs": [
|
||||
{
|
||||
"indexed": true,
|
||||
"internalType": "bytes32",
|
||||
"name": "node",
|
||||
"type": "bytes32"
|
||||
},
|
||||
{
|
||||
"indexed": false,
|
||||
"internalType": "string",
|
||||
"name": "name",
|
||||
"type": "string"
|
||||
}
|
||||
],
|
||||
"name": "NameChanged",
|
||||
"type": "event"
|
||||
},
|
||||
{
|
||||
"anonymous": false,
|
||||
"inputs": [
|
||||
{
|
||||
"indexed": true,
|
||||
"internalType": "bytes32",
|
||||
"name": "node",
|
||||
"type": "bytes32"
|
||||
},
|
||||
{
|
||||
"indexed": false,
|
||||
"internalType": "bytes32",
|
||||
"name": "x",
|
||||
"type": "bytes32"
|
||||
},
|
||||
{
|
||||
"indexed": false,
|
||||
"internalType": "bytes32",
|
||||
"name": "y",
|
||||
"type": "bytes32"
|
||||
}
|
||||
],
|
||||
"name": "PubkeyChanged",
|
||||
"type": "event"
|
||||
},
|
||||
{
|
||||
"anonymous": false,
|
||||
"inputs": [
|
||||
{
|
||||
"indexed": true,
|
||||
"internalType": "bytes32",
|
||||
"name": "node",
|
||||
"type": "bytes32"
|
||||
},
|
||||
{
|
||||
"indexed": true,
|
||||
"internalType": "string",
|
||||
"name": "indexedKey",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"indexed": false,
|
||||
"internalType": "string",
|
||||
"name": "key",
|
||||
"type": "string"
|
||||
}
|
||||
],
|
||||
"name": "TextChanged",
|
||||
"type": "event"
|
||||
},
|
||||
{
|
||||
"constant": true,
|
||||
"inputs": [
|
||||
{
|
||||
"internalType": "bytes32",
|
||||
"name": "node",
|
||||
"type": "bytes32"
|
||||
},
|
||||
{
|
||||
"internalType": "uint256",
|
||||
"name": "contentTypes",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"name": "ABI",
|
||||
"outputs": [
|
||||
{
|
||||
"internalType": "uint256",
|
||||
"name": "",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"internalType": "bytes",
|
||||
"name": "",
|
||||
"type": "bytes"
|
||||
}
|
||||
],
|
||||
"payable": false,
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": true,
|
||||
"inputs": [
|
||||
{
|
||||
"internalType": "bytes32",
|
||||
"name": "node",
|
||||
"type": "bytes32"
|
||||
}
|
||||
],
|
||||
"name": "addr",
|
||||
"outputs": [
|
||||
{
|
||||
"internalType": "address payable",
|
||||
"name": "",
|
||||
"type": "address"
|
||||
}
|
||||
],
|
||||
"payable": false,
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": true,
|
||||
"inputs": [
|
||||
{
|
||||
"internalType": "bytes32",
|
||||
"name": "",
|
||||
"type": "bytes32"
|
||||
},
|
||||
{
|
||||
"internalType": "address",
|
||||
"name": "",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"internalType": "address",
|
||||
"name": "",
|
||||
"type": "address"
|
||||
}
|
||||
],
|
||||
"name": "authorisations",
|
||||
"outputs": [
|
||||
{
|
||||
"internalType": "bool",
|
||||
"name": "",
|
||||
"type": "bool"
|
||||
}
|
||||
],
|
||||
"payable": false,
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": false,
|
||||
"inputs": [
|
||||
{
|
||||
"internalType": "bytes32",
|
||||
"name": "node",
|
||||
"type": "bytes32"
|
||||
}
|
||||
],
|
||||
"name": "clearDNSZone",
|
||||
"outputs": [],
|
||||
"payable": false,
|
||||
"stateMutability": "nonpayable",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": true,
|
||||
"inputs": [
|
||||
{
|
||||
"internalType": "bytes32",
|
||||
"name": "node",
|
||||
"type": "bytes32"
|
||||
}
|
||||
],
|
||||
"name": "contenthash",
|
||||
"outputs": [
|
||||
{
|
||||
"internalType": "bytes",
|
||||
"name": "",
|
||||
"type": "bytes"
|
||||
}
|
||||
],
|
||||
"payable": false,
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": true,
|
||||
"inputs": [
|
||||
{
|
||||
"internalType": "bytes32",
|
||||
"name": "node",
|
||||
"type": "bytes32"
|
||||
},
|
||||
{
|
||||
"internalType": "bytes32",
|
||||
"name": "name",
|
||||
"type": "bytes32"
|
||||
},
|
||||
{
|
||||
"internalType": "uint16",
|
||||
"name": "resource",
|
||||
"type": "uint16"
|
||||
}
|
||||
],
|
||||
"name": "dnsRecord",
|
||||
"outputs": [
|
||||
{
|
||||
"internalType": "bytes",
|
||||
"name": "",
|
||||
"type": "bytes"
|
||||
}
|
||||
],
|
||||
"payable": false,
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": true,
|
||||
"inputs": [
|
||||
{
|
||||
"internalType": "bytes32",
|
||||
"name": "node",
|
||||
"type": "bytes32"
|
||||
},
|
||||
{
|
||||
"internalType": "bytes32",
|
||||
"name": "name",
|
||||
"type": "bytes32"
|
||||
}
|
||||
],
|
||||
"name": "hasDNSRecords",
|
||||
"outputs": [
|
||||
{
|
||||
"internalType": "bool",
|
||||
"name": "",
|
||||
"type": "bool"
|
||||
}
|
||||
],
|
||||
"payable": false,
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": true,
|
||||
"inputs": [
|
||||
{
|
||||
"internalType": "bytes32",
|
||||
"name": "node",
|
||||
"type": "bytes32"
|
||||
},
|
||||
{
|
||||
"internalType": "bytes4",
|
||||
"name": "interfaceID",
|
||||
"type": "bytes4"
|
||||
}
|
||||
],
|
||||
"name": "interfaceImplementer",
|
||||
"outputs": [
|
||||
{
|
||||
"internalType": "address",
|
||||
"name": "",
|
||||
"type": "address"
|
||||
}
|
||||
],
|
||||
"payable": false,
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": true,
|
||||
"inputs": [
|
||||
{
|
||||
"internalType": "bytes32",
|
||||
"name": "node",
|
||||
"type": "bytes32"
|
||||
}
|
||||
],
|
||||
"name": "name",
|
||||
"outputs": [
|
||||
{
|
||||
"internalType": "string",
|
||||
"name": "",
|
||||
"type": "string"
|
||||
}
|
||||
],
|
||||
"payable": false,
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": true,
|
||||
"inputs": [
|
||||
{
|
||||
"internalType": "bytes32",
|
||||
"name": "node",
|
||||
"type": "bytes32"
|
||||
}
|
||||
],
|
||||
"name": "pubkey",
|
||||
"outputs": [
|
||||
{
|
||||
"internalType": "bytes32",
|
||||
"name": "x",
|
||||
"type": "bytes32"
|
||||
},
|
||||
{
|
||||
"internalType": "bytes32",
|
||||
"name": "y",
|
||||
"type": "bytes32"
|
||||
}
|
||||
],
|
||||
"payable": false,
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": false,
|
||||
"inputs": [
|
||||
{
|
||||
"internalType": "bytes32",
|
||||
"name": "node",
|
||||
"type": "bytes32"
|
||||
},
|
||||
{
|
||||
"internalType": "uint256",
|
||||
"name": "contentType",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"internalType": "bytes",
|
||||
"name": "data",
|
||||
"type": "bytes"
|
||||
}
|
||||
],
|
||||
"name": "setABI",
|
||||
"outputs": [],
|
||||
"payable": false,
|
||||
"stateMutability": "nonpayable",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": false,
|
||||
"inputs": [
|
||||
{
|
||||
"internalType": "bytes32",
|
||||
"name": "node",
|
||||
"type": "bytes32"
|
||||
},
|
||||
{
|
||||
"internalType": "uint256",
|
||||
"name": "coinType",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"internalType": "bytes",
|
||||
"name": "a",
|
||||
"type": "bytes"
|
||||
}
|
||||
],
|
||||
"name": "setAddr",
|
||||
"outputs": [],
|
||||
"payable": false,
|
||||
"stateMutability": "nonpayable",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": false,
|
||||
"inputs": [
|
||||
{
|
||||
"internalType": "bytes32",
|
||||
"name": "node",
|
||||
"type": "bytes32"
|
||||
},
|
||||
{
|
||||
"internalType": "address",
|
||||
"name": "a",
|
||||
"type": "address"
|
||||
}
|
||||
],
|
||||
"name": "setAddr",
|
||||
"outputs": [],
|
||||
"payable": false,
|
||||
"stateMutability": "nonpayable",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": false,
|
||||
"inputs": [
|
||||
{
|
||||
"internalType": "bytes32",
|
||||
"name": "node",
|
||||
"type": "bytes32"
|
||||
},
|
||||
{
|
||||
"internalType": "address",
|
||||
"name": "target",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"internalType": "bool",
|
||||
"name": "isAuthorised",
|
||||
"type": "bool"
|
||||
}
|
||||
],
|
||||
"name": "setAuthorisation",
|
||||
"outputs": [],
|
||||
"payable": false,
|
||||
"stateMutability": "nonpayable",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": false,
|
||||
"inputs": [
|
||||
{
|
||||
"internalType": "bytes32",
|
||||
"name": "node",
|
||||
"type": "bytes32"
|
||||
},
|
||||
{
|
||||
"internalType": "bytes",
|
||||
"name": "hash",
|
||||
"type": "bytes"
|
||||
}
|
||||
],
|
||||
"name": "setContenthash",
|
||||
"outputs": [],
|
||||
"payable": false,
|
||||
"stateMutability": "nonpayable",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": false,
|
||||
"inputs": [
|
||||
{
|
||||
"internalType": "bytes32",
|
||||
"name": "node",
|
||||
"type": "bytes32"
|
||||
},
|
||||
{
|
||||
"internalType": "bytes",
|
||||
"name": "data",
|
||||
"type": "bytes"
|
||||
}
|
||||
],
|
||||
"name": "setDNSRecords",
|
||||
"outputs": [],
|
||||
"payable": false,
|
||||
"stateMutability": "nonpayable",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": false,
|
||||
"inputs": [
|
||||
{
|
||||
"internalType": "bytes32",
|
||||
"name": "node",
|
||||
"type": "bytes32"
|
||||
},
|
||||
{
|
||||
"internalType": "bytes4",
|
||||
"name": "interfaceID",
|
||||
"type": "bytes4"
|
||||
},
|
||||
{
|
||||
"internalType": "address",
|
||||
"name": "implementer",
|
||||
"type": "address"
|
||||
}
|
||||
],
|
||||
"name": "setInterface",
|
||||
"outputs": [],
|
||||
"payable": false,
|
||||
"stateMutability": "nonpayable",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": false,
|
||||
"inputs": [
|
||||
{
|
||||
"internalType": "bytes32",
|
||||
"name": "node",
|
||||
"type": "bytes32"
|
||||
},
|
||||
{
|
||||
"internalType": "string",
|
||||
"name": "name",
|
||||
"type": "string"
|
||||
}
|
||||
],
|
||||
"name": "setName",
|
||||
"outputs": [],
|
||||
"payable": false,
|
||||
"stateMutability": "nonpayable",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": false,
|
||||
"inputs": [
|
||||
{
|
||||
"internalType": "bytes32",
|
||||
"name": "node",
|
||||
"type": "bytes32"
|
||||
},
|
||||
{
|
||||
"internalType": "bytes32",
|
||||
"name": "x",
|
||||
"type": "bytes32"
|
||||
},
|
||||
{
|
||||
"internalType": "bytes32",
|
||||
"name": "y",
|
||||
"type": "bytes32"
|
||||
}
|
||||
],
|
||||
"name": "setPubkey",
|
||||
"outputs": [],
|
||||
"payable": false,
|
||||
"stateMutability": "nonpayable",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": false,
|
||||
"inputs": [
|
||||
{
|
||||
"internalType": "bytes32",
|
||||
"name": "node",
|
||||
"type": "bytes32"
|
||||
},
|
||||
{
|
||||
"internalType": "string",
|
||||
"name": "key",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"internalType": "string",
|
||||
"name": "value",
|
||||
"type": "string"
|
||||
}
|
||||
],
|
||||
"name": "setText",
|
||||
"outputs": [],
|
||||
"payable": false,
|
||||
"stateMutability": "nonpayable",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": true,
|
||||
"inputs": [
|
||||
{
|
||||
"internalType": "bytes4",
|
||||
"name": "interfaceID",
|
||||
"type": "bytes4"
|
||||
}
|
||||
],
|
||||
"name": "supportsInterface",
|
||||
"outputs": [
|
||||
{
|
||||
"internalType": "bool",
|
||||
"name": "",
|
||||
"type": "bool"
|
||||
}
|
||||
],
|
||||
"payable": false,
|
||||
"stateMutability": "pure",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": true,
|
||||
"inputs": [
|
||||
{
|
||||
"internalType": "bytes32",
|
||||
"name": "node",
|
||||
"type": "bytes32"
|
||||
},
|
||||
{
|
||||
"internalType": "string",
|
||||
"name": "key",
|
||||
"type": "string"
|
||||
}
|
||||
],
|
||||
"name": "text",
|
||||
"outputs": [
|
||||
{
|
||||
"internalType": "string",
|
||||
"name": "",
|
||||
"type": "string"
|
||||
}
|
||||
],
|
||||
"payable": false,
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
}
|
||||
]
|
||||
422
src/constants/abis/ens-registrar.json
Normal file
422
src/constants/abis/ens-registrar.json
Normal file
@@ -0,0 +1,422 @@
|
||||
[
|
||||
{
|
||||
"inputs": [
|
||||
{
|
||||
"internalType": "contract ENS",
|
||||
"name": "_old",
|
||||
"type": "address"
|
||||
}
|
||||
],
|
||||
"payable": false,
|
||||
"stateMutability": "nonpayable",
|
||||
"type": "constructor"
|
||||
},
|
||||
{
|
||||
"anonymous": false,
|
||||
"inputs": [
|
||||
{
|
||||
"indexed": true,
|
||||
"internalType": "address",
|
||||
"name": "owner",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"indexed": true,
|
||||
"internalType": "address",
|
||||
"name": "operator",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"indexed": false,
|
||||
"internalType": "bool",
|
||||
"name": "approved",
|
||||
"type": "bool"
|
||||
}
|
||||
],
|
||||
"name": "ApprovalForAll",
|
||||
"type": "event"
|
||||
},
|
||||
{
|
||||
"anonymous": false,
|
||||
"inputs": [
|
||||
{
|
||||
"indexed": true,
|
||||
"internalType": "bytes32",
|
||||
"name": "node",
|
||||
"type": "bytes32"
|
||||
},
|
||||
{
|
||||
"indexed": true,
|
||||
"internalType": "bytes32",
|
||||
"name": "label",
|
||||
"type": "bytes32"
|
||||
},
|
||||
{
|
||||
"indexed": false,
|
||||
"internalType": "address",
|
||||
"name": "owner",
|
||||
"type": "address"
|
||||
}
|
||||
],
|
||||
"name": "NewOwner",
|
||||
"type": "event"
|
||||
},
|
||||
{
|
||||
"anonymous": false,
|
||||
"inputs": [
|
||||
{
|
||||
"indexed": true,
|
||||
"internalType": "bytes32",
|
||||
"name": "node",
|
||||
"type": "bytes32"
|
||||
},
|
||||
{
|
||||
"indexed": false,
|
||||
"internalType": "address",
|
||||
"name": "resolver",
|
||||
"type": "address"
|
||||
}
|
||||
],
|
||||
"name": "NewResolver",
|
||||
"type": "event"
|
||||
},
|
||||
{
|
||||
"anonymous": false,
|
||||
"inputs": [
|
||||
{
|
||||
"indexed": true,
|
||||
"internalType": "bytes32",
|
||||
"name": "node",
|
||||
"type": "bytes32"
|
||||
},
|
||||
{
|
||||
"indexed": false,
|
||||
"internalType": "uint64",
|
||||
"name": "ttl",
|
||||
"type": "uint64"
|
||||
}
|
||||
],
|
||||
"name": "NewTTL",
|
||||
"type": "event"
|
||||
},
|
||||
{
|
||||
"anonymous": false,
|
||||
"inputs": [
|
||||
{
|
||||
"indexed": true,
|
||||
"internalType": "bytes32",
|
||||
"name": "node",
|
||||
"type": "bytes32"
|
||||
},
|
||||
{
|
||||
"indexed": false,
|
||||
"internalType": "address",
|
||||
"name": "owner",
|
||||
"type": "address"
|
||||
}
|
||||
],
|
||||
"name": "Transfer",
|
||||
"type": "event"
|
||||
},
|
||||
{
|
||||
"constant": true,
|
||||
"inputs": [
|
||||
{
|
||||
"internalType": "address",
|
||||
"name": "owner",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"internalType": "address",
|
||||
"name": "operator",
|
||||
"type": "address"
|
||||
}
|
||||
],
|
||||
"name": "isApprovedForAll",
|
||||
"outputs": [
|
||||
{
|
||||
"internalType": "bool",
|
||||
"name": "",
|
||||
"type": "bool"
|
||||
}
|
||||
],
|
||||
"payable": false,
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": true,
|
||||
"inputs": [],
|
||||
"name": "old",
|
||||
"outputs": [
|
||||
{
|
||||
"internalType": "contract ENS",
|
||||
"name": "",
|
||||
"type": "address"
|
||||
}
|
||||
],
|
||||
"payable": false,
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": true,
|
||||
"inputs": [
|
||||
{
|
||||
"internalType": "bytes32",
|
||||
"name": "node",
|
||||
"type": "bytes32"
|
||||
}
|
||||
],
|
||||
"name": "owner",
|
||||
"outputs": [
|
||||
{
|
||||
"internalType": "address",
|
||||
"name": "",
|
||||
"type": "address"
|
||||
}
|
||||
],
|
||||
"payable": false,
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": true,
|
||||
"inputs": [
|
||||
{
|
||||
"internalType": "bytes32",
|
||||
"name": "node",
|
||||
"type": "bytes32"
|
||||
}
|
||||
],
|
||||
"name": "recordExists",
|
||||
"outputs": [
|
||||
{
|
||||
"internalType": "bool",
|
||||
"name": "",
|
||||
"type": "bool"
|
||||
}
|
||||
],
|
||||
"payable": false,
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": true,
|
||||
"inputs": [
|
||||
{
|
||||
"internalType": "bytes32",
|
||||
"name": "node",
|
||||
"type": "bytes32"
|
||||
}
|
||||
],
|
||||
"name": "resolver",
|
||||
"outputs": [
|
||||
{
|
||||
"internalType": "address",
|
||||
"name": "",
|
||||
"type": "address"
|
||||
}
|
||||
],
|
||||
"payable": false,
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": false,
|
||||
"inputs": [
|
||||
{
|
||||
"internalType": "address",
|
||||
"name": "operator",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"internalType": "bool",
|
||||
"name": "approved",
|
||||
"type": "bool"
|
||||
}
|
||||
],
|
||||
"name": "setApprovalForAll",
|
||||
"outputs": [],
|
||||
"payable": false,
|
||||
"stateMutability": "nonpayable",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": false,
|
||||
"inputs": [
|
||||
{
|
||||
"internalType": "bytes32",
|
||||
"name": "node",
|
||||
"type": "bytes32"
|
||||
},
|
||||
{
|
||||
"internalType": "address",
|
||||
"name": "owner",
|
||||
"type": "address"
|
||||
}
|
||||
],
|
||||
"name": "setOwner",
|
||||
"outputs": [],
|
||||
"payable": false,
|
||||
"stateMutability": "nonpayable",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": false,
|
||||
"inputs": [
|
||||
{
|
||||
"internalType": "bytes32",
|
||||
"name": "node",
|
||||
"type": "bytes32"
|
||||
},
|
||||
{
|
||||
"internalType": "address",
|
||||
"name": "owner",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"internalType": "address",
|
||||
"name": "resolver",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"internalType": "uint64",
|
||||
"name": "ttl",
|
||||
"type": "uint64"
|
||||
}
|
||||
],
|
||||
"name": "setRecord",
|
||||
"outputs": [],
|
||||
"payable": false,
|
||||
"stateMutability": "nonpayable",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": false,
|
||||
"inputs": [
|
||||
{
|
||||
"internalType": "bytes32",
|
||||
"name": "node",
|
||||
"type": "bytes32"
|
||||
},
|
||||
{
|
||||
"internalType": "address",
|
||||
"name": "resolver",
|
||||
"type": "address"
|
||||
}
|
||||
],
|
||||
"name": "setResolver",
|
||||
"outputs": [],
|
||||
"payable": false,
|
||||
"stateMutability": "nonpayable",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": false,
|
||||
"inputs": [
|
||||
{
|
||||
"internalType": "bytes32",
|
||||
"name": "node",
|
||||
"type": "bytes32"
|
||||
},
|
||||
{
|
||||
"internalType": "bytes32",
|
||||
"name": "label",
|
||||
"type": "bytes32"
|
||||
},
|
||||
{
|
||||
"internalType": "address",
|
||||
"name": "owner",
|
||||
"type": "address"
|
||||
}
|
||||
],
|
||||
"name": "setSubnodeOwner",
|
||||
"outputs": [
|
||||
{
|
||||
"internalType": "bytes32",
|
||||
"name": "",
|
||||
"type": "bytes32"
|
||||
}
|
||||
],
|
||||
"payable": false,
|
||||
"stateMutability": "nonpayable",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": false,
|
||||
"inputs": [
|
||||
{
|
||||
"internalType": "bytes32",
|
||||
"name": "node",
|
||||
"type": "bytes32"
|
||||
},
|
||||
{
|
||||
"internalType": "bytes32",
|
||||
"name": "label",
|
||||
"type": "bytes32"
|
||||
},
|
||||
{
|
||||
"internalType": "address",
|
||||
"name": "owner",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"internalType": "address",
|
||||
"name": "resolver",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"internalType": "uint64",
|
||||
"name": "ttl",
|
||||
"type": "uint64"
|
||||
}
|
||||
],
|
||||
"name": "setSubnodeRecord",
|
||||
"outputs": [],
|
||||
"payable": false,
|
||||
"stateMutability": "nonpayable",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": false,
|
||||
"inputs": [
|
||||
{
|
||||
"internalType": "bytes32",
|
||||
"name": "node",
|
||||
"type": "bytes32"
|
||||
},
|
||||
{
|
||||
"internalType": "uint64",
|
||||
"name": "ttl",
|
||||
"type": "uint64"
|
||||
}
|
||||
],
|
||||
"name": "setTTL",
|
||||
"outputs": [],
|
||||
"payable": false,
|
||||
"stateMutability": "nonpayable",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": true,
|
||||
"inputs": [
|
||||
{
|
||||
"internalType": "bytes32",
|
||||
"name": "node",
|
||||
"type": "bytes32"
|
||||
}
|
||||
],
|
||||
"name": "ttl",
|
||||
"outputs": [
|
||||
{
|
||||
"internalType": "uint64",
|
||||
"name": "",
|
||||
"type": "uint64"
|
||||
}
|
||||
],
|
||||
"payable": false,
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
}
|
||||
]
|
||||
@@ -1,4 +1,5 @@
|
||||
import { ChainId, JSBI, Percent, Token, WETH } from '@uniswap/sdk'
|
||||
import { AbstractConnector } from '@web3-react/abstract-connector'
|
||||
|
||||
import { fortmatic, injected, portis, walletconnect, walletlink } from '../connectors'
|
||||
|
||||
@@ -14,6 +15,7 @@ export const USDC = new Token(ChainId.MAINNET, '0xA0b86991c6218b36c1d19D4a2e9Eb0
|
||||
export const USDT = new Token(ChainId.MAINNET, '0xdAC17F958D2ee523a2206206994597C13D831ec7', 6, 'USDT', 'Tether USD')
|
||||
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')
|
||||
|
||||
const WETH_ONLY: ChainTokenList = {
|
||||
[ChainId.MAINNET]: [WETH[ChainId.MAINNET]],
|
||||
@@ -29,6 +31,16 @@ export const BASES_TO_CHECK_TRADES_AGAINST: ChainTokenList = {
|
||||
[ChainId.MAINNET]: [...WETH_ONLY[ChainId.MAINNET], DAI, USDC, USDT, COMP, MKR]
|
||||
}
|
||||
|
||||
/**
|
||||
* Some tokens can only be swapped via certain pairs, so we override the list of bases that are considered for these
|
||||
* tokens.
|
||||
*/
|
||||
export const CUSTOM_BASES: { [chainId in ChainId]?: { [tokenAddress: string]: Token[] } } = {
|
||||
[ChainId.MAINNET]: {
|
||||
[AMPL.address]: [DAI, WETH[ChainId.MAINNET]]
|
||||
}
|
||||
}
|
||||
|
||||
// used for display in the default list when adding liquidity
|
||||
export const SUGGESTED_BASES: ChainTokenList = {
|
||||
...WETH_ONLY,
|
||||
@@ -52,7 +64,19 @@ export const PINNED_PAIRS: { readonly [chainId in ChainId]?: [Token, Token][] }
|
||||
]
|
||||
}
|
||||
|
||||
const TESTNET_CAPABLE_WALLETS = {
|
||||
export interface WalletInfo {
|
||||
connector?: AbstractConnector
|
||||
name: string
|
||||
iconName: string
|
||||
description: string
|
||||
href: string | null
|
||||
color: string
|
||||
primary?: true
|
||||
mobile?: true
|
||||
mobileOnly?: true
|
||||
}
|
||||
|
||||
export const SUPPORTED_WALLETS: { [key: string]: WalletInfo } = {
|
||||
INJECTED: {
|
||||
connector: injected,
|
||||
name: 'Injected',
|
||||
@@ -69,62 +93,53 @@ const TESTNET_CAPABLE_WALLETS = {
|
||||
description: 'Easy-to-use browser extension.',
|
||||
href: null,
|
||||
color: '#E8831D'
|
||||
},
|
||||
WALLET_CONNECT: {
|
||||
connector: walletconnect,
|
||||
name: 'WalletConnect',
|
||||
iconName: 'walletConnectIcon.svg',
|
||||
description: 'Connect to Trust Wallet, Rainbow Wallet and more...',
|
||||
href: null,
|
||||
color: '#4196FC',
|
||||
mobile: true
|
||||
},
|
||||
WALLET_LINK: {
|
||||
connector: walletlink,
|
||||
name: 'Coinbase Wallet',
|
||||
iconName: 'coinbaseWalletIcon.svg',
|
||||
description: 'Use Coinbase Wallet app on mobile device',
|
||||
href: null,
|
||||
color: '#315CF5'
|
||||
},
|
||||
COINBASE_LINK: {
|
||||
name: 'Open in Coinbase Wallet',
|
||||
iconName: 'coinbaseWalletIcon.svg',
|
||||
description: 'Open in Coinbase Wallet app.',
|
||||
href: 'https://go.cb-w.com/mtUDhEZPy1',
|
||||
color: '#315CF5',
|
||||
mobile: true,
|
||||
mobileOnly: true
|
||||
},
|
||||
FORTMATIC: {
|
||||
connector: fortmatic,
|
||||
name: 'Fortmatic',
|
||||
iconName: 'fortmaticIcon.png',
|
||||
description: 'Login using Fortmatic hosted wallet',
|
||||
href: null,
|
||||
color: '#6748FF',
|
||||
mobile: true
|
||||
},
|
||||
Portis: {
|
||||
connector: portis,
|
||||
name: 'Portis',
|
||||
iconName: 'portisIcon.png',
|
||||
description: 'Login using Portis hosted wallet',
|
||||
href: null,
|
||||
color: '#4A6C9B',
|
||||
mobile: true
|
||||
}
|
||||
}
|
||||
|
||||
export const SUPPORTED_WALLETS =
|
||||
process.env.REACT_APP_CHAIN_ID !== '1'
|
||||
? TESTNET_CAPABLE_WALLETS
|
||||
: {
|
||||
...TESTNET_CAPABLE_WALLETS,
|
||||
...{
|
||||
WALLET_CONNECT: {
|
||||
connector: walletconnect,
|
||||
name: 'WalletConnect',
|
||||
iconName: 'walletConnectIcon.svg',
|
||||
description: 'Connect to Trust Wallet, Rainbow Wallet and more...',
|
||||
href: null,
|
||||
color: '#4196FC',
|
||||
mobile: true
|
||||
},
|
||||
WALLET_LINK: {
|
||||
connector: walletlink,
|
||||
name: 'Coinbase Wallet',
|
||||
iconName: 'coinbaseWalletIcon.svg',
|
||||
description: 'Use Coinbase Wallet app on mobile device',
|
||||
href: null,
|
||||
color: '#315CF5'
|
||||
},
|
||||
COINBASE_LINK: {
|
||||
name: 'Open in Coinbase Wallet',
|
||||
iconName: 'coinbaseWalletIcon.svg',
|
||||
description: 'Open in Coinbase Wallet app.',
|
||||
href: 'https://go.cb-w.com/mtUDhEZPy1',
|
||||
color: '#315CF5',
|
||||
mobile: true,
|
||||
mobileOnly: true
|
||||
},
|
||||
FORTMATIC: {
|
||||
connector: fortmatic,
|
||||
name: 'Fortmatic',
|
||||
iconName: 'fortmaticIcon.png',
|
||||
description: 'Login using Fortmatic hosted wallet',
|
||||
href: null,
|
||||
color: '#6748FF',
|
||||
mobile: true
|
||||
},
|
||||
Portis: {
|
||||
connector: portis,
|
||||
name: 'Portis',
|
||||
iconName: 'portisIcon.png',
|
||||
description: 'Login using Portis hosted wallet',
|
||||
href: null,
|
||||
color: '#4A6C9B',
|
||||
mobile: true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const NetworkContextName = 'NETWORK'
|
||||
|
||||
// default allowed slippage, in bips
|
||||
@@ -147,7 +162,3 @@ 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))
|
||||
|
||||
// the Uniswap Default token list lives here
|
||||
export const DEFAULT_TOKEN_LIST_URL =
|
||||
'https://unpkg.com/@uniswap/default-token-list@latest/uniswap-default.tokenlist.json'
|
||||
|
||||
20
src/constants/lists.ts
Normal file
20
src/constants/lists.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
// the Uniswap Default token list lives here
|
||||
export const DEFAULT_TOKEN_LIST_URL = 'tokens.uniswap.eth'
|
||||
|
||||
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://www.coingecko.com/tokens_list/uniswap/defi_100/v_0_0_0.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'
|
||||
]
|
||||
@@ -1,4 +0,0 @@
|
||||
{
|
||||
"extends": "../../tsconfig.strict.json",
|
||||
"include": ["**/*"]
|
||||
}
|
||||
@@ -131,7 +131,7 @@ export function useV1Trade(
|
||||
? new Trade(route, exactAmount, isExactIn ? TradeType.EXACT_INPUT : TradeType.EXACT_OUTPUT)
|
||||
: undefined
|
||||
} catch (error) {
|
||||
console.error('Failed to create V1 trade', error)
|
||||
console.debug('Failed to create V1 trade', error)
|
||||
}
|
||||
return v1Trade
|
||||
}
|
||||
@@ -167,6 +167,8 @@ export function isTradeBetter(
|
||||
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 (
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
{
|
||||
"extends": "../../tsconfig.strict.json",
|
||||
"include": ["**/*"]
|
||||
}
|
||||
8
src/ethereum.d.ts
vendored
8
src/ethereum.d.ts
vendored
@@ -1,8 +0,0 @@
|
||||
interface Window {
|
||||
ethereum?: {
|
||||
isMetaMask?: true
|
||||
on?: (...args: any[]) => void
|
||||
removeListener?: (...args: any[]) => void
|
||||
}
|
||||
web3?: {}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import { parseBytes32String } from '@ethersproject/strings'
|
||||
import { Currency, ETHER, Token } from '@uniswap/sdk'
|
||||
import { Currency, ETHER, Token, currencyEquals } from '@uniswap/sdk'
|
||||
import { useMemo } from 'react'
|
||||
import { useDefaultTokenList } from '../state/lists/hooks'
|
||||
import { useSelectedTokenList } from '../state/lists/hooks'
|
||||
import { NEVER_RELOAD, useSingleCallResult } from '../state/multicall/hooks'
|
||||
import { useUserAddedTokens } from '../state/user/hooks'
|
||||
import { isAddress } from '../utils'
|
||||
@@ -12,7 +12,7 @@ import { useBytes32TokenContract, useTokenContract } from './useContract'
|
||||
export function useAllTokens(): { [address: string]: Token } {
|
||||
const { chainId } = useActiveWeb3React()
|
||||
const userAddedTokens = useUserAddedTokens()
|
||||
const allTokens = useDefaultTokenList()
|
||||
const allTokens = useSelectedTokenList()
|
||||
|
||||
return useMemo(() => {
|
||||
if (!chainId) return {}
|
||||
@@ -32,6 +32,12 @@ export function useAllTokens(): { [address: string]: Token } {
|
||||
}, [chainId, userAddedTokens, allTokens])
|
||||
}
|
||||
|
||||
// Check if currency is included in custom list from user storage
|
||||
export function useIsUserAddedToken(currency: Currency): boolean {
|
||||
const userAddedTokens = useUserAddedTokens()
|
||||
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 {
|
||||
|
||||
@@ -2,9 +2,8 @@ import { Currency, CurrencyAmount, Pair, Token, Trade } from '@uniswap/sdk'
|
||||
import flatMap from 'lodash.flatmap'
|
||||
import { useMemo } from 'react'
|
||||
|
||||
import { BASES_TO_CHECK_TRADES_AGAINST } from '../constants'
|
||||
import { BASES_TO_CHECK_TRADES_AGAINST, CUSTOM_BASES } from '../constants'
|
||||
import { PairState, usePairs } from '../data/Reserves'
|
||||
import { maxHopsFor } from '../utils/maxHopsFor'
|
||||
import { wrappedCurrency } from '../utils/wrappedCurrency'
|
||||
|
||||
import { useActiveWeb3React } from './index'
|
||||
@@ -18,18 +17,46 @@ function useAllCommonPairs(currencyA?: Currency, currencyB?: Currency): Pair[] {
|
||||
? [wrappedCurrency(currencyA, chainId), wrappedCurrency(currencyB, chainId)]
|
||||
: [undefined, undefined]
|
||||
|
||||
const allPairCombinations: [Token | undefined, Token | undefined][] = useMemo(
|
||||
() => [
|
||||
// the direct pair
|
||||
[tokenA, tokenB],
|
||||
// token A against all bases
|
||||
...bases.map((base): [Token | undefined, Token | undefined] => [tokenA, base]),
|
||||
// token B against all bases
|
||||
...bases.map((base): [Token | undefined, Token | undefined] => [tokenB, base]),
|
||||
// each base against all bases
|
||||
...flatMap(bases, (base): [Token, Token][] => bases.map(otherBase => [base, otherBase]))
|
||||
],
|
||||
[tokenA, tokenB, bases]
|
||||
const basePairs: [Token, Token][] = useMemo(
|
||||
() =>
|
||||
flatMap(bases, (base): [Token, Token][] => bases.map(otherBase => [base, otherBase])).filter(
|
||||
([t0, t1]) => t0.address !== t1.address
|
||||
),
|
||||
[bases]
|
||||
)
|
||||
|
||||
const allPairCombinations: [Token, Token][] = useMemo(
|
||||
() =>
|
||||
tokenA && tokenB
|
||||
? [
|
||||
// the direct pair
|
||||
[tokenA, tokenB],
|
||||
// token A against all bases
|
||||
...bases.map((base): [Token, Token] => [tokenA, base]),
|
||||
// token B against all bases
|
||||
...bases.map((base): [Token, Token] => [tokenB, base]),
|
||||
// each base against all bases
|
||||
...basePairs
|
||||
]
|
||||
.filter((tokens): tokens is [Token, Token] => Boolean(tokens[0] && tokens[1]))
|
||||
.filter(([t0, t1]) => t0.address !== t1.address)
|
||||
.filter(([tokenA, tokenB]) => {
|
||||
if (!chainId) return true
|
||||
const customBases = CUSTOM_BASES[chainId]
|
||||
if (!customBases) return true
|
||||
|
||||
const customBasesA: Token[] | undefined = customBases[tokenA.address]
|
||||
const customBasesB: Token[] | undefined = customBases[tokenB.address]
|
||||
|
||||
if (!customBasesA && !customBasesB) return true
|
||||
|
||||
if (customBasesA && !customBasesA.find(base => tokenB.equals(base))) return false
|
||||
if (customBasesB && !customBasesB.find(base => tokenA.equals(base))) return false
|
||||
|
||||
return true
|
||||
})
|
||||
: [],
|
||||
[tokenA, tokenB, bases, basePairs, chainId]
|
||||
)
|
||||
|
||||
const allPairs = usePairs(allPairCombinations)
|
||||
@@ -56,12 +83,10 @@ function useAllCommonPairs(currencyA?: Currency, currencyB?: Currency): Pair[] {
|
||||
*/
|
||||
export function useTradeExactIn(currencyAmountIn?: CurrencyAmount, currencyOut?: Currency): Trade | null {
|
||||
const allowedPairs = useAllCommonPairs(currencyAmountIn?.currency, currencyOut)
|
||||
|
||||
return useMemo(() => {
|
||||
if (currencyAmountIn && currencyOut && allowedPairs.length > 0) {
|
||||
const maxHops = maxHopsFor(currencyAmountIn.currency, currencyOut)
|
||||
return (
|
||||
Trade.bestTradeExactIn(allowedPairs, currencyAmountIn, currencyOut, { maxHops, maxNumResults: 1 })[0] ?? null
|
||||
Trade.bestTradeExactIn(allowedPairs, currencyAmountIn, currencyOut, { maxHops: 3, maxNumResults: 1 })[0] ?? null
|
||||
)
|
||||
}
|
||||
return null
|
||||
@@ -76,9 +101,9 @@ export function useTradeExactOut(currencyIn?: Currency, currencyAmountOut?: Curr
|
||||
|
||||
return useMemo(() => {
|
||||
if (currencyIn && currencyAmountOut && allowedPairs.length > 0) {
|
||||
const maxHops = maxHopsFor(currencyIn, currencyAmountOut.currency)
|
||||
return (
|
||||
Trade.bestTradeExactOut(allowedPairs, currencyIn, currencyAmountOut, { maxHops, maxNumResults: 1 })[0] ?? null
|
||||
Trade.bestTradeExactOut(allowedPairs, currencyIn, currencyAmountOut, { maxHops: 3, maxNumResults: 1 })[0] ??
|
||||
null
|
||||
)
|
||||
}
|
||||
return null
|
||||
|
||||
8
src/hooks/ethereum.d.ts
vendored
8
src/hooks/ethereum.d.ts
vendored
@@ -1,8 +0,0 @@
|
||||
interface Window {
|
||||
ethereum?: {
|
||||
isMetaMask?: true
|
||||
on?: (...args: any[]) => void
|
||||
removeListener?: (...args: any[]) => void
|
||||
}
|
||||
web3?: {}
|
||||
}
|
||||
@@ -82,6 +82,6 @@ export function useInactiveListener(suppress = false) {
|
||||
}
|
||||
}
|
||||
}
|
||||
return
|
||||
return undefined
|
||||
}, [active, error, suppress, activate])
|
||||
}
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
{
|
||||
"extends": "../../tsconfig.strict.json",
|
||||
"include": ["**/*"]
|
||||
}
|
||||
@@ -2,18 +2,20 @@ import { Contract } from '@ethersproject/contracts'
|
||||
import { ChainId, WETH } from '@uniswap/sdk'
|
||||
import { abi as IUniswapV2PairABI } from '@uniswap/v2-core/build/IUniswapV2Pair.json'
|
||||
import { useMemo } from 'react'
|
||||
import ENS_ABI from '../constants/abis/ens-registrar.json'
|
||||
import ENS_PUBLIC_RESOLVER_ABI from '../constants/abis/ens-public-resolver.json'
|
||||
import { ERC20_BYTES32_ABI } from '../constants/abis/erc20'
|
||||
import UNISOCKS_ABI from '../constants/abis/unisocks.json'
|
||||
import ERC20_ABI from '../constants/abis/erc20.json'
|
||||
import WETH_ABI from '../constants/abis/weth.json'
|
||||
import { MIGRATOR_ABI, MIGRATOR_ADDRESS } from '../constants/abis/migrator'
|
||||
import UNISOCKS_ABI from '../constants/abis/unisocks.json'
|
||||
import WETH_ABI from '../constants/abis/weth.json'
|
||||
import { MULTICALL_ABI, MULTICALL_NETWORKS } from '../constants/multicall'
|
||||
import { V1_EXCHANGE_ABI, V1_FACTORY_ABI, V1_FACTORY_ADDRESSES } from '../constants/v1'
|
||||
import { getContract } from '../utils'
|
||||
import { useActiveWeb3React } from './index'
|
||||
|
||||
// returns null on errors
|
||||
function useContract(address?: string, ABI?: any, withSignerIfPossible = true): Contract | null {
|
||||
function useContract(address: string | undefined, ABI: any, withSignerIfPossible = true): Contract | null {
|
||||
const { library, account } = useActiveWeb3React()
|
||||
|
||||
return useMemo(() => {
|
||||
@@ -49,6 +51,26 @@ export function useWETHContract(withSignerIfPossible?: boolean): Contract | null
|
||||
return useContract(chainId ? WETH[chainId].address : undefined, WETH_ABI, withSignerIfPossible)
|
||||
}
|
||||
|
||||
export function useENSRegistrarContract(withSignerIfPossible?: boolean): Contract | null {
|
||||
const { chainId } = useActiveWeb3React()
|
||||
let address: string | undefined
|
||||
if (chainId) {
|
||||
switch (chainId) {
|
||||
case ChainId.MAINNET:
|
||||
case ChainId.GÖRLI:
|
||||
case ChainId.ROPSTEN:
|
||||
case ChainId.RINKEBY:
|
||||
address = '0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e'
|
||||
break
|
||||
}
|
||||
}
|
||||
return useContract(address, ENS_ABI, withSignerIfPossible)
|
||||
}
|
||||
|
||||
export function useENSResolverContract(address: string | undefined, withSignerIfPossible?: boolean): Contract | null {
|
||||
return useContract(address, ENS_PUBLIC_RESOLVER_ABI, withSignerIfPossible)
|
||||
}
|
||||
|
||||
export function useBytes32TokenContract(tokenAddress?: string, withSignerIfPossible?: boolean): Contract | null {
|
||||
return useContract(tokenAddress, ERC20_BYTES32_ABI, withSignerIfPossible)
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ export default function useCopyClipboard(timeout = 500): [boolean, (toCopy: stri
|
||||
clearTimeout(hide)
|
||||
}
|
||||
}
|
||||
return
|
||||
return undefined
|
||||
}, [isCopied, setIsCopied, timeout])
|
||||
|
||||
return [isCopied, staticCopy]
|
||||
|
||||
@@ -1,46 +1,35 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useActiveWeb3React } from './index'
|
||||
import { namehash } from 'ethers/lib/utils'
|
||||
import { useMemo } from 'react'
|
||||
import { useSingleCallResult } from '../state/multicall/hooks'
|
||||
import isZero from '../utils/isZero'
|
||||
import { useENSRegistrarContract, useENSResolverContract } from './useContract'
|
||||
import useDebounce from './useDebounce'
|
||||
|
||||
/**
|
||||
* Does a lookup for an ENS name to find its address.
|
||||
*/
|
||||
export default function useENSAddress(ensName?: string | null): { loading: boolean; address: string | null } {
|
||||
const { library } = useActiveWeb3React()
|
||||
|
||||
const [address, setAddress] = useState<{ loading: boolean; address: string | null }>({
|
||||
loading: false,
|
||||
address: null
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
if (!library || typeof ensName !== 'string') {
|
||||
setAddress({ loading: false, address: null })
|
||||
return
|
||||
} else {
|
||||
let stale = false
|
||||
setAddress({ loading: true, address: null })
|
||||
library
|
||||
.resolveName(ensName)
|
||||
.then(address => {
|
||||
if (!stale) {
|
||||
if (address) {
|
||||
setAddress({ loading: false, address })
|
||||
} else {
|
||||
setAddress({ loading: false, address: null })
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
if (!stale) {
|
||||
setAddress({ loading: false, address: null })
|
||||
}
|
||||
})
|
||||
|
||||
return () => {
|
||||
stale = true
|
||||
}
|
||||
const debouncedName = useDebounce(ensName, 200)
|
||||
const ensNodeArgument = useMemo(() => {
|
||||
if (!debouncedName) return [undefined]
|
||||
try {
|
||||
return debouncedName ? [namehash(debouncedName)] : [undefined]
|
||||
} catch (error) {
|
||||
return [undefined]
|
||||
}
|
||||
}, [library, ensName])
|
||||
}, [debouncedName])
|
||||
const registrarContract = useENSRegistrarContract(false)
|
||||
const resolverAddress = useSingleCallResult(registrarContract, 'resolver', ensNodeArgument)
|
||||
const resolverAddressResult = resolverAddress.result?.[0]
|
||||
const resolverContract = useENSResolverContract(
|
||||
resolverAddressResult && !isZero(resolverAddressResult) ? resolverAddressResult : undefined,
|
||||
false
|
||||
)
|
||||
const addr = useSingleCallResult(resolverContract, 'addr', ensNodeArgument)
|
||||
|
||||
return address
|
||||
const changed = debouncedName !== ensName
|
||||
return {
|
||||
address: changed ? null : addr.result?.[0] ?? null,
|
||||
loading: changed || resolverAddress.loading || addr.loading
|
||||
}
|
||||
}
|
||||
|
||||
32
src/hooks/useENSContentHash.ts
Normal file
32
src/hooks/useENSContentHash.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { namehash } from 'ethers/lib/utils'
|
||||
import { useMemo } from 'react'
|
||||
import { useSingleCallResult } from '../state/multicall/hooks'
|
||||
import isZero from '../utils/isZero'
|
||||
import { useENSRegistrarContract, useENSResolverContract } from './useContract'
|
||||
|
||||
/**
|
||||
* Does a lookup for an ENS name to find its contenthash.
|
||||
*/
|
||||
export default function useENSContentHash(ensName?: string | null): { loading: boolean; contenthash: string | null } {
|
||||
const ensNodeArgument = useMemo(() => {
|
||||
if (!ensName) return [undefined]
|
||||
try {
|
||||
return ensName ? [namehash(ensName)] : [undefined]
|
||||
} catch (error) {
|
||||
return [undefined]
|
||||
}
|
||||
}, [ensName])
|
||||
const registrarContract = useENSRegistrarContract(false)
|
||||
const resolverAddressResult = useSingleCallResult(registrarContract, 'resolver', ensNodeArgument)
|
||||
const resolverAddress = resolverAddressResult.result?.[0]
|
||||
const resolverContract = useENSResolverContract(
|
||||
resolverAddress && isZero(resolverAddress) ? undefined : resolverAddress,
|
||||
false
|
||||
)
|
||||
const contenthash = useSingleCallResult(resolverContract, 'contenthash', ensNodeArgument)
|
||||
|
||||
return {
|
||||
contenthash: contenthash.result?.[0] ?? null,
|
||||
loading: resolverAddressResult.loading || contenthash.loading
|
||||
}
|
||||
}
|
||||
@@ -1,49 +1,37 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { namehash } from 'ethers/lib/utils'
|
||||
import { useMemo } from 'react'
|
||||
import { useSingleCallResult } from '../state/multicall/hooks'
|
||||
import { isAddress } from '../utils'
|
||||
import { useActiveWeb3React } from './index'
|
||||
import isZero from '../utils/isZero'
|
||||
import { useENSRegistrarContract, useENSResolverContract } from './useContract'
|
||||
import useDebounce from './useDebounce'
|
||||
|
||||
/**
|
||||
* Does a reverse lookup for an address to find its ENS name.
|
||||
* Note this is not the same as looking up an ENS name to find an address.
|
||||
*/
|
||||
export default function useENSName(address?: string): { ENSName: string | null; loading: boolean } {
|
||||
const { library } = useActiveWeb3React()
|
||||
|
||||
const [ENSName, setENSName] = useState<{ ENSName: string | null; loading: boolean }>({
|
||||
loading: false,
|
||||
ENSName: null
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
const validated = isAddress(address)
|
||||
if (!library || !validated) {
|
||||
setENSName({ loading: false, ENSName: null })
|
||||
return
|
||||
} else {
|
||||
let stale = false
|
||||
setENSName({ loading: true, ENSName: null })
|
||||
library
|
||||
.lookupAddress(validated)
|
||||
.then(name => {
|
||||
if (!stale) {
|
||||
if (name) {
|
||||
setENSName({ loading: false, ENSName: name })
|
||||
} else {
|
||||
setENSName({ loading: false, ENSName: null })
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
if (!stale) {
|
||||
setENSName({ loading: false, ENSName: null })
|
||||
}
|
||||
})
|
||||
|
||||
return () => {
|
||||
stale = true
|
||||
}
|
||||
const debouncedAddress = useDebounce(address, 200)
|
||||
const ensNodeArgument = useMemo(() => {
|
||||
if (!debouncedAddress || !isAddress(debouncedAddress)) return [undefined]
|
||||
try {
|
||||
return debouncedAddress ? [namehash(`${debouncedAddress.toLowerCase().substr(2)}.addr.reverse`)] : [undefined]
|
||||
} catch (error) {
|
||||
return [undefined]
|
||||
}
|
||||
}, [library, address])
|
||||
}, [debouncedAddress])
|
||||
const registrarContract = useENSRegistrarContract(false)
|
||||
const resolverAddress = useSingleCallResult(registrarContract, 'resolver', ensNodeArgument)
|
||||
const resolverAddressResult = resolverAddress.result?.[0]
|
||||
const resolverContract = useENSResolverContract(
|
||||
resolverAddressResult && !isZero(resolverAddressResult) ? resolverAddressResult : undefined,
|
||||
false
|
||||
)
|
||||
const name = useSingleCallResult(resolverContract, 'name', ensNodeArgument)
|
||||
|
||||
return ENSName
|
||||
const changed = debouncedAddress !== address
|
||||
return {
|
||||
ENSName: changed ? null : name.result?.[0] ?? null,
|
||||
loading: changed || resolverAddress.loading || name.loading
|
||||
}
|
||||
}
|
||||
|
||||
50
src/hooks/useFetchListCallback.ts
Normal file
50
src/hooks/useFetchListCallback.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import { nanoid } from '@reduxjs/toolkit'
|
||||
import { ChainId } from '@uniswap/sdk'
|
||||
import { TokenList } from '@uniswap/token-lists'
|
||||
import { useCallback } from 'react'
|
||||
import { useDispatch } from 'react-redux'
|
||||
import { getNetworkLibrary, NETWORK_CHAIN_ID } from '../connectors'
|
||||
import { AppDispatch } from '../state'
|
||||
import { fetchTokenList } from '../state/lists/actions'
|
||||
import getTokenList from '../utils/getTokenList'
|
||||
import resolveENSContentHash from '../utils/resolveENSContentHash'
|
||||
import { useActiveWeb3React } from './index'
|
||||
|
||||
export function useFetchListCallback(): (listUrl: string) => Promise<TokenList> {
|
||||
const { chainId, library } = useActiveWeb3React()
|
||||
const dispatch = useDispatch<AppDispatch>()
|
||||
|
||||
const ensResolver = useCallback(
|
||||
(ensName: string) => {
|
||||
if (!library || chainId !== ChainId.MAINNET) {
|
||||
if (NETWORK_CHAIN_ID === ChainId.MAINNET) {
|
||||
const networkLibrary = getNetworkLibrary()
|
||||
if (networkLibrary) {
|
||||
return resolveENSContentHash(ensName, networkLibrary)
|
||||
}
|
||||
}
|
||||
throw new Error('Could not construct mainnet ENS resolver')
|
||||
}
|
||||
return resolveENSContentHash(ensName, library)
|
||||
},
|
||||
[chainId, library]
|
||||
)
|
||||
|
||||
return useCallback(
|
||||
async (listUrl: string) => {
|
||||
const requestId = nanoid()
|
||||
dispatch(fetchTokenList.pending({ requestId, url: listUrl }))
|
||||
return getTokenList(listUrl, ensResolver)
|
||||
.then(tokenList => {
|
||||
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 }))
|
||||
throw error
|
||||
})
|
||||
},
|
||||
[dispatch, ensResolver]
|
||||
)
|
||||
}
|
||||
17
src/hooks/useHttpLocations.ts
Normal file
17
src/hooks/useHttpLocations.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { useMemo } from 'react'
|
||||
import contenthashToUri from '../utils/contenthashToUri'
|
||||
import { parseENSAddress } from '../utils/parseENSAddress'
|
||||
import uriToHttp from '../utils/uriToHttp'
|
||||
import useENSContentHash from './useENSContentHash'
|
||||
|
||||
export default function useHttpLocations(uri: string | undefined): string[] {
|
||||
const ens = useMemo(() => (uri ? parseENSAddress(uri) : undefined), [uri])
|
||||
const resolvedContentHash = useENSContentHash(ens?.ensName)
|
||||
return useMemo(() => {
|
||||
if (ens) {
|
||||
return resolvedContentHash.contenthash ? uriToHttp(contenthashToUri(resolvedContentHash.contenthash)) : []
|
||||
} else {
|
||||
return uri ? uriToHttp(uri) : []
|
||||
}
|
||||
}, [ens, resolvedContentHash.contenthash, uri])
|
||||
}
|
||||
@@ -20,6 +20,6 @@ export default function useInterval(callback: () => void, delay: null | number,
|
||||
const id = setInterval(tick, delay)
|
||||
return () => clearInterval(id)
|
||||
}
|
||||
return
|
||||
return undefined
|
||||
}, [delay, leading])
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ export default function useIsWindowVisible(): boolean {
|
||||
}, [setFocused])
|
||||
|
||||
useEffect(() => {
|
||||
if (!VISIBILITY_STATE_SUPPORTED) return
|
||||
if (!VISIBILITY_STATE_SUPPORTED) return undefined
|
||||
|
||||
document.addEventListener('visibilitychange', listener)
|
||||
return () => {
|
||||
|
||||
@@ -1,13 +1,33 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
/**
|
||||
* Returns the last value of type T that passes a filter function
|
||||
* @param value changing value
|
||||
* @param filterFn function that determines whether a given value should be considered for the last value
|
||||
*/
|
||||
export default function useLast<T>(
|
||||
value: T | undefined | null,
|
||||
filterFn?: (value: T | null | undefined) => boolean
|
||||
): T | null | undefined {
|
||||
const [last, setLast] = useState<T | null | undefined>(filterFn && filterFn(value) ? value : undefined)
|
||||
useEffect(() => {
|
||||
setLast(last => {
|
||||
const shouldUse: boolean = filterFn ? filterFn(value) : true
|
||||
if (shouldUse) return value
|
||||
return last
|
||||
})
|
||||
}, [filterFn, value])
|
||||
return last
|
||||
}
|
||||
|
||||
function isDefined<T>(x: T | null | undefined): x is T {
|
||||
return x !== null && x !== undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the last truthy value of type T
|
||||
* @param value changing value
|
||||
*/
|
||||
export default function useLast<T>(value: T | undefined | null): T | null | undefined {
|
||||
const [last, setLast] = useState<T | null | undefined>(value)
|
||||
useEffect(() => {
|
||||
setLast(last => value ?? last)
|
||||
}, [value])
|
||||
return last
|
||||
export function useLastTruthy<T>(value: T | undefined | null): T | null | undefined {
|
||||
return useLast(value, isDefined)
|
||||
}
|
||||
|
||||
26
src/hooks/useOnClickOutside.tsx
Normal file
26
src/hooks/useOnClickOutside.tsx
Normal file
@@ -0,0 +1,26 @@
|
||||
import { RefObject, useEffect, useRef } from 'react'
|
||||
|
||||
export function useOnClickOutside<T extends HTMLElement>(
|
||||
node: RefObject<T | undefined>,
|
||||
handler: undefined | (() => void)
|
||||
) {
|
||||
const handlerRef = useRef<undefined | (() => void)>(handler)
|
||||
useEffect(() => {
|
||||
handlerRef.current = handler
|
||||
}, [handler])
|
||||
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (e: MouseEvent) => {
|
||||
if (node.current?.contains(e.target as Node) ?? false) {
|
||||
return
|
||||
}
|
||||
if (handlerRef.current) handlerRef.current()
|
||||
}
|
||||
|
||||
document.addEventListener('mousedown', handleClickOutside)
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('mousedown', handleClickOutside)
|
||||
}
|
||||
}, [node])
|
||||
}
|
||||
@@ -1,19 +1,107 @@
|
||||
import { BigNumber } from '@ethersproject/bignumber'
|
||||
import { Contract } from '@ethersproject/contracts'
|
||||
import { JSBI, Percent, Router, Trade, TradeType } from '@uniswap/sdk'
|
||||
import { JSBI, Percent, Router, SwapParameters, Trade, TradeType } from '@uniswap/sdk'
|
||||
import { useMemo } from 'react'
|
||||
import { BIPS_BASE, DEFAULT_DEADLINE_FROM_NOW, INITIAL_ALLOWED_SLIPPAGE } from '../constants'
|
||||
import { getTradeVersion, useV1TradeExchangeAddress } from '../data/V1'
|
||||
import { useTransactionAdder } from '../state/transactions/hooks'
|
||||
import { calculateGasMargin, getRouterContract, isAddress, shortenAddress } from '../utils'
|
||||
import isZero from '../utils/isZero'
|
||||
import v1SwapArguments from '../utils/v1SwapArguments'
|
||||
import { useActiveWeb3React } from './index'
|
||||
import { useV1ExchangeContract } from './useContract'
|
||||
import useENS from './useENS'
|
||||
import { Version } from './useToggledVersion'
|
||||
|
||||
function isZero(hexNumber: string) {
|
||||
return /^0x0*$/.test(hexNumber)
|
||||
export enum SwapCallbackState {
|
||||
INVALID,
|
||||
LOADING,
|
||||
VALID
|
||||
}
|
||||
|
||||
interface SwapCall {
|
||||
contract: Contract
|
||||
parameters: SwapParameters
|
||||
}
|
||||
|
||||
interface SuccessfulCall {
|
||||
call: SwapCall
|
||||
gasEstimate: BigNumber
|
||||
}
|
||||
|
||||
interface FailedCall {
|
||||
call: SwapCall
|
||||
error: Error
|
||||
}
|
||||
|
||||
type EstimatedSwapCall = SuccessfulCall | FailedCall
|
||||
|
||||
/**
|
||||
* Returns the swap calls that can be used to make the trade
|
||||
* @param trade trade to execute
|
||||
* @param allowedSlippage user allowed slippage
|
||||
* @param deadline the deadline for the trade
|
||||
* @param recipientAddressOrName
|
||||
*/
|
||||
function useSwapCallArguments(
|
||||
trade: Trade | undefined, // trade to execute, required
|
||||
allowedSlippage: number = INITIAL_ALLOWED_SLIPPAGE, // in bips
|
||||
deadline: number = DEFAULT_DEADLINE_FROM_NOW, // in seconds from now
|
||||
recipientAddressOrName: string | null // the ENS name or address of the recipient of the trade, or null if swap should be returned to sender
|
||||
): SwapCall[] {
|
||||
const { account, chainId, library } = useActiveWeb3React()
|
||||
|
||||
const { address: recipientAddress } = useENS(recipientAddressOrName)
|
||||
const recipient = recipientAddressOrName === null ? account : recipientAddress
|
||||
|
||||
const v1Exchange = useV1ExchangeContract(useV1TradeExchangeAddress(trade), true)
|
||||
|
||||
return useMemo(() => {
|
||||
const tradeVersion = getTradeVersion(trade)
|
||||
if (!trade || !recipient || !library || !account || !tradeVersion || !chainId) return []
|
||||
|
||||
const contract: Contract | null =
|
||||
tradeVersion === Version.v2 ? getRouterContract(chainId, library, account) : v1Exchange
|
||||
if (!contract) {
|
||||
return []
|
||||
}
|
||||
|
||||
const swapMethods = []
|
||||
|
||||
switch (tradeVersion) {
|
||||
case Version.v2:
|
||||
swapMethods.push(
|
||||
Router.swapCallParameters(trade, {
|
||||
feeOnTransfer: false,
|
||||
allowedSlippage: new Percent(JSBI.BigInt(allowedSlippage), BIPS_BASE),
|
||||
recipient,
|
||||
ttl: deadline
|
||||
})
|
||||
)
|
||||
|
||||
if (trade.tradeType === TradeType.EXACT_INPUT) {
|
||||
swapMethods.push(
|
||||
Router.swapCallParameters(trade, {
|
||||
feeOnTransfer: true,
|
||||
allowedSlippage: new Percent(JSBI.BigInt(allowedSlippage), BIPS_BASE),
|
||||
recipient,
|
||||
ttl: deadline
|
||||
})
|
||||
)
|
||||
}
|
||||
break
|
||||
case Version.v1:
|
||||
swapMethods.push(
|
||||
v1SwapArguments(trade, {
|
||||
allowedSlippage: new Percent(JSBI.BigInt(allowedSlippage), BIPS_BASE),
|
||||
recipient,
|
||||
ttl: deadline
|
||||
})
|
||||
)
|
||||
break
|
||||
}
|
||||
return swapMethods.map(parameters => ({ parameters, contract }))
|
||||
}, [account, allowedSlippage, chainId, deadline, library, recipient, trade, v1Exchange])
|
||||
}
|
||||
|
||||
// returns a function that will execute a swap, if the parameters are all valid
|
||||
@@ -23,115 +111,97 @@ export function useSwapCallback(
|
||||
allowedSlippage: number = INITIAL_ALLOWED_SLIPPAGE, // in bips
|
||||
deadline: number = DEFAULT_DEADLINE_FROM_NOW, // in seconds from now
|
||||
recipientAddressOrName: string | null // the ENS name or address of the recipient of the trade, or null if swap should be returned to sender
|
||||
): null | (() => Promise<string>) {
|
||||
): { state: SwapCallbackState; callback: null | (() => Promise<string>); error: string | null } {
|
||||
const { account, chainId, library } = useActiveWeb3React()
|
||||
|
||||
const swapCalls = useSwapCallArguments(trade, allowedSlippage, deadline, recipientAddressOrName)
|
||||
|
||||
const addTransaction = useTransactionAdder()
|
||||
|
||||
const { address: recipientAddress } = useENS(recipientAddressOrName)
|
||||
const recipient = recipientAddressOrName === null ? account : recipientAddress
|
||||
|
||||
const tradeVersion = getTradeVersion(trade)
|
||||
const v1Exchange = useV1ExchangeContract(useV1TradeExchangeAddress(trade), true)
|
||||
|
||||
return useMemo(() => {
|
||||
if (!trade || !recipient || !library || !account || !tradeVersion || !chainId) return null
|
||||
|
||||
return async function onSwap() {
|
||||
const contract: Contract | null =
|
||||
tradeVersion === Version.v2 ? getRouterContract(chainId, library, account) : v1Exchange
|
||||
if (!contract) {
|
||||
throw new Error('Failed to get a swap contract')
|
||||
}
|
||||
|
||||
const swapMethods = []
|
||||
|
||||
switch (tradeVersion) {
|
||||
case Version.v2:
|
||||
swapMethods.push(
|
||||
Router.swapCallParameters(trade, {
|
||||
feeOnTransfer: false,
|
||||
allowedSlippage: new Percent(JSBI.BigInt(allowedSlippage), BIPS_BASE),
|
||||
recipient,
|
||||
ttl: deadline
|
||||
})
|
||||
)
|
||||
|
||||
if (trade.tradeType === TradeType.EXACT_INPUT) {
|
||||
swapMethods.push(
|
||||
Router.swapCallParameters(trade, {
|
||||
feeOnTransfer: true,
|
||||
allowedSlippage: new Percent(JSBI.BigInt(allowedSlippage), BIPS_BASE),
|
||||
recipient,
|
||||
ttl: deadline
|
||||
})
|
||||
)
|
||||
}
|
||||
break
|
||||
case Version.v1:
|
||||
swapMethods.push(
|
||||
v1SwapArguments(trade, {
|
||||
allowedSlippage: new Percent(JSBI.BigInt(allowedSlippage), BIPS_BASE),
|
||||
recipient,
|
||||
ttl: deadline
|
||||
})
|
||||
)
|
||||
break
|
||||
}
|
||||
|
||||
const safeGasEstimates: (BigNumber | undefined)[] = await Promise.all(
|
||||
swapMethods.map(({ args, methodName, value }) =>
|
||||
contract.estimateGas[methodName](...args, value && !isZero(value) ? { value } : {})
|
||||
.then(calculateGasMargin)
|
||||
.catch(error => {
|
||||
console.error(`estimateGas failed for ${methodName}`, error)
|
||||
return undefined
|
||||
})
|
||||
)
|
||||
)
|
||||
|
||||
// we expect failures from left to right, so throw if we see failures
|
||||
// from right to left
|
||||
for (let i = 0; i < safeGasEstimates.length - 1; i++) {
|
||||
// if the FoT method fails, but the regular method does not, we should not
|
||||
// use the regular method. this probably means something is wrong with the fot token.
|
||||
if (BigNumber.isBigNumber(safeGasEstimates[i]) && !BigNumber.isBigNumber(safeGasEstimates[i + 1])) {
|
||||
throw new Error(
|
||||
'An error occurred. Please try raising your slippage. If that does not work, contact support.'
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const indexOfSuccessfulEstimation = safeGasEstimates.findIndex(safeGasEstimate =>
|
||||
BigNumber.isBigNumber(safeGasEstimate)
|
||||
)
|
||||
|
||||
// all estimations failed...
|
||||
if (indexOfSuccessfulEstimation === -1) {
|
||||
// if only 1 method exists, either:
|
||||
// a) the token is doing something weird not related to FoT (e.g. enforcing a whitelist)
|
||||
// b) the token is FoT and the user specified an exact output, which is not allowed
|
||||
if (swapMethods.length === 1) {
|
||||
throw Error(
|
||||
`An error occurred. If either of the tokens you're swapping take a fee on transfer, you must specify an exact input amount.`
|
||||
)
|
||||
}
|
||||
// if 2 methods exists, either:
|
||||
// a) the token is doing something weird not related to FoT (e.g. enforcing a whitelist)
|
||||
// b) the token is FoT and is taking more than the specified slippage
|
||||
else if (swapMethods.length === 2) {
|
||||
throw Error(
|
||||
`An error occurred. If either of the tokens you're swapping take a fee on transfer, you must specify a slippage tolerance higher than the fee.`
|
||||
)
|
||||
} else {
|
||||
throw Error('This transaction would fail. Please contact support.')
|
||||
}
|
||||
if (!trade || !library || !account || !chainId) {
|
||||
return { state: SwapCallbackState.INVALID, callback: null, error: 'Missing dependencies' }
|
||||
}
|
||||
if (!recipient) {
|
||||
if (recipientAddressOrName !== null) {
|
||||
return { state: SwapCallbackState.INVALID, callback: null, error: 'Invalid recipient' }
|
||||
} else {
|
||||
const { methodName, args, value } = swapMethods[indexOfSuccessfulEstimation]
|
||||
const safeGasEstimate = safeGasEstimates[indexOfSuccessfulEstimation]
|
||||
return { state: SwapCallbackState.LOADING, callback: null, error: null }
|
||||
}
|
||||
}
|
||||
|
||||
const tradeVersion = getTradeVersion(trade)
|
||||
|
||||
return {
|
||||
state: SwapCallbackState.VALID,
|
||||
callback: async function onSwap(): Promise<string> {
|
||||
const estimatedCalls: EstimatedSwapCall[] = await Promise.all(
|
||||
swapCalls.map(call => {
|
||||
const {
|
||||
parameters: { methodName, args, value },
|
||||
contract
|
||||
} = call
|
||||
const options = !value || isZero(value) ? {} : { value }
|
||||
|
||||
return contract.estimateGas[methodName](...args, options)
|
||||
.then(gasEstimate => {
|
||||
return {
|
||||
call,
|
||||
gasEstimate
|
||||
}
|
||||
})
|
||||
.catch(gasError => {
|
||||
console.debug('Gas estimate failed, trying eth_call to extract error', call)
|
||||
|
||||
return contract.callStatic[methodName](...args, options)
|
||||
.then(result => {
|
||||
console.debug('Unexpected successful call after failed estimate gas', call, gasError, result)
|
||||
return { call, error: new Error('Unexpected issue with estimating the gas. Please try again.') }
|
||||
})
|
||||
.catch(callError => {
|
||||
console.debug('Call threw error', call, callError)
|
||||
let errorMessage: string
|
||||
switch (callError.reason) {
|
||||
case 'UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT':
|
||||
case 'UniswapV2Router: EXCESSIVE_INPUT_AMOUNT':
|
||||
errorMessage =
|
||||
'This transaction will not succeed either due to price movement or fee on transfer. Try increasing your slippage tolerance.'
|
||||
break
|
||||
default:
|
||||
errorMessage = `The transaction cannot succeed due to error: ${callError.reason}. This is probably an issue with one of the tokens you are swapping.`
|
||||
}
|
||||
return { call, error: new Error(errorMessage) }
|
||||
})
|
||||
})
|
||||
})
|
||||
)
|
||||
|
||||
// a successful estimation is a bignumber gas estimate and the next call is also a bignumber gas estimate
|
||||
const successfulEstimation = estimatedCalls.find(
|
||||
(el, ix, list): el is SuccessfulCall =>
|
||||
'gasEstimate' in el && (ix === list.length - 1 || 'gasEstimate' in list[ix + 1])
|
||||
)
|
||||
|
||||
if (!successfulEstimation) {
|
||||
const errorCalls = estimatedCalls.filter((call): call is FailedCall => 'error' in call)
|
||||
if (errorCalls.length > 0) throw errorCalls[errorCalls.length - 1].error
|
||||
throw new Error('Unexpected error. Please contact support: none of the calls threw an error')
|
||||
}
|
||||
|
||||
const {
|
||||
call: {
|
||||
contract,
|
||||
parameters: { methodName, args, value }
|
||||
},
|
||||
gasEstimate
|
||||
} = successfulEstimation
|
||||
|
||||
return contract[methodName](...args, {
|
||||
gasLimit: safeGasEstimate,
|
||||
...(value && !isZero(value) ? { value } : {})
|
||||
gasLimit: calculateGasMargin(gasEstimate),
|
||||
...(value && !isZero(value) ? { value, from: account } : { from: account })
|
||||
})
|
||||
.then((response: any) => {
|
||||
const inputSymbol = trade.inputAmount.currency.symbol
|
||||
@@ -161,27 +231,15 @@ export function useSwapCallback(
|
||||
.catch((error: any) => {
|
||||
// if the user rejected the tx, pass this along
|
||||
if (error?.code === 4001) {
|
||||
throw error
|
||||
}
|
||||
// otherwise, the error was unexpected and we need to convey that
|
||||
else {
|
||||
throw new Error('Transaction rejected.')
|
||||
} else {
|
||||
// otherwise, the error was unexpected and we need to convey that
|
||||
console.error(`Swap failed`, error, methodName, args, value)
|
||||
throw Error('An error occurred while swapping. Please contact support.')
|
||||
throw new Error(`Swap failed: ${error.message}`)
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
error: null
|
||||
}
|
||||
}, [
|
||||
trade,
|
||||
recipient,
|
||||
library,
|
||||
account,
|
||||
tradeVersion,
|
||||
chainId,
|
||||
allowedSlippage,
|
||||
v1Exchange,
|
||||
deadline,
|
||||
recipientAddressOrName,
|
||||
addTransaction
|
||||
])
|
||||
}, [trade, library, account, chainId, recipient, recipientAddressOrName, swapCalls, addTransaction])
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ export default function useWrapCallback(
|
||||
inputCurrency: Currency | undefined,
|
||||
outputCurrency: Currency | undefined,
|
||||
typedValue: string | undefined
|
||||
): { wrapType: WrapType; execute?: undefined | (() => Promise<void>); error?: string } {
|
||||
): { wrapType: WrapType; execute?: undefined | (() => Promise<void>); inputError?: string } {
|
||||
const { chainId, account } = useActiveWeb3React()
|
||||
const wethContract = useWETHContract()
|
||||
const balance = useCurrencyBalance(account ?? undefined, inputCurrency)
|
||||
@@ -50,7 +50,7 @@ export default function useWrapCallback(
|
||||
}
|
||||
}
|
||||
: undefined,
|
||||
error: sufficientBalance ? undefined : 'Insufficient ETH balance'
|
||||
inputError: sufficientBalance ? undefined : 'Insufficient ETH balance'
|
||||
}
|
||||
} else if (currencyEquals(WETH[chainId], inputCurrency) && outputCurrency === ETHER) {
|
||||
return {
|
||||
@@ -66,7 +66,7 @@ export default function useWrapCallback(
|
||||
}
|
||||
}
|
||||
: undefined,
|
||||
error: sufficientBalance ? undefined : 'Insufficient WETH balance'
|
||||
inputError: sufficientBalance ? undefined : 'Insufficient WETH balance'
|
||||
}
|
||||
} else {
|
||||
return NOT_APPLICABLE
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user