Compare commits
231 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e667615449 | ||
|
|
4ab61faeae | ||
|
|
0004db3d4a | ||
|
|
c133c472be | ||
|
|
0019ccdf51 | ||
|
|
5a1a469f35 | ||
|
|
4c28f34803 | ||
|
|
104be830fc | ||
|
|
24c70791cd | ||
|
|
216fdea290 | ||
|
|
40e4ce2ed3 | ||
|
|
b2508fc6f2 | ||
|
|
f73b37287f | ||
|
|
c09eb738c3 | ||
|
|
6de3a6ec28 | ||
|
|
c1d35cc8b3 | ||
|
|
f279b2bea2 | ||
|
|
6ffbf756f8 | ||
|
|
10837d7ba1 | ||
|
|
2d6eddf9d4 | ||
|
|
aadf43efc3 | ||
|
|
227f729ecd | ||
|
|
a5b15e37f6 | ||
|
|
2408b2966e | ||
|
|
dc391d1bea | ||
|
|
e2d0514344 | ||
|
|
98d25dd2af | ||
|
|
f289dec684 | ||
|
|
73d3df05f2 | ||
|
|
83554f44f8 | ||
|
|
320b2e384b | ||
|
|
9492e7375a | ||
|
|
8a6a10be9d | ||
|
|
5e486fca7f | ||
|
|
87d24c404b | ||
|
|
d4011f73d1 | ||
|
|
6fc3157977 | ||
|
|
9c1fe53e4b | ||
|
|
28c916ff45 | ||
|
|
7adb4b6bd6 | ||
|
|
b2f0236ee8 | ||
|
|
4b57059353 | ||
|
|
6926f9a4ae | ||
|
|
7dec580944 | ||
|
|
5cf95680ef | ||
|
|
f8d6bab4ae | ||
|
|
c9721c42bf | ||
|
|
4414134bb2 | ||
|
|
44ba54e44a | ||
|
|
9ec3109f72 | ||
|
|
e75793676a | ||
|
|
32006ded21 | ||
|
|
d4f1c579d8 | ||
|
|
95f3541807 | ||
|
|
da4ca73a1d | ||
|
|
e75bf8d003 | ||
|
|
236f68a459 | ||
|
|
9f07baaad2 | ||
|
|
c75464e1aa | ||
|
|
bc80585bb4 | ||
|
|
ad45b2b7bb | ||
|
|
63ac89e9f3 | ||
|
|
1b6ae0d3db | ||
|
|
7d67819604 | ||
|
|
7b9b332c42 | ||
|
|
01feae978a | ||
|
|
2452d51e14 | ||
|
|
bbdc258083 | ||
|
|
27b103e3f7 | ||
|
|
2a751b9892 | ||
|
|
175e93fbba | ||
|
|
0b5fc07ee5 | ||
|
|
a0d4710a11 | ||
|
|
63af1a160d | ||
|
|
85d52b3480 | ||
|
|
219de1f471 | ||
|
|
f110fa7732 | ||
|
|
513a1b0c4b | ||
|
|
96c9eede18 | ||
|
|
f4a97501e5 | ||
|
|
2ff5ce62db | ||
|
|
79176dfe79 | ||
|
|
4880e0fb02 | ||
|
|
ceea563af5 | ||
|
|
992c5a8e54 | ||
|
|
850e9bb2bd | ||
|
|
e5bf2d0823 | ||
|
|
2708d26495 | ||
|
|
ccab6f01cf | ||
|
|
384c3cada4 | ||
|
|
c94e8ff6e5 | ||
|
|
04584cc4ac | ||
|
|
7d20dd976b | ||
|
|
2b4f511b55 | ||
|
|
7d379b796c | ||
|
|
4e8777c728 | ||
|
|
9916c84e5d | ||
|
|
462f2482e9 | ||
|
|
2ff8f5a7ff | ||
|
|
0b8d05b696 | ||
|
|
425a23774d | ||
|
|
fea11871ae | ||
|
|
b616c7b50d | ||
|
|
9ac1bcf008 | ||
|
|
1bb4db5414 | ||
|
|
536de89a38 | ||
|
|
199eb8bf50 | ||
|
|
f1c6119f8b | ||
|
|
a10b22a382 | ||
|
|
206e845731 | ||
|
|
1e7072d5f2 | ||
|
|
70722b5e30 | ||
|
|
cba76c43f6 | ||
|
|
373b3180d3 | ||
|
|
c35d8fc702 | ||
|
|
e6a043cdc5 | ||
|
|
5afddb2ede | ||
|
|
3b2c21e26d | ||
|
|
0e72f1c5d3 | ||
|
|
81b063351d | ||
|
|
bfb1992e45 | ||
|
|
690a121218 | ||
|
|
3fe0a5a9b0 | ||
|
|
f0e1e92178 | ||
|
|
fcb012cb1a | ||
|
|
1aea452f3b | ||
|
|
e311e2fcf9 | ||
|
|
9c4f63f444 | ||
|
|
f261675602 | ||
|
|
67df4959fb | ||
|
|
095beae0c2 | ||
|
|
51e929bd1e | ||
|
|
af892c1fec | ||
|
|
1380f865a9 | ||
|
|
487b5ceccc | ||
|
|
6c9eeb4691 | ||
|
|
a34dc73c38 | ||
|
|
07fee04526 | ||
|
|
5a9b7a382e | ||
|
|
7bffea0692 | ||
|
|
b1ffab1890 | ||
|
|
60582de4d6 | ||
|
|
4ef3746a09 | ||
|
|
d2462af821 | ||
|
|
5405648a54 | ||
|
|
4ba7dd9535 | ||
|
|
0733cdd63c | ||
|
|
8a61b4e4ad | ||
|
|
e0cbee79a6 | ||
|
|
141e605c17 | ||
|
|
3a426fa36b | ||
|
|
52d40949a6 | ||
|
|
685bb1745a | ||
|
|
9006acb4c5 | ||
|
|
1e1a049990 | ||
|
|
3a2566b4e7 | ||
|
|
c9db5fb276 | ||
|
|
1cbf67872b | ||
|
|
38947fe760 | ||
|
|
31f72a541b | ||
|
|
287f64cf5c | ||
|
|
822f9e5fe2 | ||
|
|
6781fce58e | ||
|
|
4398fef38f | ||
|
|
ef65943659 | ||
|
|
19a53cd999 | ||
|
|
b28ad2865d | ||
|
|
0d4108937f | ||
|
|
6da8e2c84d | ||
|
|
ea015d16f2 | ||
|
|
d40d81ca6a | ||
|
|
dba6abadf5 | ||
|
|
4131268b8e | ||
|
|
c365a5ec33 | ||
|
|
ee92df1554 | ||
|
|
f1ccd7a166 | ||
|
|
7c43023860 | ||
|
|
843d5e790a | ||
|
|
40a7cea6f6 | ||
|
|
e023a02037 | ||
|
|
7add2a916c | ||
|
|
5f36437c3f | ||
|
|
152e84bc25 | ||
|
|
3b3f281319 | ||
|
|
17eceebcf5 | ||
|
|
6112c8a2a2 | ||
|
|
2adb0a3dfb | ||
|
|
86fa969d6b | ||
|
|
28b24036c6 | ||
|
|
a1a9d9f041 | ||
|
|
9b2fe0bdca | ||
|
|
cb00e0baa5 | ||
|
|
4ccbd0c3ba | ||
|
|
7fcecc22d9 | ||
|
|
404d7a60e5 | ||
|
|
b6b094acb3 | ||
|
|
49462fdb3b | ||
|
|
3c112b5746 | ||
|
|
e5c5bad7ab | ||
|
|
9eee45a8f1 | ||
|
|
e5ced44d75 | ||
|
|
cdbf440e9c | ||
|
|
5426e48872 | ||
|
|
64dd09f2cc | ||
|
|
6fd9808e7d | ||
|
|
c3d8bc7ed5 | ||
|
|
3f1d7ab310 | ||
|
|
60d7bc4532 | ||
|
|
76dbc9fa18 | ||
|
|
006fe9b325 | ||
|
|
50c9d9973a | ||
|
|
af6add09a0 | ||
|
|
cf5c67ec88 | ||
|
|
eaf8d5a39c | ||
|
|
02dedcbc6e | ||
|
|
d8c4ebc243 | ||
|
|
2b15979e93 | ||
|
|
6211dff044 | ||
|
|
655b79569b | ||
|
|
4e2c5c1e84 | ||
|
|
e43d9e03f1 | ||
|
|
35d398406d | ||
|
|
217a700832 | ||
|
|
55eb03c237 | ||
|
|
a6d8613bdd | ||
|
|
2ca3bf8da3 | ||
|
|
236c3030e1 | ||
|
|
753e5f3423 | ||
|
|
3091ebc158 | ||
|
|
912a19d66d | ||
|
|
4890ad1172 |
2
.env
Normal file
@@ -0,0 +1,2 @@
|
||||
REACT_APP_CHAIN_ID="1"
|
||||
REACT_APP_NETWORK_URL="https://mainnet.infura.io/v3/acb7e55995d04c49bfb52b7141599467"
|
||||
@@ -1,5 +0,0 @@
|
||||
REACT_APP_CHAIN_ID="1"
|
||||
REACT_APP_NETWORK_URL=""
|
||||
REACT_APP_PORTIS_ID=""
|
||||
REACT_APP_FORTMATIC_KEY=""
|
||||
REACT_APP_IS_PRODUCTION_DEPLOY="false"
|
||||
5
.env.production
Normal file
@@ -0,0 +1,5 @@
|
||||
REACT_APP_CHAIN_ID="1"
|
||||
REACT_APP_NETWORK_URL="https://mainnet.infura.io/v3/febcb10ca2754433a61e0805bc6c047d"
|
||||
REACT_APP_PORTIS_ID="c0e2bf01-4b08-4fd5-ac7b-8e26b58cd236"
|
||||
REACT_APP_FORTMATIC_KEY="pk_live_F937DF033A1666BF"
|
||||
REACT_APP_GOOGLE_ANALYTICS_ID="UA-128182339-4"
|
||||
31
.eslintrc.json
Normal file
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 2020,
|
||||
"sourceType": "module",
|
||||
"ecmaFeatures": {
|
||||
// Allows for the parsing of JSX
|
||||
"jsx": true
|
||||
}
|
||||
},
|
||||
"ignorePatterns": [
|
||||
"node_modules/**/*"
|
||||
],
|
||||
"settings": {
|
||||
"react": {
|
||||
"version": "detect"
|
||||
}
|
||||
},
|
||||
"extends": [
|
||||
"plugin:react/recommended",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"plugin:react-hooks/recommended",
|
||||
"prettier/@typescript-eslint",
|
||||
"plugin:prettier/recommended"
|
||||
],
|
||||
"rules": {
|
||||
"@typescript-eslint/explicit-function-return-type": "off",
|
||||
"prettier/prettier": "error",
|
||||
"@typescript-eslint/no-explicit-any": "off"
|
||||
}
|
||||
}
|
||||
135
.github/workflows/release.yaml
vendored
@@ -1,52 +1,105 @@
|
||||
name: Release
|
||||
on:
|
||||
# every morning
|
||||
schedule:
|
||||
- cron: '0 12 * * *'
|
||||
|
||||
# releases are triggered on changes to this file
|
||||
push:
|
||||
# Sequence of patterns matched against refs/tags
|
||||
tags:
|
||||
- 'v*'
|
||||
name: Build and Release
|
||||
branches:
|
||||
- v2
|
||||
paths:
|
||||
- '.github/workflows/release.yaml'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Global Job
|
||||
strategy:
|
||||
matrix:
|
||||
node: ['10.x']
|
||||
os: [ubuntu-latest]
|
||||
runs-on: ${{ matrix.os }}
|
||||
bump_version:
|
||||
name: Bump Version
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
new_tag: ${{ steps.github_tag_action.outputs.new_tag }}
|
||||
changelog: ${{ steps.github_tag_action.outputs.changelog }}
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@master
|
||||
- name: Create production build
|
||||
uses: actions/setup-node@master
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v1
|
||||
|
||||
- name: Bump version and push tag
|
||||
id: github_tag_action
|
||||
uses: mathieudutour/github-tag-action@v4.5
|
||||
with:
|
||||
node-version: ${{ matrix.node }}
|
||||
- run: npm install -g yarn
|
||||
- run: yarn
|
||||
- run: yarn build
|
||||
env:
|
||||
REACT_APP_NETWORK_ID: ${{ secrets.REACT_APP_NETWORK_ID }}
|
||||
- name: Create Release
|
||||
id: create_release
|
||||
uses: actions/create-release@v1.0.0
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
release_branches: .*
|
||||
|
||||
create_release:
|
||||
name: Create Release
|
||||
runs-on: ubuntu-latest
|
||||
needs: bump_version
|
||||
if: ${{ needs.bump_version.outputs.new_tag != null }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v1
|
||||
|
||||
- uses: actions/setup-node@v1
|
||||
with:
|
||||
tag_name: ${{ github.ref }}
|
||||
release_name: Release ${{ github.ref }}
|
||||
draft: false
|
||||
prerelease: false
|
||||
- name: Zip the build
|
||||
uses: thedoctor0/zip-release@master
|
||||
node-version: '12'
|
||||
|
||||
- name: Install dependencies
|
||||
run: yarn install --ignore-scripts --frozen-lockfile
|
||||
|
||||
- name: Build the IPFS bundle
|
||||
run: yarn ipfs-build
|
||||
|
||||
- name: Pin to IPFS
|
||||
id: upload
|
||||
uses: anantaramdas/ipfs-pinata-deploy-action@v1.5.2
|
||||
with:
|
||||
filename: 'build.zip'
|
||||
pin-name: Uniswap ${{ needs.bump_version.outputs.new_tag }}
|
||||
path: './build'
|
||||
exclusions: 'x'
|
||||
- name: Upload Build
|
||||
id: build-asset
|
||||
uses: actions/upload-release-asset@v1.0.1
|
||||
pinata-api-key: ${{ secrets.PINATA_API_KEY }}
|
||||
pinata-secret-api-key: ${{ secrets.PINATA_API_SECRET_KEY }}
|
||||
|
||||
- name: Convert CIDv0 to CIDv1
|
||||
id: convert_cidv0
|
||||
uses: uniswap/convert-cidv0-cidv1@v1.0.0
|
||||
with:
|
||||
cidv0: ${{ steps.upload.outputs.hash }}
|
||||
|
||||
- name: Update DNS with new IPFS hash
|
||||
uses: uniswap/replace-vercel-dns-records@v1.0.0
|
||||
with:
|
||||
domain: 'uniswap.org'
|
||||
subdomain: '_dnslink.app'
|
||||
record-type: 'TXT'
|
||||
value: dnslink=/ipfs/${{ steps.upload.outputs.hash }}
|
||||
token: ${{ secrets.VERCEL_TOKEN }}
|
||||
team-name: 'uniswap'
|
||||
|
||||
- name: Create GitHub Release
|
||||
id: create_release
|
||||
uses: actions/create-release@v1.1.0
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
asset_path: build.zip
|
||||
asset_name: build.zip
|
||||
asset_content_type: application/zip
|
||||
tag_name: ${{ needs.bump_version.outputs.new_tag }}
|
||||
release_name: Release ${{ needs.bump_version.outputs.new_tag }}
|
||||
body: |
|
||||
Release built from commit [`${{ github.sha }}`](https://github.com/Uniswap/uniswap-frontend/tree/${{ github.sha }})
|
||||
|
||||
The IPFS hash of the bundle is:
|
||||
- CIDv0: `${{ steps.upload.outputs.hash }}`
|
||||
- CIDv1: `${{ steps.convert_cidv0.outputs.cidv1 }}`
|
||||
|
||||
Uniswap uses [`localStorage`](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage) to store your settings.
|
||||
**Beware** that other sites you access via the _same_ IPFS gateway can read and modify your settings on Uniswap without your permission.
|
||||
You can avoid this issue by using a subdomain IPFS gateway. The preferred gateway URLs below utilize the CIDv1 of the release in the subdomain, and are relatively safer.
|
||||
|
||||
Preferred URLs:
|
||||
- https://${{ steps.convert_cidv0.outputs.cidv1 }}.ipfs.dweb.link/
|
||||
- https://${{ steps.convert_cidv0.outputs.cidv1 }}.cf-ipfs.com/
|
||||
- [ipfs://${{ steps.upload.outputs.hash }}/](ipfs://${{ steps.upload.outputs.hash }}/)
|
||||
|
||||
Other IPFS gateways:
|
||||
- https://cloudflare-ipfs.com/ipfs/${{ steps.upload.outputs.hash }}/
|
||||
- https://ipfs.infura.io/ipfs/${{ steps.upload.outputs.hash }}/
|
||||
- https://ipfs.io/ipfs/${{ steps.upload.outputs.hash }}/
|
||||
|
||||
${{ needs.bump_version.outputs.changelog }}
|
||||
75
.github/workflows/tests.yaml
vendored
Normal file
@@ -0,0 +1,75 @@
|
||||
name: Tests
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- v2
|
||||
pull_request:
|
||||
branches:
|
||||
- v2
|
||||
jobs:
|
||||
integration-tests:
|
||||
name: Integration tests
|
||||
runs-on: ubuntu-16.04
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v1
|
||||
- uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: '12'
|
||||
- name: Get yarn cache directory path
|
||||
id: yarn-cache-dir-path
|
||||
run: echo "::set-output name=dir::$(yarn cache dir)"
|
||||
- uses: actions/cache@v1
|
||||
id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`)
|
||||
with:
|
||||
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
|
||||
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-yarn-
|
||||
- run: yarn install
|
||||
- run: yarn integration-test
|
||||
|
||||
unit-tests:
|
||||
name: Unit tests
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v1
|
||||
- uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: '12'
|
||||
- name: Get yarn cache directory path
|
||||
id: yarn-cache-dir-path
|
||||
run: echo "::set-output name=dir::$(yarn cache dir)"
|
||||
- uses: actions/cache@v1
|
||||
id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`)
|
||||
with:
|
||||
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
|
||||
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-yarn-
|
||||
- run: yarn install --ignore-scripts --frozen-lockfile
|
||||
- run: yarn test
|
||||
|
||||
lint:
|
||||
name: Lint
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v1
|
||||
- uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: '12'
|
||||
- name: Get yarn cache directory path
|
||||
id: yarn-cache-dir-path
|
||||
run: echo "::set-output name=dir::$(yarn cache dir)"
|
||||
- uses: actions/cache@v1
|
||||
id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`)
|
||||
with:
|
||||
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
|
||||
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-yarn-
|
||||
- run: yarn install --ignore-scripts --frozen-lockfile
|
||||
- run: yarn lint
|
||||
|
||||
7
.gitignore
vendored
@@ -11,7 +11,6 @@
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
.env
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
@@ -28,4 +27,8 @@ notes.txt
|
||||
|
||||
.vscode/
|
||||
|
||||
package-lock.json
|
||||
package-lock.json
|
||||
|
||||
cypress/videos
|
||||
cypress/screenshots
|
||||
cypress/fixtures/example.json
|
||||
13
.travis.yml
@@ -1,13 +0,0 @@
|
||||
branches:
|
||||
except:
|
||||
- master
|
||||
language: node_js
|
||||
node_js:
|
||||
- '10'
|
||||
cache:
|
||||
directories:
|
||||
- node_modules
|
||||
install: yarn
|
||||
script:
|
||||
- yarn check:all
|
||||
- yarn build
|
||||
50
README.md
@@ -1,28 +1,25 @@
|
||||
# Uniswap Frontend
|
||||
|
||||
[](https://app.netlify.com/sites/uniswap/deploys)
|
||||
[](https://travis-ci.org/Uniswap/uniswap-frontend)
|
||||
[](https://github.com/Uniswap/uniswap-frontend/actions?query=workflow%3ATests)
|
||||
[](https://prettier.io/)
|
||||
|
||||
This an an open source interface for Uniswap - a protocol for decentralized exchange of Ethereum tokens.
|
||||
An open source interface for Uniswap -- a protocol for decentralized exchange of Ethereum tokens.
|
||||
|
||||
- Website: [uniswap.io](https://uniswap.io/)
|
||||
- Docs: [docs.uniswap.io](https://docs.uniswap.io/)
|
||||
- Twitter: [@UniswapExchange](https://twitter.com/UniswapExchange)
|
||||
- Reddit: [/r/Uniswap](https://www.reddit.com/r/UniSwap/)
|
||||
- Email: [contact@uniswap.io](mailto:contact@uniswap.io)
|
||||
- Website: [uniswap.org](https://uniswap.org/)
|
||||
- Docs: [uniswap.org/docs/](https://uniswap.org/docs/)
|
||||
- Twitter: [@UniswapProtocol](https://twitter.com/UniswapProtocol)
|
||||
- Reddit: [/r/Uniswap](https://www.reddit.com/r/Uniswap/)
|
||||
- Email: [contact@uniswap.org](mailto:contact@uniswap.org)
|
||||
- Discord: [Uniswap](https://discord.gg/Y7TF6QA)
|
||||
- Whitepaper: [Link](https://hackmd.io/C-DvwDSfSxuh-Gd4WKE_ig)
|
||||
|
||||
## Run Uniswap Locally
|
||||
|
||||
1. Download and unzip the `build.zip` file from the latest release in the [Releases tab](https://github.com/Uniswap/uniswap-frontend/releases/latest).
|
||||
|
||||
2. Serve the `build/` folder locally, and access the application via a browser.
|
||||
## Accessing the frontend
|
||||
|
||||
For more information on running a local server see [https://developer.mozilla.org/en-US/docs/Learn/Common_questions/set_up_a_local_testing_server](https://developer.mozilla.org/en-US/docs/Learn/Common_questions/set_up_a_local_testing_server). This simple approach has one downside: refreshing the page will give a `404` because of how React handles client-side routing. To fix this issue, consider running `serve -s` courtesy of the [serve](https://github.com/zeit/serve) package.
|
||||
|
||||
## Develop Uniswap Locally
|
||||
To access the front end, use an IPFS gateway link from the
|
||||
[latest release](https://github.com/Uniswap/uniswap-frontend/releases/latest)
|
||||
or visit [uniswap.exchange](https://uniswap.exchange).
|
||||
|
||||
## Development
|
||||
|
||||
### Install Dependencies
|
||||
|
||||
@@ -30,9 +27,9 @@ For more information on running a local server see [https://developer.mozilla.or
|
||||
yarn
|
||||
```
|
||||
|
||||
### Configure Environment
|
||||
### Configure Environment (optional)
|
||||
|
||||
Rename `.env.local.example` to `.env.local` and fill in the appropriate variables.
|
||||
Copy `.env` to `.env.local` and change the appropriate variables.
|
||||
|
||||
### Run
|
||||
|
||||
@@ -40,10 +37,21 @@ Rename `.env.local.example` to `.env.local` and fill in the appropriate variable
|
||||
yarn start
|
||||
```
|
||||
|
||||
To run on a testnet, make a copy of `.env.local.example` named `.env.local`, change `REACT_APP_NETWORK_ID` to `"{yourNetworkId}"`, and change `REACT_APP_NETWORK_URL` to e.g. `"https://{yourNetwork}.infura.io/v3/{yourKey}"`.
|
||||
To have the frontend default to a different network, make a copy of `.env` named `.env.local`,
|
||||
change `REACT_APP_NETWORK_ID` to `"{yourNetworkId}"`, and change `REACT_APP_NETWORK_URL` to e.g.
|
||||
`"https://{yourNetwork}.infura.io/v3/{yourKey}"`.
|
||||
|
||||
If deploying with Github Pages, be aware that there's some [tricky client-side routing behavior with `create-react-app`](https://create-react-app.dev/docs/deployment#notes-on-client-side-routing).
|
||||
Note that the front end only works properly on testnets where both
|
||||
[Uniswap V2](https://uniswap.org/docs/v2/smart-contracts/factory/) and
|
||||
[multicall](https://github.com/makerdao/multicall) are deployed.
|
||||
The frontend will not work on other networks.
|
||||
|
||||
## Contributions
|
||||
|
||||
**Please open all pull requests against the `beta` branch.** CI checks will run against all PRs. To ensure that your changes will pass, run `yarn check:all` before pushing. If this command fails, you can try to automatically fix problems with `yarn fix:all`, or do it manually.
|
||||
**Please open all pull requests against the `v2` branch.**
|
||||
CI checks will run against all PRs.
|
||||
|
||||
## Accessing Uniswap V1 interface
|
||||
|
||||
The Uniswap V1 interface for mainnet and testnets is accessible via IPFS gateways linked
|
||||
from the [v1.0.0 release](https://github.com/Uniswap/uniswap-frontend/releases/tag/v1.0.0).
|
||||
7
cypress.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"baseUrl": "http://localhost:3000",
|
||||
"pluginsFile": false,
|
||||
"fixturesFolder": false,
|
||||
"supportFile": "cypress/support/index.js",
|
||||
"video": false
|
||||
}
|
||||
19
cypress/integration/add-liquidity.test.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
describe('Add Liquidity', () => {
|
||||
it('loads the two correct tokens', () => {
|
||||
cy.visit('/add/0xF9bA5210F91D0474bd1e1DcDAeC4C58E359AaD85-0xc778417E063141139Fce010982780140Aa0cD5Ab')
|
||||
cy.get('#add-liquidity-input-tokena .token-symbol-container').should('contain.text', 'MKR')
|
||||
cy.get('#add-liquidity-input-tokenb .token-symbol-container').should('contain.text', 'ETH')
|
||||
})
|
||||
|
||||
it('does not crash if ETH is duplicated', () => {
|
||||
cy.visit('/add/0xc778417E063141139Fce010982780140Aa0cD5Ab-0xc778417E063141139Fce010982780140Aa0cD5Ab')
|
||||
cy.get('#add-liquidity-input-tokena .token-symbol-container').should('contain.text', 'ETH')
|
||||
cy.get('#add-liquidity-input-tokenb .token-symbol-container').should('not.contain.text', 'ETH')
|
||||
})
|
||||
|
||||
it('token not in storage is loaded', () => {
|
||||
cy.visit('/add/0xb290b2f9f8f108d03ff2af3ac5c8de6de31cdf6d-0xF9bA5210F91D0474bd1e1DcDAeC4C58E359AaD85')
|
||||
cy.get('#add-liquidity-input-tokena .token-symbol-container').should('contain.text', 'SKL')
|
||||
cy.get('#add-liquidity-input-tokenb .token-symbol-container').should('contain.text', 'MKR')
|
||||
})
|
||||
})
|
||||
27
cypress/integration/landing.test.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { TEST_ADDRESS_NEVER_USE_SHORTENED } from '../support/commands'
|
||||
|
||||
describe('Landing Page', () => {
|
||||
beforeEach(() => cy.visit('/'))
|
||||
it('loads swap page', () => {
|
||||
cy.get('#swap-page')
|
||||
})
|
||||
|
||||
it('redirects to url /swap', () => {
|
||||
cy.url().should('include', '/swap')
|
||||
})
|
||||
|
||||
it('allows navigation to send', () => {
|
||||
cy.get('#send-nav-link').click()
|
||||
cy.url().should('include', '/send')
|
||||
})
|
||||
|
||||
it('allows navigation to pool', () => {
|
||||
cy.get('#pool-nav-link').click()
|
||||
cy.url().should('include', '/pool')
|
||||
})
|
||||
|
||||
it('is connected', () => {
|
||||
cy.get('#web3-status-connected').click()
|
||||
cy.get('#web3-account-identifier-row').contains(TEST_ADDRESS_NEVER_USE_SHORTENED)
|
||||
})
|
||||
})
|
||||
13
cypress/integration/pool.test.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
describe('Pool', () => {
|
||||
beforeEach(() => cy.visit('/pool'))
|
||||
it('can search for a pool', () => {
|
||||
cy.get('#join-pool-button').click()
|
||||
cy.get('#token-search-input').type('DAI')
|
||||
})
|
||||
|
||||
it('can import a pool', () => {
|
||||
cy.get('#join-pool-button').click()
|
||||
cy.get('#import-pool-link').click({ force: true }) // blocked by the grid element in the search box
|
||||
cy.url().should('include', '/find')
|
||||
})
|
||||
})
|
||||
19
cypress/integration/remove-liquidity.test.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
describe('Remove Liquidity', () => {
|
||||
it('loads the two correct tokens', () => {
|
||||
cy.visit('/remove/0xc778417E063141139Fce010982780140Aa0cD5Ab-0xF9bA5210F91D0474bd1e1DcDAeC4C58E359AaD85')
|
||||
cy.get('#remove-liquidity-tokena-symbol').should('contain.text', 'ETH')
|
||||
cy.get('#remove-liquidity-tokenb-symbol').should('contain.text', 'MKR')
|
||||
})
|
||||
|
||||
it('does not crash if ETH is duplicated', () => {
|
||||
cy.visit('/remove/0xc778417E063141139Fce010982780140Aa0cD5Ab-0xc778417E063141139Fce010982780140Aa0cD5Ab')
|
||||
cy.get('#remove-liquidity-tokena-symbol').should('contain.text', 'ETH')
|
||||
cy.get('#remove-liquidity-tokenb-symbol').should('not.contain.text', 'ETH')
|
||||
})
|
||||
|
||||
it('token not in storage is loaded', () => {
|
||||
cy.visit('/remove/0xb290b2f9f8f108d03ff2af3ac5c8de6de31cdf6d-0xF9bA5210F91D0474bd1e1DcDAeC4C58E359AaD85')
|
||||
cy.get('#remove-liquidity-tokena-symbol').should('contain.text', 'SKL')
|
||||
cy.get('#remove-liquidity-tokenb-symbol').should('contain.text', 'MKR')
|
||||
})
|
||||
})
|
||||
9
cypress/integration/send.test.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
describe('Send', () => {
|
||||
beforeEach(() => cy.visit('/send'))
|
||||
|
||||
it('can enter an amount into input', () => {
|
||||
cy.get('#sending-no-swap-input')
|
||||
.type('0.001')
|
||||
.should('have.value', '0.001')
|
||||
})
|
||||
})
|
||||
44
cypress/integration/swap.test.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
describe('Swap', () => {
|
||||
beforeEach(() => cy.visit('/swap'))
|
||||
it('can enter an amount into input', () => {
|
||||
cy.get('#swap-currency-input .token-amount-input')
|
||||
.type('0.001')
|
||||
.should('have.value', '0.001')
|
||||
})
|
||||
|
||||
it('zero swap amount', () => {
|
||||
cy.get('#swap-currency-input .token-amount-input')
|
||||
.type('0.0')
|
||||
.should('have.value', '0.0')
|
||||
})
|
||||
|
||||
it('invalid swap amount', () => {
|
||||
cy.get('#swap-currency-input .token-amount-input')
|
||||
.type('\\')
|
||||
.should('have.value', '')
|
||||
})
|
||||
|
||||
it('can enter an amount into output', () => {
|
||||
cy.get('#swap-currency-output .token-amount-input')
|
||||
.type('0.001')
|
||||
.should('have.value', '0.001')
|
||||
})
|
||||
|
||||
it('zero output amount', () => {
|
||||
cy.get('#swap-currency-output .token-amount-input')
|
||||
.type('0.0')
|
||||
.should('have.value', '0.0')
|
||||
})
|
||||
|
||||
it('can swap ETH for DAI', () => {
|
||||
cy.get('#swap-currency-output .open-currency-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')
|
||||
cy.get('#swap-currency-input .token-amount-input').type('0.001', { force: true })
|
||||
cy.get('#swap-currency-output .token-amount-input').should('not.equal', '')
|
||||
cy.get('#show-advanced').click()
|
||||
cy.get('#swap-button').click()
|
||||
cy.get('#confirm-swap-or-send').should('contain', 'Confirm Swap')
|
||||
})
|
||||
})
|
||||
10
cypress/support/commands.d.ts
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
export const TEST_ADDRESS_NEVER_USE: string
|
||||
|
||||
export const TEST_ADDRESS_NEVER_USE_SHORTENED: string
|
||||
|
||||
// declare namespace Cypress {
|
||||
// // eslint-disable-next-line @typescript-eslint/class-name-casing
|
||||
// interface cy {
|
||||
// additionalCommands(): void
|
||||
// }
|
||||
// }
|
||||
81
cypress/support/commands.js
Normal file
@@ -0,0 +1,81 @@
|
||||
// ***********************************************
|
||||
// For more comprehensive examples of custom
|
||||
// commands please read more here:
|
||||
// https://on.cypress.io/custom-commands
|
||||
// ***********************************************
|
||||
|
||||
import { JsonRpcProvider } from '@ethersproject/providers'
|
||||
import { Wallet } from '@ethersproject/wallet'
|
||||
import { _Eip1193Bridge } from '@ethersproject/experimental/lib/eip1193-bridge'
|
||||
|
||||
// never send real ether to this, obviously
|
||||
const PRIVATE_KEY_TEST_NEVER_USE = '0xad20c82497421e9784f18460ad2fe84f73569068e98e270b3e63743268af5763'
|
||||
|
||||
// address of the above key
|
||||
export const TEST_ADDRESS_NEVER_USE = '0x0fF2D1eFd7A57B7562b2bf27F3f37899dB27F4a5'
|
||||
|
||||
export const TEST_ADDRESS_NEVER_USE_SHORTENED = '0x0fF2...F4a5'
|
||||
|
||||
class CustomizedBridge extends _Eip1193Bridge {
|
||||
async sendAsync(...args) {
|
||||
console.debug('sendAsync called', ...args)
|
||||
return this.send(...args)
|
||||
}
|
||||
async send(...args) {
|
||||
console.debug('send called', ...args)
|
||||
const isCallbackForm = typeof args[0] === 'object' && typeof args[1] === 'function'
|
||||
let callback
|
||||
let method
|
||||
let params
|
||||
if (isCallbackForm) {
|
||||
callback = args[1]
|
||||
method = args[0].method
|
||||
params = args[0].params
|
||||
} else {
|
||||
method = args[0]
|
||||
params = args[1]
|
||||
}
|
||||
if (method === 'eth_requestAccounts' || method === 'eth_accounts') {
|
||||
if (isCallbackForm) {
|
||||
callback({ result: [TEST_ADDRESS_NEVER_USE] })
|
||||
} else {
|
||||
return Promise.resolve([TEST_ADDRESS_NEVER_USE])
|
||||
}
|
||||
}
|
||||
if (method === 'eth_chainId') {
|
||||
if (isCallbackForm) {
|
||||
callback(null, { result: '0x4' })
|
||||
} else {
|
||||
return Promise.resolve('0x4')
|
||||
}
|
||||
}
|
||||
try {
|
||||
const result = await super.send(method, params)
|
||||
console.debug('result received', method, params, result)
|
||||
if (isCallbackForm) {
|
||||
callback(null, { result })
|
||||
} else {
|
||||
return result
|
||||
}
|
||||
} catch (error) {
|
||||
if (isCallbackForm) {
|
||||
callback(error, null)
|
||||
} else {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// sets up the injected provider to be a mock ethereum provider with the given mnemonic/index
|
||||
Cypress.Commands.overwrite('visit', (original, url, options) => {
|
||||
return original(url, {
|
||||
...options,
|
||||
onBeforeLoad(win) {
|
||||
options && options.onBeforeLoad && options.onBeforeLoad(win)
|
||||
const provider = new JsonRpcProvider('https://rinkeby.infura.io/v3/acb7e55995d04c49bfb52b7141599467', 4)
|
||||
const signer = new Wallet(PRIVATE_KEY_TEST_NEVER_USE, provider)
|
||||
win.ethereum = new CustomizedBridge(signer, provider)
|
||||
}
|
||||
})
|
||||
})
|
||||
9
cypress/support/index.js
Normal file
@@ -0,0 +1,9 @@
|
||||
// ***********************************************************
|
||||
// This file is processed and loaded automatically before your test files.
|
||||
//
|
||||
// You can read more here:
|
||||
// https://on.cypress.io/configuration
|
||||
// ***********************************************************
|
||||
|
||||
// Import commands.ts using ES2015 syntax:
|
||||
import './commands'
|
||||
17
cypress/tsconfig.json
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"strict": true,
|
||||
"baseUrl": "../node_modules",
|
||||
"target": "es5",
|
||||
"lib": [
|
||||
"es5",
|
||||
"dom"
|
||||
],
|
||||
"types": [
|
||||
"cypress"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"**/*.ts"
|
||||
]
|
||||
}
|
||||
19
netlify.toml
@@ -7,14 +7,21 @@
|
||||
conditions = {Country=["BY","CU","IR","IQ","CI","LR","KP","SD","SY","ZW"]}
|
||||
headers = {Link="<https://uniswap.exchange>"}
|
||||
|
||||
# forward migrate
|
||||
[[redirects]]
|
||||
from = "https://migrate.uniswap.exchange/*"
|
||||
to = "https://uniswap.exchange/migrate/v1"
|
||||
status = 301
|
||||
force = true
|
||||
|
||||
# forward v2 subdomain to apex
|
||||
[[redirects]]
|
||||
from = "https://v2.uniswap.exchange/*"
|
||||
to = "https://uniswap.exchange/:splat"
|
||||
status = 301
|
||||
|
||||
# support SPA setup
|
||||
[[redirects]]
|
||||
from = "/*"
|
||||
to = "/index.html"
|
||||
status = 200
|
||||
|
||||
[build.environment]
|
||||
REACT_APP_IS_PRODUCTION_DEPLOY = "false"
|
||||
|
||||
[context.production.environment]
|
||||
REACT_APP_IS_PRODUCTION_DEPLOY = "true"
|
||||
|
||||
107
package.json
@@ -1,56 +1,94 @@
|
||||
{
|
||||
"name": "uniswap",
|
||||
"description": "Uniswap Exchange Protocol",
|
||||
"version": "0.1.0",
|
||||
"name": "@uniswap/interface",
|
||||
"description": "Uniswap Interface",
|
||||
"homepage": "https://uniswap.exchange",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@reach/dialog": "^0.2.8",
|
||||
"@reach/tooltip": "^0.2.0",
|
||||
"@uniswap/sdk": "^1.0.0-beta.4",
|
||||
"@web3-react/core": "^6.0.2",
|
||||
"@web3-react/fortmatic-connector": "^6.0.2",
|
||||
"@web3-react/injected-connector": "^6.0.3",
|
||||
"@web3-react/network-connector": "^6.0.4",
|
||||
"@web3-react/portis-connector": "^6.0.2",
|
||||
"@web3-react/walletconnect-connector": "^6.0.2",
|
||||
"@web3-react/walletlink-connector": "^6.0.2",
|
||||
"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/providers": "5.0.0-beta.162",
|
||||
"@ethersproject/strings": "^5.0.0-beta.136",
|
||||
"@ethersproject/units": "^5.0.0-beta.132",
|
||||
"@ethersproject/wallet": "^5.0.0-beta.141",
|
||||
"@popperjs/core": "^2.4.0",
|
||||
"@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/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-window": "^1.8.2",
|
||||
"@types/rebass": "^4.0.5",
|
||||
"@types/styled-components": "^4.2.0",
|
||||
"@types/testing-library__cypress": "^5.0.5",
|
||||
"@typescript-eslint/eslint-plugin": "^2.31.0",
|
||||
"@typescript-eslint/parser": "^2.31.0",
|
||||
"@uniswap/sdk": "^2.0.5",
|
||||
"@uniswap/v2-core": "1.0.0",
|
||||
"@uniswap/v2-periphery": "^1.1.0-beta.0",
|
||||
"@web3-react/core": "^6.0.9",
|
||||
"@web3-react/fortmatic-connector": "^6.0.9",
|
||||
"@web3-react/injected-connector": "^6.0.7",
|
||||
"@web3-react/portis-connector": "^6.0.9",
|
||||
"@web3-react/walletconnect-connector": "^6.0.9",
|
||||
"@web3-react/walletlink-connector": "^6.0.9",
|
||||
"copy-to-clipboard": "^3.2.0",
|
||||
"escape-string-regexp": "^2.0.0",
|
||||
"ethers": "^4.0.44",
|
||||
"history": "^4.9.0",
|
||||
"cross-env": "^7.0.2",
|
||||
"cypress": "^4.5.0",
|
||||
"eslint": "^6.8.0",
|
||||
"eslint-config-prettier": "^6.11.0",
|
||||
"eslint-plugin-prettier": "^3.1.3",
|
||||
"eslint-plugin-react": "^7.19.0",
|
||||
"eslint-plugin-react-hooks": "^4.0.0",
|
||||
"i18next": "^15.0.9",
|
||||
"i18next-browser-languagedetector": "^3.0.1",
|
||||
"i18next-xhr-backend": "^2.0.1",
|
||||
"jazzicon": "^1.5.0",
|
||||
"lodash.flatmap": "^4.5.0",
|
||||
"polished": "^3.3.2",
|
||||
"prettier": "^1.17.0",
|
||||
"qrcode.react": "^0.9.3",
|
||||
"react": "^16.8.6",
|
||||
"qs": "^6.9.4",
|
||||
"react": "^16.13.1",
|
||||
"react-device-detect": "^1.6.2",
|
||||
"react-dom": "^16.8.6",
|
||||
"react-feather": "^1.1.6",
|
||||
"react-dom": "^16.13.1",
|
||||
"react-feather": "^2.0.8",
|
||||
"react-ga": "^2.5.7",
|
||||
"react-i18next": "^10.7.0",
|
||||
"react-popper": "^2.2.3",
|
||||
"react-redux": "^7.2.0",
|
||||
"react-router-dom": "^5.0.0",
|
||||
"react-scripts": "^3.0.1",
|
||||
"react-scripts": "^3.4.1",
|
||||
"react-spring": "^8.0.27",
|
||||
"react-switch": "^5.0.1",
|
||||
"react-use-gesture": "^6.0.14",
|
||||
"styled-components": "^4.2.0"
|
||||
"react-window": "^1.8.5",
|
||||
"rebass": "^4.0.7",
|
||||
"redux-localstorage-simple": "^2.2.0",
|
||||
"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"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "react-scripts start",
|
||||
"build": "react-scripts build",
|
||||
"build": "cross-env REACT_APP_GIT_COMMIT_HASH=$(git show -s --format=%H) react-scripts build",
|
||||
"ipfs-build": "cross-env PUBLIC_URL=\".\" react-scripts build",
|
||||
"test": "react-scripts test --env=jsdom",
|
||||
"eject": "react-scripts eject",
|
||||
"lint:base": "yarn eslint './src/**/*.{js,jsx}'",
|
||||
"format:base": "yarn prettier './src/**/*.{js,jsx,scss}'",
|
||||
"fix:lint": "yarn lint:base --fix",
|
||||
"fix:format": "yarn format:base --write",
|
||||
"fix:all": "yarn fix:lint && yarn fix:format",
|
||||
"check:lint": "yarn lint:base",
|
||||
"check:format": "yarn format:base --check",
|
||||
"check:all": "yarn check:lint && yarn check:format"
|
||||
"lint": "eslint 'src/**/*.{js,jsx,ts,tsx}'",
|
||||
"lint:fix": "eslint 'src/**/*.{js,jsx,ts,tsx}' --fix",
|
||||
"cy:run": "cypress run",
|
||||
"serve:build": "serve -s build -l 3000",
|
||||
"integration-test": "yarn build && start-server-and-test 'yarn run serve:build' http://localhost:3000 cy:run"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": "react-app"
|
||||
@@ -67,8 +105,5 @@
|
||||
"last 1 safari version"
|
||||
]
|
||||
},
|
||||
"license": "GPL-3.0-or-later",
|
||||
"devDependencies": {
|
||||
"prettier": "^1.17.0"
|
||||
}
|
||||
"license": "GPL-3.0-or-later"
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 31 KiB |
BIN
public/favicon.png
Normal file
|
After Width: | Height: | Size: 6.9 KiB |
BIN
public/images/192x192_App_Icon.png
Normal file
|
After Width: | Height: | Size: 50 KiB |
BIN
public/images/512x512_App_Icon.png
Normal file
|
After Width: | Height: | Size: 378 KiB |
@@ -2,9 +2,12 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico" />
|
||||
<link rel="shortcut icon" type="image/png" href="%PUBLIC_URL%/favicon.png" />
|
||||
<link rel="apple-touch-icon" sizes="192x192" href="%PUBLIC_URL%/images/192x192_App_Icon.png" />
|
||||
<link rel="apple-touch-icon" sizes="512x512" href="%PUBLIC_URL%/images/512x512_App_Icon.png" />
|
||||
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<meta name="theme-color" content="#ff007a" />
|
||||
<meta name="fortmatic-site-verification" content="j93LgcVZk79qcgyo" />
|
||||
<!--
|
||||
manifest.json provides metadata used when your web app is installed on a
|
||||
@@ -20,11 +23,12 @@
|
||||
work correctly both with client-side routing and a non-root public URL.
|
||||
Learn how to configure a non-root public URL by running `npm run build`.
|
||||
-->
|
||||
<title>Uniswap Exchange</title>
|
||||
|
||||
<title>Uniswap Interface</title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root" />
|
||||
<div id="root"></div>
|
||||
<!--
|
||||
This HTML file is a template.
|
||||
If you open it directly in the browser, you will see an empty page.
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
"searchOrPaste": "Search Token Name, Symbol, or Address",
|
||||
"searchOrPasteMobile": "Name, Symbol, or Address",
|
||||
"noExchange": "No Exchange Found",
|
||||
"noToken": "No Token Found",
|
||||
"exchangeRate": "Exchange Rate",
|
||||
"unknownError": "Oops! An unknown error occurred. Please refresh the page, or visit from another browser or device.",
|
||||
"enterValueCont": "Enter a {{ missingCurrencyValue }} value to continue.",
|
||||
@@ -84,5 +85,7 @@
|
||||
"enterTokenCont": "Enter a token address to continue",
|
||||
"priceChange": "Expected price slippage",
|
||||
"forAtLeast": "for at least ",
|
||||
"brokenToken": "The selected token is not compatible with Uniswap V1. Adding liquidity will result in locked funds."
|
||||
"brokenToken": "The selected token is not compatible with Uniswap V1. Adding liquidity will result in locked funds.",
|
||||
"toleranceExplanation": "Lowering this limit decreases your risk of frontrunning. However, this makes more likely that your transaction will fail due to normal price movements.",
|
||||
"tokenSearchPlaceholder": "Search name or paste address"
|
||||
}
|
||||
|
||||
75
public/locales/he.json
Normal file
@@ -0,0 +1,75 @@
|
||||
{
|
||||
"noWallet": "לא נמצא ארנק",
|
||||
"wrongNetwork": "נבחרה רשת לא נכונה",
|
||||
"switchNetwork": "{{ correctNetwork }} יש צורך לשנות את הרשת ל",
|
||||
"installWeb3MobileBrowser": "יש צורך בארנק ווב3.0, תתקין מטאמאסק או ארנק דומה",
|
||||
"installMetamask": " Metamask יש צורך להתקין תוסף מטאמאסק לדפדפן, חפשו בגוגל ",
|
||||
"disconnected": "מנותק",
|
||||
"swap": "המרה",
|
||||
"send": "שליחה",
|
||||
"pool": "להפקיד",
|
||||
"betaWarning": "הפרויקט נמצא בשלב בטא, השתמשו באחריות",
|
||||
"input": "מוכר",
|
||||
"output": "אקבל",
|
||||
"estimated": "הערכה",
|
||||
"balance": "בארנק שלי {{ balanceInput }}",
|
||||
"unlock": "שחרור נעילת ארנק",
|
||||
"pending": "ממתין לאישור",
|
||||
"selectToken": "בחרו את הטוקן להמרה",
|
||||
"searchOrPaste": "הכניסו שם או כתובת של טוקן לחיפוש",
|
||||
"noExchange": "לא מתאפשרת המרה",
|
||||
"exchangeRate": "שער המרה",
|
||||
"enterValueCont": "כדי להמשיך {{ missingCurrencyValue }} הזינו ",
|
||||
"selectTokenCont": "בחרו טוקן כדי להמשיך",
|
||||
"noLiquidity": "אין נזילות",
|
||||
"unlockTokenCont": "יש צורך לאשר את הטוקן למסחר",
|
||||
"transactionDetails": "פרטי הטרנזקציה",
|
||||
"hideDetails": "הסתר פרטים נוספים",
|
||||
"youAreSelling": "למכירה",
|
||||
"orTransFail": "או שהטרנזקציה תיכשל",
|
||||
"youWillReceive": "תוצר המרה מינימלי",
|
||||
"youAreBuying": "קונה",
|
||||
"itWillCost": "זה יעלה",
|
||||
"insufficientBalance": "אין בחשבון מספיק מטבעות",
|
||||
"inputNotValid": "קלט לא תקין",
|
||||
"differentToken": "יש צורך בטוקנים שונים",
|
||||
"noRecipient": "לא הוכנסה כתובת ארנק יעד",
|
||||
"invalidRecipient": "לא הוכנסה כתובת תקינה",
|
||||
"recipientAddress": "כתובת יעד",
|
||||
"youAreSending": "כמות לשליחה",
|
||||
"willReceive": "יתקבל לכל הפחות",
|
||||
"to": "אל",
|
||||
"addLiquidity": "להוספת נזילות למאגר",
|
||||
"deposit": "הפקדה",
|
||||
"currentPoolSize": "גודל מאגר הנזילות הכולל",
|
||||
"yourPoolShare": "חלקך במאגר הנזילות",
|
||||
"noZero": "אפס אינו ערך תקין",
|
||||
"mustBeETH": "ETH חייב להופיע באחד מהצדדים",
|
||||
"enterCurrencyOrLabelCont": "כדי להמשיך {{ inputCurrency }} או {{ label }} הכנס",
|
||||
"youAreAdding": "מתווספים למאגר",
|
||||
"and": "וגם",
|
||||
"intoPool": "לתוך הנזילות",
|
||||
"outPool": "מתוך",
|
||||
"youWillMint": "יונפקו לכם",
|
||||
"liquidityTokens": "טוקנים של נזילות",
|
||||
"totalSupplyIs": "חלקך במאגר הנזילות",
|
||||
"youAreSettingExRate": "שער ההמרה יקבע על ידך",
|
||||
"totalSupplyIs0": "אין לך טוקנים של נזילות",
|
||||
"tokenWorth": "שווי כל טוקן נזילות הינו",
|
||||
"firstLiquidity": "את\ה הראשון\ה שמזרים נזילות למאגר",
|
||||
"initialExchangeRate": "ושל האית'ר הינן בערך שווה {{ label }} תוודאו שההפקדה של הטוקן",
|
||||
"removeLiquidity": "הוצאה של נזילות",
|
||||
"poolTokens": "טוקנים של מאגר הנזילות",
|
||||
"enterLabelCont": "כדי להמשיך {{ label }} הכנס ",
|
||||
"youAreRemoving": "יוסרו",
|
||||
"youWillRemove": "יוסרו",
|
||||
"createExchange": "ליצירת זוג מסחר",
|
||||
"invalidTokenAddress": "כתובת טוקן לא נכונה",
|
||||
"exchangeExists": "{{ label }} כבר קיים זוג המרה עבור",
|
||||
"invalidSymbol": "תו שגוי",
|
||||
"invalidDecimals": "ספרות עשרוניות שגויות",
|
||||
"tokenAddress": "כתובת הטוקן",
|
||||
"label": "שם",
|
||||
"decimals": "ספרות עשרויות",
|
||||
"enterTokenCont": "הכניסו כתובת טוקן כדי להמשיך"
|
||||
}
|
||||
@@ -1,15 +1,22 @@
|
||||
{
|
||||
"short_name": "Uniswap",
|
||||
"name": "Uniswap Exchange",
|
||||
"name": "Uniswap",
|
||||
"icons": [
|
||||
{
|
||||
"src": "favicon.ico",
|
||||
"sizes": "64x64 32x32 24x24 16x16",
|
||||
"type": "image/x-icon"
|
||||
"src": "./images/192x192_App_Icon.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png",
|
||||
"purpose": "any maskable"
|
||||
},
|
||||
{
|
||||
"src": "./images/512x512_App_Icon.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png",
|
||||
"purpose": "any maskable"
|
||||
}
|
||||
],
|
||||
"start_url": ".",
|
||||
"orientation": "portrait",
|
||||
"display": "standalone",
|
||||
"theme_color": "#000000",
|
||||
"background_color": "#ffffff"
|
||||
"theme_color": "#ff007a",
|
||||
"background_color": "#fff"
|
||||
}
|
||||
|
||||
3
src/assets/images/blue-loader.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="94" height="94" viewBox="0 0 94 94" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M92 47C92 22.1472 71.8528 2 47 2C22.1472 2 2 22.1472 2 47C2 71.8528 22.1472 92 47 92" stroke="#2172E5" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 283 B |
BIN
src/assets/images/ethereum-logo.png
Normal file
|
After Width: | Height: | Size: 3.5 KiB |
@@ -1,11 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg width="256px" height="417px" viewBox="0 0 256 417" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" preserveAspectRatio="xMidYMid">
|
||||
<g>
|
||||
<polygon fill="#343434" points="127.9611 0 125.1661 9.5 125.1661 285.168 127.9611 287.958 255.9231 212.32"/>
|
||||
<polygon fill="#8C8C8C" points="127.962 0 0 212.32 127.962 287.959 127.962 154.158"/>
|
||||
<polygon fill="#3C3C3B" points="127.9611 312.1866 126.3861 314.1066 126.3861 412.3056 127.9611 416.9066 255.9991 236.5866"/>
|
||||
<polygon fill="#8C8C8C" points="127.962 416.9052 127.962 312.1852 0 236.5852"/>
|
||||
<polygon fill="#141414" points="127.9611 287.9577 255.9211 212.3207 127.9611 154.1587"/>
|
||||
<polygon fill="#393939" points="0.0009 212.3208 127.9609 287.9578 127.9609 154.1588"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 840 B |
1
src/assets/images/link.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg viewBox="0 0 24 24" focusable="false" role="presentation" aria-hidden="true" class="css-yyruks"><g fill="none" stroke="currentColor" stroke-linecap="full" stroke-width="2"><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"></path><path d="M15 3h6v6"></path><path d="M10 14L21 3"></path></g></svg>
|
||||
|
After Width: | Height: | Size: 317 B |
5
src/assets/images/menu.svg
Normal file
@@ -0,0 +1,5 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12 13C12.5523 13 13 12.5523 13 12C13 11.4477 12.5523 11 12 11C11.4477 11 11 11.4477 11 12C11 12.5523 11.4477 13 12 13Z" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M19 13C19.5523 13 20 12.5523 20 12C20 11.4477 19.5523 11 19 11C18.4477 11 18 11.4477 18 12C18 12.5523 18.4477 13 19 13Z" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M5 13C5.55228 13 6 12.5523 6 12C6 11.4477 5.55228 11 5 11C4.44772 11 4 11.4477 4 12C4 12.5523 4.44772 13 5 13Z" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 727 B |
@@ -1,4 +1,4 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="8" cy="8" r="8" fill="#E1E1E1"/>
|
||||
<path d="M7.09618 9.67828H8.18831V9.60573C8.20358 8.72745 8.45561 8.33413 9.10477 7.93317C9.78831 7.52076 10.2084 6.94033 10.2084 6.08115C10.2084 4.8401 9.26897 4 7.86754 4C6.58067 4 5.54964 4.75227 5.5 6.12697H6.66086C6.70668 5.31742 7.28329 4.96229 7.86754 4.96229C8.51671 4.96229 9.04368 5.39379 9.04368 6.06969C9.04368 6.63866 8.68854 7.03962 8.23413 7.3222C7.52387 7.75752 7.10382 8.18902 7.09618 9.60573V9.67828ZM7.67279 12C8.08902 12 8.43652 11.6601 8.43652 11.2363C8.43652 10.82 8.08902 10.4764 7.67279 10.4764C7.25274 10.4764 6.90907 10.82 6.90907 11.2363C6.90907 11.6601 7.25274 12 7.67279 12Z" fill="#737373"/>
|
||||
<circle cx="8" cy="8" r="8" fill="#EDEEF2"/>
|
||||
<path d="M7.09618 9.67828H8.18831V9.60573C8.20358 8.72745 8.45561 8.33413 9.10477 7.93317C9.78831 7.52076 10.2084 6.94033 10.2084 6.08115C10.2084 4.8401 9.26897 4 7.86754 4C6.58067 4 5.54964 4.75227 5.5 6.12697H6.66086C6.70668 5.31742 7.28329 4.96229 7.86754 4.96229C8.51671 4.96229 9.04368 5.39379 9.04368 6.06969C9.04368 6.63866 8.68854 7.03962 8.23413 7.3222C7.52387 7.75752 7.10382 8.18902 7.09618 9.60573V9.67828ZM7.67279 12C8.08902 12 8.43652 11.6601 8.43652 11.2363C8.43652 10.82 8.08902 10.4764 7.67279 10.4764C7.25274 10.4764 6.90907 10.82 6.90907 11.2363C6.90907 11.6601 7.25274 12 7.67279 12Z" fill="#565A69"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 770 B After Width: | Height: | Size: 770 B |
3
src/assets/svg/QR.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="21" height="21" viewBox="0 0 21 21" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.5 1.5H1.5V7.5H7.5V1.5ZM1.5 0H0V1.5V7.5V9H1.5H7.5H9V7.5V1.5V0H7.5H1.5ZM4.5 3H3V4.5V6H4.5H6V4.5V3H4.5ZM1.5 19.5V13.5H7.5V19.5H1.5ZM0 12H1.5H7.5H9V13.5V19.5V21H7.5H1.5H0V19.5V13.5V12ZM4.5 15H3V16.5V18H4.5H6V16.5V15H4.5ZM13.5 1.5H19.5V7.5H13.5V1.5ZM12 0H13.5H19.5H21V1.5V7.5V9H19.5H13.5H12V7.5V1.5V0ZM16.5 3H15V4.5V6H16.5H18V4.5V3H16.5ZM16.5 12H12V21H13.5V16.5H15V18H21V12H19.5V13.5H16.5V12ZM18 19.5H16.5V21H18V19.5ZM19.5 19.5H21V21H19.5V19.5Z" fill="black"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 611 B |
@@ -1,12 +0,0 @@
|
||||
import React from 'react'
|
||||
|
||||
const SVGArrowDown = props => (
|
||||
<svg width="1em" height="1em" viewBox="0 0 9 10" fill="currentColor" {...props}>
|
||||
<path
|
||||
d="M5.298 0H4.24v7.911h-.075L1.256 4.932l-.717.735L4.769 10 9 5.667l-.718-.735-2.908 2.979h-.076V0z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
|
||||
export default SVGArrowDown
|
||||
11
src/assets/svg/lightcircle.svg
Normal file
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="22px" height="22px" viewBox="0 0 22 22" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<!-- Generator: Sketch 52 (66869) - http://www.bohemiancoding.com/sketch -->
|
||||
<title>Path</title>
|
||||
<desc>Created with Sketch.</desc>
|
||||
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="round" stroke-linejoin="round">
|
||||
<g id="check-circle-(1)" transform="translate(1.000000, 0.000000)" stroke="#D9EAFF" stroke-width="2">
|
||||
<path d="M20,10.08 L20,11 C19.9974678,15.4286859 17.082294,19.328213 12.8353524,20.583901 C8.58841086,21.839589 4.02139355,20.1523121 1.61095509,16.4370663 C-0.799483376,12.7218205 -0.479136554,7.86363898 2.39827419,4.49707214 C5.27568494,1.13050531 10.0247126,0.0575252842 14.07,1.86" id="Path"></path>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 911 B |
14
src/assets/svg/logo.svg
Normal file
@@ -0,0 +1,14 @@
|
||||
<svg width="33" height="32" viewBox="0 0 33 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M14.3463 13.7365C14.9529 14.8091 13.5964 15.1416 12.9598 15.2028C11.9952 15.2969 11.7955 14.7385 11.9839 14.0369C12.0457 13.7857 12.183 13.5597 12.377 13.3896C12.571 13.2194 12.8125 13.1133 13.0686 13.0857C13.3209 13.0625 13.5747 13.1112 13.8007 13.2263C14.0267 13.3414 14.2158 13.5183 14.3463 13.7365Z" fill="black"/>
|
||||
<path d="M18.1331 11.752C17.4281 17.1689 26.9322 16.0416 26.7513 20.3529C27.6888 19.1287 28.0919 15.8024 25.3319 14.0903C22.8729 12.5637 19.6687 13.3991 18.1331 11.752Z" fill="black"/>
|
||||
<path d="M23.6074 9.70952C23.5455 9.65207 23.4818 9.59557 23.4199 9.53906C23.4827 9.59651 23.5455 9.65678 23.6074 9.70952Z" fill="black"/>
|
||||
<path d="M25.8167 13.6578L25.8111 13.6493C25.7253 13.4892 25.6275 13.3359 25.5186 13.1906C25.1981 12.7546 24.7385 12.4421 24.2164 12.3054C23.8689 12.2153 23.5135 12.1592 23.1552 12.1378C22.7924 12.1114 22.4221 12.0963 22.048 12.0766C21.2981 12.0351 20.5303 11.9579 19.7803 11.7432C19.5928 11.6895 19.4053 11.6311 19.2244 11.5605C19.1306 11.5266 19.0434 11.4899 18.9506 11.4503C18.8578 11.4107 18.7631 11.3655 18.6694 11.3175C18.3167 11.1235 17.9901 10.8852 17.6972 10.6084C17.1197 10.0688 16.666 9.45662 16.2113 8.86332C15.7827 8.26774 15.3227 7.69559 14.8333 7.14934C14.3504 6.6142 13.7796 6.16633 13.1458 5.82524C12.4896 5.4932 11.7768 5.28871 11.0449 5.22253C11.8041 5.13971 12.5721 5.23381 13.2892 5.49752C14.0129 5.77972 14.6744 6.20174 15.2364 6.73968C15.6033 7.08534 15.9477 7.45433 16.2676 7.84435C18.6488 7.37347 20.58 7.79161 22.064 8.6034L22.0977 8.62035C22.5701 8.8777 23.0127 9.18658 23.4177 9.54138C23.4824 9.59788 23.5461 9.65439 23.6052 9.71183C23.9213 10.0036 24.2155 10.3183 24.4855 10.6536L24.5061 10.6809C25.3826 11.7959 25.8157 12.9552 25.8167 13.6578Z" fill="black"/>
|
||||
<path d="M25.8161 13.6577L25.8105 13.6465L25.8161 13.6577Z" fill="black"/>
|
||||
<path d="M11.1654 5.91992C11.7729 6.00845 12.3925 6.25142 12.7919 6.71476C13.1913 7.1781 13.3384 7.78082 13.4538 8.35434C13.5475 8.79885 13.6216 9.25183 13.7959 9.67279C13.8803 9.87809 14.004 10.058 14.1072 10.252C14.1925 10.413 14.3481 10.558 14.4081 10.7304C14.4188 10.7551 14.4228 10.7823 14.4197 10.8091C14.4166 10.8359 14.4064 10.8614 14.3903 10.8829C14.1784 11.1184 13.6066 10.8566 13.3909 10.7511C13.0182 10.5655 12.6843 10.3097 12.4075 9.99769C11.5423 9.0324 11.0951 7.64332 11.1223 6.37855C11.1283 6.225 11.1427 6.07189 11.1654 5.91992Z" fill="black"/>
|
||||
<path d="M21.326 16.8066C20.0136 20.486 25.9665 22.9544 23.7363 26.695C26.0246 25.7457 27.1112 22.8781 26.1615 20.6028C25.3309 18.6054 22.8747 17.8774 21.326 16.8066Z" fill="black"/>
|
||||
<path d="M13.4531 21.6165C13.8108 21.3457 14.2018 21.1226 14.6165 20.9526C15.0361 20.7839 15.4714 20.6577 15.9159 20.5759C16.798 20.4073 17.6708 20.3659 18.4039 20.0693C18.7661 19.9272 19.0994 19.7197 19.3873 19.4571C19.666 19.197 19.8793 18.8743 20.0098 18.5154C20.1435 18.1367 20.1977 17.7343 20.1692 17.3335C20.1361 16.903 20.0391 16.4798 19.8814 16.0781C20.2082 16.4195 20.4461 16.8366 20.5741 17.2925C20.7022 17.7483 20.7163 18.2288 20.6154 18.6915C20.5007 19.1812 20.2531 19.6295 19.9001 19.9864C19.5507 20.3322 19.1294 20.5959 18.6664 20.7586C18.2246 20.9143 17.7639 21.0093 17.2968 21.0411C16.8468 21.0788 16.4118 21.0892 15.9843 21.1146C15.12 21.151 14.2664 21.3203 13.4531 21.6165Z" fill="black"/>
|
||||
<path d="M21.8492 28.0849C21.7171 28.1904 21.5849 28.3025 21.4396 28.4004C21.2934 28.4972 21.1401 28.5828 20.9811 28.6566C20.6502 28.8188 20.2863 28.9016 19.9181 28.8986C18.9206 28.8798 18.2156 28.1339 17.8031 27.291C17.5219 26.7166 17.3278 26.0959 16.9941 25.5488C16.5169 24.7662 15.7004 24.1362 14.7441 24.2529C14.3542 24.3019 13.9885 24.478 13.772 24.818C13.202 25.7061 14.0204 26.9501 15.0638 26.774C15.1526 26.7604 15.2394 26.7364 15.3226 26.7024C15.4054 26.667 15.4828 26.6197 15.5522 26.5621C15.698 26.4402 15.8079 26.2806 15.87 26.1007C15.9386 25.913 15.9538 25.7098 15.9141 25.5139C15.8713 25.3092 15.751 25.1292 15.5785 25.012C15.779 25.1063 15.9353 25.2751 16.0144 25.4829C16.0965 25.6966 16.1176 25.9292 16.0754 26.1543C16.0344 26.3889 15.9314 26.6082 15.7772 26.7891C15.6953 26.8821 15.6006 26.963 15.496 27.0292C15.3923 27.0947 15.2812 27.1475 15.1651 27.1865C14.9296 27.2673 14.6789 27.2934 14.432 27.2628C14.0852 27.2132 13.7582 27.0707 13.4851 26.8503C13.1626 26.5941 12.9226 26.2589 12.6311 25.9707C12.2969 25.6184 11.9025 25.3291 11.4667 25.1165C11.1662 24.9839 10.8517 24.886 10.5293 24.8246C10.3671 24.7907 10.203 24.7662 10.039 24.7455C9.96397 24.7379 9.59648 24.656 9.54492 24.704C10.052 24.2351 10.6048 23.8187 11.1949 23.4609C11.8008 23.0997 12.4513 22.8201 13.1298 22.6294C13.8333 22.4305 14.569 22.3738 15.2944 22.4627C15.6678 22.5077 16.0336 22.6028 16.3819 22.7452C16.7469 22.8916 17.0838 23.1007 17.3775 23.363C17.6682 23.6381 17.9032 23.9672 18.0694 24.3321C18.2194 24.6736 18.3313 25.0308 18.4031 25.3972C18.6168 26.4924 18.5381 28.1904 19.9659 28.4409C20.0403 28.4554 20.1154 28.4661 20.1909 28.4729L20.4243 28.4786C20.5848 28.4671 20.7442 28.4441 20.9015 28.4098C21.2272 28.3329 21.5446 28.224 21.8492 28.0849Z" fill="black"/>
|
||||
<path d="M13.5531 26.8907L13.5156 26.8613L13.5531 26.8907Z" fill="black"/>
|
||||
<path d="M12.2721 11.4336C12.2201 11.6343 12.1293 11.8228 12.0049 11.9883C11.7733 12.2902 11.4669 12.5256 11.1162 12.6711C10.8007 12.8076 10.4667 12.8958 10.1253 12.9329C10.0512 12.9423 9.97433 12.9479 9.90027 12.9536C9.684 12.9618 9.47603 13.0394 9.30683 13.175C9.13763 13.3106 9.01608 13.497 8.95998 13.707C8.93429 13.8115 8.91519 13.9175 8.90279 14.0244C8.86905 14.3012 8.86342 14.5894 8.83342 14.9378C8.75997 15.5308 8.58922 16.1074 8.32812 16.6443C7.98876 17.3619 7.60815 17.9401 7.69627 18.767C7.75439 19.3038 8.02813 19.6635 8.39187 20.0346C9.0481 20.7089 10.5171 21.0102 10.189 22.6715C9.9912 23.6631 8.35437 24.7038 6.05382 25.0673C6.28256 25.0324 5.76039 24.1481 5.72757 24.0916C5.48102 23.7036 5.21103 23.3382 5.0151 22.9173C4.63073 22.1008 4.45261 21.1562 4.61011 20.2615C4.77604 19.3198 5.46977 18.5984 6.04632 17.8789C6.73255 17.0229 7.45252 15.9013 7.61189 14.79C7.64939 14.5207 7.67564 14.1835 7.73564 13.8482C7.79284 13.4768 7.90867 13.1169 8.07875 12.7822C8.19485 12.5626 8.3477 12.3648 8.53062 12.1974C8.62598 12.1085 8.6889 11.99 8.7093 11.8609C8.72969 11.7318 8.70639 11.5996 8.64311 11.4854L4.97572 4.85926L10.2434 11.3893C10.3034 11.465 10.3792 11.5265 10.4655 11.5694C10.5518 11.6123 10.6464 11.6357 10.7427 11.6377C10.839 11.6398 10.9345 11.6206 11.0225 11.5814C11.1106 11.5422 11.189 11.484 11.2521 11.411C11.3188 11.3328 11.3567 11.2339 11.3592 11.131C11.3618 11.0281 11.3289 10.9274 11.2662 10.846C10.9221 10.4043 10.5584 9.95412 10.2059 9.5115L8.8803 7.86344L6.21975 4.57297L2.5918 0L6.56099 4.27821L9.39309 7.42364L10.8059 9.00013C11.2746 9.53034 11.7433 10.0436 12.2121 10.6011L12.2889 10.6953L12.3058 10.8412C12.3287 11.0392 12.3173 11.2396 12.2721 11.4336Z" fill="black"/>
|
||||
<path d="M13.4856 26.8466C13.231 26.6491 13.0046 26.4174 12.8125 26.1582C13.0186 26.405 13.2436 26.6352 13.4856 26.8466Z" fill="black"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 6.9 KiB |
14
src/assets/svg/logo_pink.svg
Normal file
@@ -0,0 +1,14 @@
|
||||
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M14.3078 13.7367C14.9143 14.8093 13.5578 15.1418 12.9212 15.203C11.9566 15.2972 11.7569 14.7387 11.9453 14.0371C12.0072 13.7859 12.1444 13.5599 12.3384 13.3898C12.5325 13.2196 12.7739 13.1135 13.03 13.0859C13.2823 13.0627 13.5361 13.1114 13.7621 13.2265C13.9881 13.3417 14.1773 13.5185 14.3078 13.7367V13.7367Z" fill="#FF007A"/>
|
||||
<path d="M18.094 11.7521C17.389 17.169 26.8931 16.0418 26.7122 20.3531C27.6497 19.1288 28.0528 15.8026 25.2929 14.0905C22.8339 12.5639 19.6296 13.3992 18.094 11.7521Z" fill="#FF007A"/>
|
||||
<path d="M23.5684 9.7093C23.5065 9.65186 23.4427 9.59535 23.3809 9.53885C23.4437 9.59629 23.5065 9.65657 23.5684 9.7093Z" fill="#FF007A"/>
|
||||
<path d="M25.7771 13.6581L25.7715 13.6496C25.6857 13.4895 25.5879 13.3362 25.479 13.191C25.1586 12.7549 24.699 12.4424 24.1769 12.3057C23.8293 12.2156 23.4739 12.1595 23.1156 12.1381C22.7528 12.1117 22.3825 12.0966 22.0085 12.0769C21.2585 12.0354 20.4907 11.9582 19.7407 11.7435C19.5532 11.6898 19.3658 11.6314 19.1848 11.5608C19.0911 11.5269 19.0039 11.4902 18.9111 11.4506C18.8183 11.4111 18.7236 11.3658 18.6298 11.3178C18.2772 11.1238 17.9505 10.8855 17.6577 10.6087C17.0802 10.0691 16.6265 9.45692 16.1718 8.86362C15.7432 8.26804 15.2831 7.6959 14.7937 7.14964C14.3108 6.61451 13.74 6.16664 13.1063 5.82555C12.45 5.49351 11.7372 5.28902 11.0054 5.22283C11.7646 5.14002 12.5326 5.23412 13.2497 5.49782C13.9733 5.78002 14.6348 6.20204 15.1968 6.73999C15.5637 7.08565 15.9082 7.45463 16.228 7.84465C18.6092 7.37378 20.5404 7.79192 22.0244 8.6037L22.0582 8.62065C22.5305 8.87801 22.9732 9.18689 23.3781 9.54168C23.4428 9.59819 23.5066 9.65469 23.5656 9.71214C23.8817 10.0039 24.1759 10.3187 24.4459 10.6539L24.4665 10.6812C25.3431 11.7962 25.7762 12.9555 25.7771 13.6581Z" fill="#FF007A"/>
|
||||
<path d="M25.776 13.657L25.7705 13.6458L25.776 13.657Z" fill="#FF007A"/>
|
||||
<path d="M11.1263 5.92056C11.7338 6.00909 12.3535 6.25206 12.7528 6.7154C13.1522 7.17874 13.2994 7.78146 13.4147 8.35498C13.5084 8.79949 13.5825 9.25247 13.7569 9.67343C13.8412 9.87873 13.965 10.0586 14.0681 10.2526C14.1534 10.4136 14.309 10.5587 14.369 10.731C14.3798 10.7558 14.3838 10.7829 14.3806 10.8097C14.3775 10.8365 14.3674 10.862 14.3512 10.8836C14.1394 11.119 13.5675 10.8572 13.3519 10.7517C12.9791 10.5661 12.6453 10.3104 12.3685 9.99834C11.5032 9.03304 11.056 7.64396 11.0832 6.37919C11.0892 6.22564 11.1036 6.07253 11.1263 5.92056Z" fill="#FF007A"/>
|
||||
<path d="M21.2865 16.8057C19.974 20.4851 25.927 22.9535 23.6967 26.6941C25.9851 25.7448 27.0716 22.8772 26.122 20.6019C25.2914 18.6045 22.8352 17.8765 21.2865 16.8057Z" fill="#FF007A"/>
|
||||
<path d="M13.4131 21.6174C13.7707 21.3466 14.1618 21.1234 14.5765 20.9535C14.996 20.7848 15.4313 20.6586 15.8758 20.5768C16.758 20.4082 17.6308 20.3668 18.3639 20.0701C18.7261 19.928 19.0594 19.7206 19.3473 19.458C19.626 19.1979 19.8393 18.8751 19.9698 18.5162C20.1034 18.1375 20.1577 17.7351 20.1291 17.3343C20.096 16.9038 19.999 16.4807 19.8413 16.079C20.1681 16.4204 20.4061 16.8375 20.5341 17.2933C20.6621 17.7492 20.6763 18.2297 20.5754 18.6923C20.4607 19.1821 20.2131 19.6303 19.8601 19.9872C19.5107 20.333 19.0893 20.5967 18.6264 20.7595C18.1846 20.9151 17.7238 21.0102 17.2567 21.042C16.8067 21.0797 16.3717 21.09 15.9443 21.1154C15.08 21.1519 14.2263 21.3211 13.4131 21.6174V21.6174Z" fill="#FF007A"/>
|
||||
<path d="M21.8102 28.0852C21.678 28.1907 21.5458 28.3028 21.4005 28.4007C21.2543 28.4975 21.101 28.5831 20.9421 28.6569C20.6111 28.8191 20.2472 28.9019 19.879 28.8989C18.8815 28.8801 18.1765 28.1342 17.7641 27.2913C17.4828 26.7169 17.2888 26.0962 16.955 25.5491C16.4778 24.7665 15.6613 24.1365 14.7051 24.2532C14.3151 24.3022 13.9495 24.4783 13.7329 24.8183C13.1629 25.7064 13.9814 26.9504 15.0248 26.7743C15.1135 26.7607 15.2003 26.7367 15.2835 26.7027C15.3664 26.6673 15.4437 26.62 15.5132 26.5624C15.6589 26.4405 15.7688 26.281 15.831 26.101C15.8995 25.9133 15.9148 25.7101 15.875 25.5142C15.8322 25.3095 15.7119 25.1295 15.5394 25.0123C15.74 25.1066 15.8962 25.2754 15.9754 25.4832C16.0574 25.6969 16.0785 25.9295 16.0363 26.1546C15.9953 26.3892 15.8924 26.6085 15.7382 26.7894C15.6563 26.8824 15.5616 26.9633 15.4569 27.0295C15.3532 27.095 15.2421 27.1478 15.126 27.1868C14.8905 27.2677 14.6399 27.2937 14.3929 27.2631C14.0461 27.2135 13.7191 27.071 13.4461 26.8506C13.1236 26.5944 12.8836 26.2592 12.592 25.971C12.2578 25.6187 11.8635 25.3294 11.4277 25.1168C11.1272 24.9842 10.8127 24.8863 10.4902 24.8249C10.328 24.791 10.164 24.7665 9.99991 24.7458C9.92491 24.7383 9.55742 24.6563 9.50586 24.7043C10.0129 24.2354 10.5657 23.819 11.1558 23.4612C11.7617 23.1 12.4123 22.8205 13.0908 22.6297C13.7942 22.4308 14.53 22.3741 15.2554 22.463C15.6288 22.508 15.9945 22.6031 16.3428 22.7455C16.7078 22.892 17.0447 23.101 17.3384 23.3633C17.6292 23.6384 17.8641 23.9675 18.0303 24.3324C18.1803 24.6739 18.2923 25.0311 18.364 25.3975C18.5778 26.4927 18.499 28.1907 19.9268 28.4412C20.0012 28.4557 20.0763 28.4664 20.1518 28.4732L20.3852 28.4789C20.5457 28.4674 20.7052 28.4444 20.8624 28.4101C21.1881 28.3332 21.5056 28.2243 21.8102 28.0852V28.0852Z" fill="#FF007A"/>
|
||||
<path d="M13.514 26.8916L13.4766 26.8622L13.514 26.8916Z" fill="#FF007A"/>
|
||||
<path d="M12.2325 11.4336C12.1805 11.6343 12.0897 11.8228 11.9653 11.9883C11.7338 12.2902 11.4273 12.5256 11.0766 12.6711C10.7612 12.8076 10.4272 12.8958 10.0857 12.9329C10.0116 12.9423 9.93478 12.9479 9.86072 12.9536C9.64444 12.9618 9.43648 13.0394 9.26728 13.175C9.09808 13.3106 8.97653 13.497 8.92043 13.707C8.89474 13.8115 8.87564 13.9175 8.86324 14.0244C8.82949 14.3012 8.82387 14.5894 8.79387 14.9378C8.72041 15.5308 8.54967 16.1074 8.28857 16.6443C7.94921 17.3619 7.5686 17.9401 7.65672 18.767C7.71484 19.3038 7.98858 19.6635 8.35232 20.0346C9.00855 20.7089 10.4776 21.0102 10.1495 22.6715C9.95165 23.6631 8.31482 24.7038 6.01426 25.0673C6.24301 25.0324 5.72083 24.1481 5.68802 24.0916C5.44147 23.7036 5.17148 23.3382 4.97555 22.9173C4.59118 22.1008 4.41306 21.1562 4.57056 20.2615C4.73649 19.3198 5.43022 18.5984 6.00677 17.8789C6.693 17.0229 7.41297 15.9013 7.57234 14.79C7.60984 14.5207 7.63609 14.1835 7.69609 13.8482C7.75329 13.4768 7.86912 13.1169 8.0392 12.7822C8.1553 12.5626 8.30815 12.3648 8.49106 12.1974C8.58643 12.1085 8.64935 11.99 8.66975 11.8609C8.69014 11.7318 8.66684 11.5996 8.60356 11.4854L4.93617 4.85926L10.2038 11.3893C10.2638 11.465 10.3396 11.5265 10.4259 11.5694C10.5122 11.6123 10.6069 11.6357 10.7031 11.6377C10.7994 11.6398 10.8949 11.6206 10.983 11.5814C11.071 11.5422 11.1494 11.484 11.2126 11.411C11.2793 11.3328 11.3171 11.2339 11.3197 11.131C11.3222 11.0281 11.2894 10.9274 11.2266 10.846C10.8826 10.4043 10.5188 9.95412 10.1663 9.5115L8.84075 7.86344L6.1802 4.57297L2.55225 0L6.52144 4.27821L9.35354 7.42364L10.7663 9.00013C11.235 9.53034 11.7038 10.0436 12.1725 10.6011L12.2494 10.6953L12.2663 10.8412C12.2891 11.0392 12.2777 11.2396 12.2325 11.4336Z" fill="#FF007A"/>
|
||||
<path d="M13.447 26.8463C13.1925 26.6488 12.966 26.4171 12.7739 26.1579C12.98 26.4047 13.205 26.6349 13.447 26.8463V26.8463Z" fill="#FF007A"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 6.9 KiB |
14
src/assets/svg/logo_white.svg
Normal file
@@ -0,0 +1,14 @@
|
||||
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M14.3078 13.7367C14.9143 14.8093 13.5578 15.1418 12.9212 15.203C11.9566 15.2972 11.7569 14.7387 11.9453 14.0371C12.0072 13.7859 12.1444 13.5599 12.3384 13.3898C12.5325 13.2196 12.7739 13.1135 13.03 13.0859C13.2823 13.0627 13.5361 13.1114 13.7621 13.2265C13.9881 13.3417 14.1773 13.5185 14.3078 13.7367V13.7367Z" fill="white"/>
|
||||
<path d="M18.094 11.7521C17.389 17.169 26.8931 16.0418 26.7122 20.3531C27.6497 19.1288 28.0528 15.8026 25.2929 14.0905C22.8339 12.5639 19.6296 13.3992 18.094 11.7521Z" fill="white"/>
|
||||
<path d="M23.5684 9.7093C23.5065 9.65186 23.4427 9.59535 23.3809 9.53885C23.4437 9.59629 23.5065 9.65657 23.5684 9.7093Z" fill="white"/>
|
||||
<path d="M25.7771 13.6581L25.7715 13.6496C25.6857 13.4895 25.5879 13.3362 25.479 13.191C25.1586 12.7549 24.699 12.4424 24.1769 12.3057C23.8293 12.2156 23.4739 12.1595 23.1156 12.1381C22.7528 12.1117 22.3825 12.0966 22.0085 12.0769C21.2585 12.0354 20.4907 11.9582 19.7407 11.7435C19.5532 11.6898 19.3658 11.6314 19.1848 11.5608C19.0911 11.5269 19.0039 11.4902 18.9111 11.4506C18.8183 11.4111 18.7236 11.3658 18.6298 11.3178C18.2772 11.1238 17.9505 10.8855 17.6577 10.6087C17.0802 10.0691 16.6265 9.45692 16.1718 8.86362C15.7432 8.26804 15.2831 7.6959 14.7937 7.14964C14.3108 6.61451 13.74 6.16664 13.1063 5.82555C12.45 5.49351 11.7372 5.28902 11.0054 5.22283C11.7646 5.14002 12.5326 5.23412 13.2497 5.49782C13.9733 5.78002 14.6348 6.20204 15.1968 6.73999C15.5637 7.08565 15.9082 7.45463 16.228 7.84465C18.6092 7.37378 20.5404 7.79192 22.0244 8.6037L22.0582 8.62065C22.5305 8.87801 22.9732 9.18689 23.3781 9.54168C23.4428 9.59819 23.5066 9.65469 23.5656 9.71214C23.8817 10.0039 24.1759 10.3187 24.4459 10.6539L24.4665 10.6812C25.3431 11.7962 25.7762 12.9555 25.7771 13.6581Z" fill="white"/>
|
||||
<path d="M25.776 13.657L25.7705 13.6458L25.776 13.657Z" fill="white"/>
|
||||
<path d="M11.1263 5.92056C11.7338 6.00909 12.3535 6.25206 12.7528 6.7154C13.1522 7.17874 13.2994 7.78146 13.4147 8.35498C13.5084 8.79949 13.5825 9.25247 13.7569 9.67343C13.8412 9.87873 13.965 10.0586 14.0681 10.2526C14.1534 10.4136 14.309 10.5587 14.369 10.731C14.3798 10.7558 14.3838 10.7829 14.3806 10.8097C14.3775 10.8365 14.3674 10.862 14.3512 10.8836C14.1394 11.119 13.5675 10.8572 13.3519 10.7517C12.9791 10.5661 12.6453 10.3104 12.3685 9.99834C11.5032 9.03304 11.056 7.64396 11.0832 6.37919C11.0892 6.22564 11.1036 6.07253 11.1263 5.92056Z" fill="white"/>
|
||||
<path d="M21.2865 16.8057C19.974 20.4851 25.927 22.9535 23.6967 26.6941C25.9851 25.7448 27.0716 22.8772 26.122 20.6019C25.2914 18.6045 22.8352 17.8765 21.2865 16.8057Z" fill="white"/>
|
||||
<path d="M13.4131 21.6174C13.7707 21.3466 14.1618 21.1234 14.5765 20.9535C14.996 20.7848 15.4313 20.6586 15.8758 20.5768C16.758 20.4082 17.6308 20.3668 18.3639 20.0701C18.7261 19.928 19.0594 19.7206 19.3473 19.458C19.626 19.1979 19.8393 18.8751 19.9698 18.5162C20.1034 18.1375 20.1577 17.7351 20.1291 17.3343C20.096 16.9038 19.999 16.4807 19.8413 16.079C20.1681 16.4204 20.4061 16.8375 20.5341 17.2933C20.6621 17.7492 20.6763 18.2297 20.5754 18.6923C20.4607 19.1821 20.2131 19.6303 19.8601 19.9872C19.5107 20.333 19.0893 20.5967 18.6264 20.7595C18.1846 20.9151 17.7238 21.0102 17.2567 21.042C16.8067 21.0797 16.3717 21.09 15.9443 21.1154C15.08 21.1519 14.2263 21.3211 13.4131 21.6174V21.6174Z" fill="white"/>
|
||||
<path d="M21.8102 28.0852C21.678 28.1907 21.5458 28.3028 21.4005 28.4007C21.2543 28.4975 21.101 28.5831 20.9421 28.6569C20.6111 28.8191 20.2472 28.9019 19.879 28.8989C18.8815 28.8801 18.1765 28.1342 17.7641 27.2913C17.4828 26.7169 17.2888 26.0962 16.955 25.5491C16.4778 24.7665 15.6613 24.1365 14.7051 24.2532C14.3151 24.3022 13.9495 24.4783 13.7329 24.8183C13.1629 25.7064 13.9814 26.9504 15.0248 26.7743C15.1135 26.7607 15.2003 26.7367 15.2835 26.7027C15.3664 26.6673 15.4437 26.62 15.5132 26.5624C15.6589 26.4405 15.7688 26.281 15.831 26.101C15.8995 25.9133 15.9148 25.7101 15.875 25.5142C15.8322 25.3095 15.7119 25.1295 15.5394 25.0123C15.74 25.1066 15.8962 25.2754 15.9754 25.4832C16.0574 25.6969 16.0785 25.9295 16.0363 26.1546C15.9953 26.3892 15.8924 26.6085 15.7382 26.7894C15.6563 26.8824 15.5616 26.9633 15.4569 27.0295C15.3532 27.095 15.2421 27.1478 15.126 27.1868C14.8905 27.2677 14.6399 27.2937 14.3929 27.2631C14.0461 27.2135 13.7191 27.071 13.4461 26.8506C13.1236 26.5944 12.8836 26.2592 12.592 25.971C12.2578 25.6187 11.8635 25.3294 11.4277 25.1168C11.1272 24.9842 10.8127 24.8863 10.4902 24.8249C10.328 24.791 10.164 24.7665 9.99991 24.7458C9.92491 24.7383 9.55742 24.6563 9.50586 24.7043C10.0129 24.2354 10.5657 23.819 11.1558 23.4612C11.7617 23.1 12.4123 22.8205 13.0908 22.6297C13.7942 22.4308 14.53 22.3741 15.2554 22.463C15.6288 22.508 15.9945 22.6031 16.3428 22.7455C16.7078 22.892 17.0447 23.101 17.3384 23.3633C17.6292 23.6384 17.8641 23.9675 18.0303 24.3324C18.1803 24.6739 18.2923 25.0311 18.364 25.3975C18.5778 26.4927 18.499 28.1907 19.9268 28.4412C20.0012 28.4557 20.0763 28.4664 20.1518 28.4732L20.3852 28.4789C20.5457 28.4674 20.7052 28.4444 20.8624 28.4101C21.1881 28.3332 21.5056 28.2243 21.8102 28.0852V28.0852Z" fill="white"/>
|
||||
<path d="M13.514 26.8916L13.4766 26.8622L13.514 26.8916Z" fill="white"/>
|
||||
<path d="M12.2325 11.4336C12.1805 11.6343 12.0897 11.8228 11.9653 11.9883C11.7338 12.2902 11.4273 12.5256 11.0766 12.6711C10.7612 12.8076 10.4272 12.8958 10.0857 12.9329C10.0116 12.9423 9.93478 12.9479 9.86072 12.9536C9.64444 12.9618 9.43648 13.0394 9.26728 13.175C9.09808 13.3106 8.97653 13.497 8.92043 13.707C8.89474 13.8115 8.87564 13.9175 8.86324 14.0244C8.82949 14.3012 8.82387 14.5894 8.79387 14.9378C8.72041 15.5308 8.54967 16.1074 8.28857 16.6443C7.94921 17.3619 7.5686 17.9401 7.65672 18.767C7.71484 19.3038 7.98858 19.6635 8.35232 20.0346C9.00855 20.7089 10.4776 21.0102 10.1495 22.6715C9.95165 23.6631 8.31482 24.7038 6.01426 25.0673C6.24301 25.0324 5.72083 24.1481 5.68802 24.0916C5.44147 23.7036 5.17148 23.3382 4.97555 22.9173C4.59118 22.1008 4.41306 21.1562 4.57056 20.2615C4.73649 19.3198 5.43022 18.5984 6.00677 17.8789C6.693 17.0229 7.41297 15.9013 7.57234 14.79C7.60984 14.5207 7.63609 14.1835 7.69609 13.8482C7.75329 13.4768 7.86912 13.1169 8.0392 12.7822C8.1553 12.5626 8.30815 12.3648 8.49106 12.1974C8.58643 12.1085 8.64935 11.99 8.66975 11.8609C8.69014 11.7318 8.66684 11.5996 8.60356 11.4854L4.93617 4.85926L10.2038 11.3893C10.2638 11.465 10.3396 11.5265 10.4259 11.5694C10.5122 11.6123 10.6069 11.6357 10.7031 11.6377C10.7994 11.6398 10.8949 11.6206 10.983 11.5814C11.071 11.5422 11.1494 11.484 11.2126 11.411C11.2793 11.3328 11.3171 11.2339 11.3197 11.131C11.3222 11.0281 11.2894 10.9274 11.2266 10.846C10.8826 10.4043 10.5188 9.95412 10.1663 9.5115L8.84075 7.86344L6.1802 4.57297L2.55225 0L6.52144 4.27821L9.35354 7.42364L10.7663 9.00013C11.235 9.53034 11.7038 10.0436 12.1725 10.6011L12.2494 10.6953L12.2663 10.8412C12.2891 11.0392 12.2777 11.2396 12.2325 11.4336Z" fill="white"/>
|
||||
<path d="M13.447 26.8463C13.1925 26.6488 12.966 26.4171 12.7739 26.1579C12.98 26.4047 13.205 26.6349 13.447 26.8463V26.8463Z" fill="white"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 6.9 KiB |
8
src/assets/svg/wordmark.svg
Normal file
|
After Width: | Height: | Size: 106 KiB |
8
src/assets/svg/wordmark_pink.svg
Normal file
|
After Width: | Height: | Size: 107 KiB |
8
src/assets/svg/wordmark_white.svg
Normal file
|
After Width: | Height: | Size: 107 KiB |
@@ -1,34 +1,35 @@
|
||||
import React from 'react'
|
||||
import styled from 'styled-components'
|
||||
import { useCopyClipboard } from '../../hooks'
|
||||
import useCopyClipboard from '../../hooks/useCopyClipboard'
|
||||
|
||||
import { Link } from '../../theme'
|
||||
import { LinkStyledButton } from '../../theme'
|
||||
import { CheckCircle, Copy } from 'react-feather'
|
||||
|
||||
const CopyIcon = styled(Link)`
|
||||
color: ${({ theme }) => theme.silverGray};
|
||||
const CopyIcon = styled(LinkStyledButton)`
|
||||
color: ${({ theme }) => theme.text3};
|
||||
flex-shrink: 0;
|
||||
margin-right: 1rem;
|
||||
margin-left: 0.5rem;
|
||||
display: flex;
|
||||
text-decoration: none;
|
||||
font-size: 0.825rem;
|
||||
:hover,
|
||||
:active,
|
||||
:focus {
|
||||
text-decoration: none;
|
||||
color: ${({ theme }) => theme.doveGray};
|
||||
color: ${({ theme }) => theme.text2};
|
||||
}
|
||||
`
|
||||
const TransactionStatusText = styled.span`
|
||||
margin-left: 0.25rem;
|
||||
font-size: 0.825rem;
|
||||
${({ theme }) => theme.flexRowNoWrap};
|
||||
align-items: center;
|
||||
`
|
||||
|
||||
export default function CopyHelper({ toCopy }) {
|
||||
export default function CopyHelper(props: { toCopy: string; children?: React.ReactNode }) {
|
||||
const [isCopied, setCopied] = useCopyClipboard()
|
||||
|
||||
return (
|
||||
<CopyIcon onClick={() => setCopied(toCopy)}>
|
||||
<CopyIcon onClick={() => setCopied(props.toCopy)}>
|
||||
{isCopied ? (
|
||||
<TransactionStatusText>
|
||||
<CheckCircle size={'16'} />
|
||||
@@ -39,6 +40,7 @@ export default function CopyHelper({ toCopy }) {
|
||||
<Copy size={'16'} />
|
||||
</TransactionStatusText>
|
||||
)}
|
||||
{isCopied ? '' : props.children}
|
||||
</CopyIcon>
|
||||
)
|
||||
}
|
||||
@@ -1,107 +0,0 @@
|
||||
import React from 'react'
|
||||
import styled, { keyframes } from 'styled-components'
|
||||
import { Check } from 'react-feather'
|
||||
|
||||
import { useWeb3React } from '../../hooks'
|
||||
import { getEtherscanLink } from '../../utils'
|
||||
import { Link, Spinner } from '../../theme'
|
||||
import Copy from './Copy'
|
||||
import Circle from '../../assets/images/circle.svg'
|
||||
|
||||
import { transparentize } from 'polished'
|
||||
|
||||
const TransactionStatusWrapper = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-width: 12px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
`
|
||||
|
||||
const TransactionWrapper = styled.div`
|
||||
${({ theme }) => theme.flexRowNoWrap}
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
margin-top: 0.75rem;
|
||||
a {
|
||||
/* flex: 1 1 auto; */
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
min-width: 0;
|
||||
max-width: 250px;
|
||||
}
|
||||
`
|
||||
|
||||
const TransactionStatusText = styled.span`
|
||||
margin-left: 0.5rem;
|
||||
`
|
||||
|
||||
const rotate = keyframes`
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
`
|
||||
|
||||
const TransactionState = styled.div`
|
||||
display: flex;
|
||||
background-color: ${({ pending, theme }) =>
|
||||
pending ? transparentize(0.95, theme.royalBlue) : transparentize(0.95, theme.connectedGreen)};
|
||||
border-radius: 1.5rem;
|
||||
padding: 0.5rem 0.75rem;
|
||||
font-weight: 500;
|
||||
font-size: 0.75rem;
|
||||
border: 1px solid;
|
||||
border-color: ${({ pending, theme }) =>
|
||||
pending ? transparentize(0.75, theme.royalBlue) : transparentize(0.75, theme.connectedGreen)};
|
||||
|
||||
#pending {
|
||||
animation: 2s ${rotate} linear infinite;
|
||||
}
|
||||
|
||||
:hover {
|
||||
border-color: ${({ pending, theme }) =>
|
||||
pending ? transparentize(0, theme.royalBlue) : transparentize(0, theme.connectedGreen)};
|
||||
}
|
||||
`
|
||||
const ButtonWrapper = styled.div`
|
||||
a {
|
||||
color: ${({ pending, theme }) => (pending ? theme.royalBlue : theme.connectedGreen)};
|
||||
}
|
||||
`
|
||||
|
||||
export default function Transaction({ hash, pending }) {
|
||||
const { chainId } = useWeb3React()
|
||||
|
||||
return (
|
||||
<TransactionWrapper key={hash}>
|
||||
<TransactionStatusWrapper>
|
||||
<Link href={getEtherscanLink(chainId, hash, 'transaction')}>{hash} ↗ </Link>
|
||||
<Copy toCopy={hash} />
|
||||
</TransactionStatusWrapper>
|
||||
{pending ? (
|
||||
<ButtonWrapper pending={pending}>
|
||||
<Link href={getEtherscanLink(chainId, hash, 'transaction')}>
|
||||
<TransactionState pending={pending}>
|
||||
<Spinner src={Circle} id="pending" />
|
||||
<TransactionStatusText>Pending</TransactionStatusText>
|
||||
</TransactionState>
|
||||
</Link>
|
||||
</ButtonWrapper>
|
||||
) : (
|
||||
<ButtonWrapper pending={pending}>
|
||||
<Link href={getEtherscanLink(chainId, hash, 'transaction')}>
|
||||
<TransactionState pending={pending}>
|
||||
<Check size="16" />
|
||||
<TransactionStatusText>Confirmed</TransactionStatusText>
|
||||
</TransactionState>
|
||||
</Link>
|
||||
</ButtonWrapper>
|
||||
)}
|
||||
</TransactionWrapper>
|
||||
)
|
||||
}
|
||||
62
src/components/AccountDetails/Transaction.tsx
Normal file
@@ -0,0 +1,62 @@
|
||||
import React from 'react'
|
||||
import styled from 'styled-components'
|
||||
import { CheckCircle, Triangle, ExternalLink as LinkIcon } from 'react-feather'
|
||||
|
||||
import { useActiveWeb3React } from '../../hooks'
|
||||
import { getEtherscanLink } from '../../utils'
|
||||
import { ExternalLink } from '../../theme'
|
||||
import { useAllTransactions } from '../../state/transactions/hooks'
|
||||
import { RowFixed } from '../Row'
|
||||
import Loader from '../Loader'
|
||||
|
||||
const TransactionWrapper = styled.div``
|
||||
|
||||
const TransactionStatusText = styled.div`
|
||||
margin-right: 0.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
`
|
||||
|
||||
const TransactionState = styled(ExternalLink)<{ pending: boolean; success?: boolean }>`
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
text-decoration: none !important;
|
||||
border-radius: 0.5rem;
|
||||
padding: 0.25rem 0rem;
|
||||
font-weight: 500;
|
||||
font-size: 0.825rem;
|
||||
color: ${({ theme }) => theme.primary1};
|
||||
`
|
||||
|
||||
const IconWrapper = styled.div<{ pending: boolean; success?: boolean }>`
|
||||
color: ${({ pending, success, theme }) => (pending ? theme.primary1 : success ? theme.green1 : theme.red1)};
|
||||
`
|
||||
|
||||
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')
|
||||
|
||||
return (
|
||||
<TransactionWrapper>
|
||||
<TransactionState href={getEtherscanLink(chainId, hash, 'transaction')} pending={pending} success={success}>
|
||||
<RowFixed>
|
||||
<TransactionStatusText>{summary ? summary : hash}</TransactionStatusText>
|
||||
<LinkIcon size={16} />
|
||||
</RowFixed>
|
||||
<IconWrapper pending={pending} success={success}>
|
||||
{pending ? <Loader /> : success ? <CheckCircle size="16" /> : <Triangle size="16" />}
|
||||
</IconWrapper>
|
||||
</TransactionState>
|
||||
</TransactionWrapper>
|
||||
)
|
||||
}
|
||||
@@ -1,377 +0,0 @@
|
||||
import React from 'react'
|
||||
import styled from 'styled-components'
|
||||
import { useWeb3React } from '../../hooks'
|
||||
import { isMobile } from 'react-device-detect'
|
||||
import Copy from './Copy'
|
||||
import Transaction from './Transaction'
|
||||
import { SUPPORTED_WALLETS } from '../../constants'
|
||||
import { ReactComponent as Close } from '../../assets/images/x.svg'
|
||||
import { getEtherscanLink } from '../../utils'
|
||||
import { injected, walletconnect, walletlink, fortmatic, portis } from '../../connectors'
|
||||
import CoinbaseWalletIcon from '../../assets/images/coinbaseWalletIcon.svg'
|
||||
import WalletConnectIcon from '../../assets/images/walletConnectIcon.svg'
|
||||
import FortmaticIcon from '../../assets/images/fortmaticIcon.png'
|
||||
import PortisIcon from '../../assets/images/portisIcon.png'
|
||||
import Identicon from '../Identicon'
|
||||
|
||||
import { Link } from '../../theme'
|
||||
|
||||
const OptionButton = styled.div`
|
||||
${({ theme }) => theme.flexColumnNoWrap}
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border-radius: 20px;
|
||||
border: 1px solid ${({ theme }) => theme.royalBlue};
|
||||
color: ${({ theme }) => theme.royalBlue};
|
||||
padding: 8px 24px;
|
||||
|
||||
&:hover {
|
||||
border: 1px solid ${({ theme }) => theme.malibuBlue};
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
${({ theme }) => theme.mediaWidth.upToMedium`
|
||||
font-size: 12px;
|
||||
`};
|
||||
`
|
||||
|
||||
const HeaderRow = styled.div`
|
||||
${({ theme }) => theme.flexRowNoWrap};
|
||||
padding: 1.5rem 1.5rem;
|
||||
font-weight: 500;
|
||||
color: ${props => (props.color === 'blue' ? ({ theme }) => theme.royalBlue : 'inherit')};
|
||||
${({ theme }) => theme.mediaWidth.upToMedium`
|
||||
padding: 1rem;
|
||||
`};
|
||||
`
|
||||
|
||||
const UpperSection = styled.div`
|
||||
position: relative;
|
||||
background-color: ${({ theme }) => theme.concreteGray};
|
||||
|
||||
h5 {
|
||||
margin: 0;
|
||||
margin-bottom: 0.5rem;
|
||||
font-size: 1rem;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
h5:last-child {
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
|
||||
h4 {
|
||||
margin-top: 0;
|
||||
font-weight: 500;
|
||||
}
|
||||
`
|
||||
|
||||
const InfoCard = styled.div`
|
||||
padding: 1rem;
|
||||
border: 1px solid ${({ theme }) => theme.placeholderGray};
|
||||
border-radius: 20px;
|
||||
`
|
||||
|
||||
const AccountGroupingRow = styled.div`
|
||||
${({ theme }) => theme.flexRowNoWrap};
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-weight: 500;
|
||||
color: ${({ theme }) => theme.textColor};
|
||||
|
||||
div {
|
||||
${({ theme }) => theme.flexRowNoWrap}
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
&:first-of-type {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
`
|
||||
|
||||
const AccountSection = styled.div`
|
||||
background-color: ${({ theme }) => theme.concreteGray};
|
||||
padding: 0rem 1.5rem;
|
||||
${({ theme }) => theme.mediaWidth.upToMedium`padding: 0rem 1rem 1rem 1rem;`};
|
||||
`
|
||||
|
||||
const YourAccount = styled.div`
|
||||
h5 {
|
||||
margin: 0 0 1rem 0;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
h4 {
|
||||
margin: 0;
|
||||
font-weight: 500;
|
||||
}
|
||||
`
|
||||
|
||||
const GreenCircle = styled.div`
|
||||
${({ theme }) => theme.flexRowNoWrap}
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
&:first-child {
|
||||
height: 8px;
|
||||
width: 8px;
|
||||
margin-left: 12px;
|
||||
margin-right: 2px;
|
||||
background-color: ${({ theme }) => theme.connectedGreen};
|
||||
border-radius: 50%;
|
||||
}
|
||||
`
|
||||
|
||||
const CircleWrapper = styled.div`
|
||||
color: ${({ theme }) => theme.connectedGreen};
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
`
|
||||
|
||||
const LowerSection = styled.div`
|
||||
${({ theme }) => theme.flexColumnNoWrap}
|
||||
padding: 2rem;
|
||||
flex-grow: 1;
|
||||
overflow: auto;
|
||||
|
||||
h5 {
|
||||
margin: 0;
|
||||
font-weight: 400;
|
||||
color: ${({ theme }) => theme.doveGray};
|
||||
}
|
||||
`
|
||||
|
||||
const AccountControl = styled.div`
|
||||
${({ theme }) => theme.flexRowNoWrap};
|
||||
align-items: center;
|
||||
min-width: 0;
|
||||
|
||||
font-weight: ${({ hasENS, isENS }) => (hasENS ? (isENS ? 500 : 400) : 500)};
|
||||
font-size: ${({ hasENS, isENS }) => (hasENS ? (isENS ? '1rem' : '0.8rem') : '1rem')};
|
||||
|
||||
a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
a {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
`
|
||||
|
||||
const ConnectButtonRow = styled.div`
|
||||
${({ theme }) => theme.flexRowNoWrap}
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin: 30px;
|
||||
`
|
||||
|
||||
const StyledLink = styled(Link)`
|
||||
color: ${({ hasENS, isENS, theme }) => (hasENS ? (isENS ? theme.royalBlue : theme.doveGray) : theme.royalBlue)};
|
||||
`
|
||||
|
||||
const CloseIcon = styled.div`
|
||||
position: absolute;
|
||||
right: 1rem;
|
||||
top: 14px;
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
opacity: 0.6;
|
||||
}
|
||||
`
|
||||
|
||||
const CloseColor = styled(Close)`
|
||||
path {
|
||||
stroke: ${({ theme }) => theme.chaliceGray};
|
||||
}
|
||||
`
|
||||
|
||||
const WalletName = styled.div`
|
||||
padding-left: 0.5rem;
|
||||
width: initial;
|
||||
`
|
||||
|
||||
const IconWrapper = styled.div`
|
||||
${({ theme }) => theme.flexColumnNoWrap};
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
& > img,
|
||||
span {
|
||||
height: ${({ size }) => (size ? size + 'px' : '32px')};
|
||||
width: ${({ size }) => (size ? size + 'px' : '32px')};
|
||||
}
|
||||
${({ theme }) => theme.mediaWidth.upToMedium`
|
||||
align-items: flex-end;
|
||||
`};
|
||||
`
|
||||
|
||||
const TransactionListWrapper = styled.div`
|
||||
${({ theme }) => theme.flexColumnNoWrap};
|
||||
`
|
||||
|
||||
const WalletAction = styled.div`
|
||||
color: ${({ theme }) => theme.chaliceGray};
|
||||
margin-left: 16px;
|
||||
font-weight: 400;
|
||||
:hover {
|
||||
cursor: pointer;
|
||||
text-decoration: underline;
|
||||
}
|
||||
`
|
||||
|
||||
const MainWalletAction = styled(WalletAction)`
|
||||
color: ${({ theme }) => theme.royalBlue};
|
||||
`
|
||||
|
||||
function renderTransactions(transactions, pending) {
|
||||
return (
|
||||
<TransactionListWrapper>
|
||||
{transactions.map((hash, i) => {
|
||||
return <Transaction key={i} hash={hash} pending={pending} />
|
||||
})}
|
||||
</TransactionListWrapper>
|
||||
)
|
||||
}
|
||||
|
||||
export default function AccountDetails({
|
||||
toggleWalletModal,
|
||||
pendingTransactions,
|
||||
confirmedTransactions,
|
||||
ENSName,
|
||||
openOptions
|
||||
}) {
|
||||
const { chainId, account, connector } = useWeb3React()
|
||||
|
||||
function formatConnectorName() {
|
||||
const isMetaMask = window.ethereum && window.ethereum.isMetaMask ? true : false
|
||||
const name = Object.keys(SUPPORTED_WALLETS)
|
||||
.filter(
|
||||
k =>
|
||||
SUPPORTED_WALLETS[k].connector === connector && (connector !== injected || isMetaMask === (k === 'METAMASK'))
|
||||
)
|
||||
.map(k => SUPPORTED_WALLETS[k].name)[0]
|
||||
return <WalletName>{name}</WalletName>
|
||||
}
|
||||
|
||||
function getStatusIcon() {
|
||||
if (connector === injected) {
|
||||
return (
|
||||
<IconWrapper size={16}>
|
||||
<Identicon /> {formatConnectorName()}
|
||||
</IconWrapper>
|
||||
)
|
||||
} else if (connector === walletconnect) {
|
||||
return (
|
||||
<IconWrapper size={16}>
|
||||
<img src={WalletConnectIcon} alt={''} /> {formatConnectorName()}
|
||||
</IconWrapper>
|
||||
)
|
||||
} else if (connector === walletlink) {
|
||||
return (
|
||||
<IconWrapper size={16}>
|
||||
<img src={CoinbaseWalletIcon} alt={''} /> {formatConnectorName()}
|
||||
</IconWrapper>
|
||||
)
|
||||
} else if (connector === fortmatic) {
|
||||
return (
|
||||
<IconWrapper size={16}>
|
||||
<img src={FortmaticIcon} alt={''} /> {formatConnectorName()}
|
||||
</IconWrapper>
|
||||
)
|
||||
} else if (connector === portis) {
|
||||
return (
|
||||
<>
|
||||
<IconWrapper size={16}>
|
||||
<img src={PortisIcon} alt={''} /> {formatConnectorName()}
|
||||
<MainWalletAction
|
||||
onClick={() => {
|
||||
portis.portis.showPortis()
|
||||
}}
|
||||
>
|
||||
Show Portis
|
||||
</MainWalletAction>
|
||||
</IconWrapper>
|
||||
</>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<UpperSection>
|
||||
<CloseIcon onClick={toggleWalletModal}>
|
||||
<CloseColor alt={'close icon'} />
|
||||
</CloseIcon>
|
||||
<HeaderRow>Account</HeaderRow>
|
||||
<AccountSection>
|
||||
<YourAccount>
|
||||
<InfoCard>
|
||||
<AccountGroupingRow>
|
||||
{getStatusIcon()}
|
||||
<div>
|
||||
{connector !== injected && connector !== walletlink && (
|
||||
<WalletAction
|
||||
onClick={() => {
|
||||
connector.close()
|
||||
}}
|
||||
>
|
||||
Disconnect
|
||||
</WalletAction>
|
||||
)}
|
||||
<CircleWrapper>
|
||||
<GreenCircle>
|
||||
<div />
|
||||
</GreenCircle>
|
||||
</CircleWrapper>
|
||||
</div>
|
||||
</AccountGroupingRow>
|
||||
<AccountGroupingRow>
|
||||
{ENSName ? (
|
||||
<AccountControl hasENS={!!ENSName} isENS={true}>
|
||||
<StyledLink hasENS={!!ENSName} isENS={true} href={getEtherscanLink(chainId, ENSName, 'address')}>
|
||||
{ENSName} ↗{' '}
|
||||
</StyledLink>
|
||||
<Copy toCopy={ENSName} />
|
||||
</AccountControl>
|
||||
) : (
|
||||
<AccountControl hasENS={!!ENSName} isENS={false}>
|
||||
<StyledLink hasENS={!!ENSName} isENS={false} href={getEtherscanLink(chainId, account, 'address')}>
|
||||
{account} ↗{' '}
|
||||
</StyledLink>
|
||||
<Copy toCopy={account} />
|
||||
</AccountControl>
|
||||
)}
|
||||
</AccountGroupingRow>
|
||||
</InfoCard>
|
||||
</YourAccount>
|
||||
|
||||
{!(isMobile && (window.web3 || window.ethereum)) && (
|
||||
<ConnectButtonRow>
|
||||
<OptionButton
|
||||
onClick={() => {
|
||||
openOptions()
|
||||
}}
|
||||
>
|
||||
Connect to a different wallet
|
||||
</OptionButton>
|
||||
</ConnectButtonRow>
|
||||
)}
|
||||
</AccountSection>
|
||||
</UpperSection>
|
||||
{!!pendingTransactions.length || !!confirmedTransactions.length ? (
|
||||
<LowerSection>
|
||||
<h5>Recent Transactions</h5>
|
||||
{renderTransactions(pendingTransactions, true)}
|
||||
{renderTransactions(confirmedTransactions, false)}
|
||||
</LowerSection>
|
||||
) : (
|
||||
<LowerSection>
|
||||
<h5>Your transactions will appear here...</h5>
|
||||
</LowerSection>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
407
src/components/AccountDetails/index.tsx
Normal file
@@ -0,0 +1,407 @@
|
||||
import React, { useCallback, useContext } from 'react'
|
||||
import { useDispatch } from 'react-redux'
|
||||
import styled, { ThemeContext } from 'styled-components'
|
||||
import { useActiveWeb3React } from '../../hooks'
|
||||
import { AppDispatch } from '../../state'
|
||||
import { clearAllTransactions } from '../../state/transactions/actions'
|
||||
import { shortenAddress } from '../../utils'
|
||||
import { AutoRow } from '../Row'
|
||||
import Copy from './Copy'
|
||||
import Transaction from './Transaction'
|
||||
|
||||
import { SUPPORTED_WALLETS } from '../../constants'
|
||||
import { ReactComponent as Close } from '../../assets/images/x.svg'
|
||||
import { getEtherscanLink } from '../../utils'
|
||||
import { injected, walletconnect, walletlink, fortmatic, portis } from '../../connectors'
|
||||
import CoinbaseWalletIcon from '../../assets/images/coinbaseWalletIcon.svg'
|
||||
import WalletConnectIcon from '../../assets/images/walletConnectIcon.svg'
|
||||
import FortmaticIcon from '../../assets/images/fortmaticIcon.png'
|
||||
import PortisIcon from '../../assets/images/portisIcon.png'
|
||||
import Identicon from '../Identicon'
|
||||
import { ButtonSecondary } from '../Button'
|
||||
import { ExternalLink as LinkIcon } from 'react-feather'
|
||||
import { ExternalLink, LinkStyledButton, TYPE } from '../../theme'
|
||||
|
||||
const HeaderRow = styled.div`
|
||||
${({ theme }) => theme.flexRowNoWrap};
|
||||
padding: 1rem 1rem;
|
||||
font-weight: 500;
|
||||
color: ${props => (props.color === 'blue' ? ({ theme }) => theme.primary1 : 'inherit')};
|
||||
${({ theme }) => theme.mediaWidth.upToMedium`
|
||||
padding: 1rem;
|
||||
`};
|
||||
`
|
||||
|
||||
const UpperSection = styled.div`
|
||||
position: relative;
|
||||
|
||||
h5 {
|
||||
margin: 0;
|
||||
margin-bottom: 0.5rem;
|
||||
font-size: 1rem;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
h5:last-child {
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
|
||||
h4 {
|
||||
margin-top: 0;
|
||||
font-weight: 500;
|
||||
}
|
||||
`
|
||||
|
||||
const InfoCard = styled.div`
|
||||
padding: 1rem;
|
||||
border: 1px solid ${({ theme }) => theme.bg3};
|
||||
border-radius: 20px;
|
||||
position: relative;
|
||||
display: grid;
|
||||
grid-row-gap: 12px;
|
||||
margin-bottom: 20px;
|
||||
`
|
||||
|
||||
const AccountGroupingRow = styled.div`
|
||||
${({ theme }) => theme.flexRowNoWrap};
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-weight: 400;
|
||||
color: ${({ theme }) => theme.text1};
|
||||
|
||||
div {
|
||||
${({ theme }) => theme.flexRowNoWrap}
|
||||
align-items: center;
|
||||
}
|
||||
`
|
||||
|
||||
const AccountSection = styled.div`
|
||||
background-color: ${({ theme }) => theme.bg1};
|
||||
padding: 0rem 1rem;
|
||||
${({ theme }) => theme.mediaWidth.upToMedium`padding: 0rem 1rem 1.5rem 1rem;`};
|
||||
`
|
||||
|
||||
const YourAccount = styled.div`
|
||||
h5 {
|
||||
margin: 0 0 1rem 0;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
h4 {
|
||||
margin: 0;
|
||||
font-weight: 500;
|
||||
}
|
||||
`
|
||||
|
||||
const LowerSection = styled.div`
|
||||
${({ theme }) => theme.flexColumnNoWrap}
|
||||
padding: 1.5rem;
|
||||
flex-grow: 1;
|
||||
overflow: auto;
|
||||
background-color: ${({ theme }) => theme.bg2};
|
||||
border-bottom-left-radius: 25px;
|
||||
border-bottom-right-radius: 20px;
|
||||
|
||||
h5 {
|
||||
margin: 0;
|
||||
font-weight: 400;
|
||||
color: ${({ theme }) => theme.text3};
|
||||
}
|
||||
`
|
||||
|
||||
const AccountControl = styled.div`
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
min-width: 0;
|
||||
width: 100%;
|
||||
|
||||
font-weight: 500;
|
||||
font-size: 1.25rem;
|
||||
|
||||
a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
p {
|
||||
min-width: 0;
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
`
|
||||
|
||||
const AddressLink = styled(ExternalLink)<{ hasENS: boolean; isENS: boolean }>`
|
||||
font-size: 0.825rem;
|
||||
color: ${({ theme }) => theme.text3};
|
||||
margin-left: 1rem;
|
||||
font-size: 0.825rem;
|
||||
display: flex;
|
||||
:hover {
|
||||
color: ${({ theme }) => theme.text2};
|
||||
}
|
||||
`
|
||||
|
||||
const CloseIcon = styled.div`
|
||||
position: absolute;
|
||||
right: 1rem;
|
||||
top: 14px;
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
opacity: 0.6;
|
||||
}
|
||||
`
|
||||
|
||||
const CloseColor = styled(Close)`
|
||||
path {
|
||||
stroke: ${({ theme }) => theme.text4};
|
||||
}
|
||||
`
|
||||
|
||||
const WalletName = styled.div`
|
||||
width: initial;
|
||||
font-size: 0.825rem;
|
||||
font-weight: 500;
|
||||
color: ${({ theme }) => theme.text3};
|
||||
`
|
||||
|
||||
const IconWrapper = styled.div<{ size?: number }>`
|
||||
${({ theme }) => theme.flexColumnNoWrap};
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 8px;
|
||||
& > img,
|
||||
span {
|
||||
height: ${({ size }) => (size ? size + 'px' : '32px')};
|
||||
width: ${({ size }) => (size ? size + 'px' : '32px')};
|
||||
}
|
||||
${({ theme }) => theme.mediaWidth.upToMedium`
|
||||
align-items: flex-end;
|
||||
`};
|
||||
`
|
||||
|
||||
const TransactionListWrapper = styled.div`
|
||||
${({ theme }) => theme.flexColumnNoWrap};
|
||||
`
|
||||
|
||||
const WalletAction = styled(ButtonSecondary)`
|
||||
width: fit-content;
|
||||
font-weight: 400;
|
||||
margin-left: 8px;
|
||||
font-size: 0.825rem;
|
||||
padding: 4px 6px;
|
||||
:hover {
|
||||
cursor: pointer;
|
||||
text-decoration: underline;
|
||||
}
|
||||
`
|
||||
|
||||
const MainWalletAction = styled(WalletAction)`
|
||||
color: ${({ theme }) => theme.primary1};
|
||||
`
|
||||
|
||||
function renderTransactions(transactions) {
|
||||
return (
|
||||
<TransactionListWrapper>
|
||||
{transactions.map((hash, i) => {
|
||||
return <Transaction key={i} hash={hash} />
|
||||
})}
|
||||
</TransactionListWrapper>
|
||||
)
|
||||
}
|
||||
|
||||
interface AccountDetailsProps {
|
||||
toggleWalletModal: () => void
|
||||
pendingTransactions: any[]
|
||||
confirmedTransactions: any[]
|
||||
ENSName?: string
|
||||
openOptions: () => void
|
||||
}
|
||||
|
||||
export default function AccountDetails({
|
||||
toggleWalletModal,
|
||||
pendingTransactions,
|
||||
confirmedTransactions,
|
||||
ENSName,
|
||||
openOptions
|
||||
}: AccountDetailsProps) {
|
||||
const { chainId, account, connector } = useActiveWeb3React()
|
||||
const theme = useContext(ThemeContext)
|
||||
const dispatch = useDispatch<AppDispatch>()
|
||||
|
||||
function formatConnectorName() {
|
||||
const { ethereum } = window
|
||||
const isMetaMask = !!(ethereum && ethereum.isMetaMask)
|
||||
const name = Object.keys(SUPPORTED_WALLETS)
|
||||
.filter(
|
||||
k =>
|
||||
SUPPORTED_WALLETS[k].connector === connector && (connector !== injected || isMetaMask === (k === 'METAMASK'))
|
||||
)
|
||||
.map(k => SUPPORTED_WALLETS[k].name)[0]
|
||||
return <WalletName>Connected with {name}</WalletName>
|
||||
}
|
||||
|
||||
function getStatusIcon() {
|
||||
if (connector === injected) {
|
||||
return (
|
||||
<IconWrapper size={16}>
|
||||
<Identicon />
|
||||
</IconWrapper>
|
||||
)
|
||||
} 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={''} />
|
||||
<MainWalletAction
|
||||
onClick={() => {
|
||||
portis.portis.showPortis()
|
||||
}}
|
||||
>
|
||||
Show Portis
|
||||
</MainWalletAction>
|
||||
</IconWrapper>
|
||||
</>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const clearAllTransactionsCallback = useCallback(
|
||||
(event: React.MouseEvent) => {
|
||||
event.preventDefault()
|
||||
dispatch(clearAllTransactions({ chainId }))
|
||||
},
|
||||
[dispatch, chainId]
|
||||
)
|
||||
|
||||
return (
|
||||
<>
|
||||
<UpperSection>
|
||||
<CloseIcon onClick={toggleWalletModal}>
|
||||
<CloseColor />
|
||||
</CloseIcon>
|
||||
<HeaderRow>Account</HeaderRow>
|
||||
<AccountSection>
|
||||
<YourAccount>
|
||||
<InfoCard>
|
||||
<AccountGroupingRow>
|
||||
{formatConnectorName()}
|
||||
<div>
|
||||
{connector !== injected && connector !== walletlink && (
|
||||
<WalletAction
|
||||
style={{ fontSize: '.825rem', fontWeight: 400, marginRight: '8px' }}
|
||||
onClick={() => {
|
||||
;(connector as any).close()
|
||||
}}
|
||||
>
|
||||
Disconnect
|
||||
</WalletAction>
|
||||
)}
|
||||
<WalletAction
|
||||
style={{ fontSize: '.825rem', fontWeight: 400 }}
|
||||
onClick={() => {
|
||||
openOptions()
|
||||
}}
|
||||
>
|
||||
Change
|
||||
</WalletAction>
|
||||
</div>
|
||||
</AccountGroupingRow>
|
||||
<AccountGroupingRow id="web3-account-identifier-row">
|
||||
<AccountControl>
|
||||
{ENSName ? (
|
||||
<>
|
||||
<div>
|
||||
{getStatusIcon()}
|
||||
<p> {ENSName}</p>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div>
|
||||
{getStatusIcon()}
|
||||
<p> {shortenAddress(account)}</p>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</AccountControl>
|
||||
</AccountGroupingRow>
|
||||
<AccountGroupingRow>
|
||||
{ENSName ? (
|
||||
<>
|
||||
<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>
|
||||
</div>
|
||||
</AccountControl>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<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>
|
||||
</div>
|
||||
</AccountControl>
|
||||
</>
|
||||
)}
|
||||
{/* {formatConnectorName()} */}
|
||||
</AccountGroupingRow>
|
||||
</InfoCard>
|
||||
</YourAccount>
|
||||
</AccountSection>
|
||||
</UpperSection>
|
||||
{!!pendingTransactions.length || !!confirmedTransactions.length ? (
|
||||
<LowerSection>
|
||||
<AutoRow mb={'1rem'} style={{ justifyContent: 'space-between' }}>
|
||||
<TYPE.body>Recent Transactions</TYPE.body>
|
||||
<LinkStyledButton onClick={clearAllTransactionsCallback}>(clear all)</LinkStyledButton>
|
||||
</AutoRow>
|
||||
{renderTransactions(pendingTransactions)}
|
||||
{renderTransactions(confirmedTransactions)}
|
||||
</LowerSection>
|
||||
) : (
|
||||
<LowerSection>
|
||||
<TYPE.body color={theme.text1}>Your transactions will appear here...</TYPE.body>
|
||||
</LowerSection>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -1,191 +0,0 @@
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import styled from 'styled-components'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { transparentize } from 'polished'
|
||||
|
||||
import { isAddress } from '../../utils'
|
||||
import { useWeb3React, useDebounce } from '../../hooks'
|
||||
|
||||
const InputPanel = styled.div`
|
||||
${({ theme }) => theme.flexColumnNoWrap}
|
||||
box-shadow: 0 4px 8px 0 ${({ theme }) => transparentize(0.95, theme.shadowColor)};
|
||||
position: relative;
|
||||
border-radius: 1.25rem;
|
||||
background-color: ${({ theme }) => theme.inputBackground};
|
||||
z-index: 1;
|
||||
`
|
||||
|
||||
const ContainerRow = styled.div`
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border-radius: 1.25rem;
|
||||
border: 1px solid ${({ error, theme }) => (error ? theme.salmonRed : theme.mercuryGray)};
|
||||
|
||||
background-color: ${({ theme }) => theme.inputBackground};
|
||||
`
|
||||
|
||||
const InputContainer = styled.div`
|
||||
flex: 1;
|
||||
`
|
||||
|
||||
const LabelRow = styled.div`
|
||||
${({ theme }) => theme.flexRowNoWrap}
|
||||
align-items: center;
|
||||
color: ${({ theme }) => theme.doveGray};
|
||||
font-size: 0.75rem;
|
||||
line-height: 1rem;
|
||||
padding: 0.75rem 1rem;
|
||||
`
|
||||
|
||||
const LabelContainer = styled.div`
|
||||
flex: 1 1 auto;
|
||||
width: 0;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
`
|
||||
|
||||
const InputRow = styled.div`
|
||||
${({ theme }) => theme.flexRowNoWrap}
|
||||
align-items: center;
|
||||
padding: 0.25rem 0.85rem 0.75rem;
|
||||
`
|
||||
|
||||
const Input = styled.input`
|
||||
font-size: 1rem;
|
||||
outline: none;
|
||||
border: none;
|
||||
flex: 1 1 auto;
|
||||
width: 0;
|
||||
background-color: ${({ theme }) => theme.inputBackground};
|
||||
|
||||
color: ${({ error, theme }) => (error ? theme.salmonRed : theme.royalBlue)};
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
::placeholder {
|
||||
color: ${({ theme }) => theme.placeholderGray};
|
||||
}
|
||||
`
|
||||
|
||||
export default function AddressInputPanel({ title, initialInput = '', onChange = () => {}, onError = () => {} }) {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const { library } = useWeb3React()
|
||||
|
||||
const [input, setInput] = useState(initialInput.address ? initialInput.address : '')
|
||||
|
||||
const debouncedInput = useDebounce(input, 150)
|
||||
|
||||
const [data, setData] = useState({ address: undefined, name: undefined })
|
||||
const [error, setError] = useState(false)
|
||||
|
||||
// keep data and errors in sync
|
||||
useEffect(() => {
|
||||
onChange({ address: data.address, name: data.name })
|
||||
}, [onChange, data.address, data.name])
|
||||
useEffect(() => {
|
||||
onError(error)
|
||||
}, [onError, error])
|
||||
|
||||
// run parser on debounced input
|
||||
useEffect(() => {
|
||||
let stale = false
|
||||
|
||||
if (isAddress(debouncedInput)) {
|
||||
try {
|
||||
library
|
||||
.lookupAddress(debouncedInput)
|
||||
.then(name => {
|
||||
if (!stale) {
|
||||
// if an ENS name exists, set it as the destination
|
||||
if (name) {
|
||||
setInput(name)
|
||||
} else {
|
||||
setData({ address: debouncedInput, name: '' })
|
||||
setError(null)
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
if (!stale) {
|
||||
setData({ address: debouncedInput, name: '' })
|
||||
setError(null)
|
||||
}
|
||||
})
|
||||
} catch {
|
||||
setData({ address: debouncedInput, name: '' })
|
||||
setError(null)
|
||||
}
|
||||
} else {
|
||||
if (debouncedInput !== '') {
|
||||
try {
|
||||
library
|
||||
.resolveName(debouncedInput)
|
||||
.then(address => {
|
||||
if (!stale) {
|
||||
// if the debounced input name resolves to an address
|
||||
if (address) {
|
||||
setData({ address: address, name: debouncedInput })
|
||||
setError(null)
|
||||
} else {
|
||||
setError(true)
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
if (!stale) {
|
||||
setError(true)
|
||||
}
|
||||
})
|
||||
} catch {
|
||||
setError(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return () => {
|
||||
stale = true
|
||||
}
|
||||
}, [debouncedInput, library, onChange, onError])
|
||||
|
||||
function onInput(event) {
|
||||
if (data.address !== undefined || data.name !== undefined) {
|
||||
setData({ address: undefined, name: undefined })
|
||||
}
|
||||
if (error !== undefined) {
|
||||
setError()
|
||||
}
|
||||
const input = event.target.value
|
||||
const checksummedInput = isAddress(input)
|
||||
setInput(checksummedInput || input)
|
||||
}
|
||||
|
||||
return (
|
||||
<InputPanel>
|
||||
<ContainerRow error={input !== '' && error}>
|
||||
<InputContainer>
|
||||
<LabelRow>
|
||||
<LabelContainer>
|
||||
<span>{title || t('recipientAddress')}</span>
|
||||
</LabelContainer>
|
||||
</LabelRow>
|
||||
<InputRow>
|
||||
<Input
|
||||
type="text"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
placeholder="0x1234..."
|
||||
error={input !== '' && error}
|
||||
onChange={onInput}
|
||||
value={input}
|
||||
/>
|
||||
</InputRow>
|
||||
</InputContainer>
|
||||
</ContainerRow>
|
||||
</InputPanel>
|
||||
)
|
||||
}
|
||||
187
src/components/AddressInputPanel/index.tsx
Normal file
@@ -0,0 +1,187 @@
|
||||
import React, { useState, useEffect, useContext } from 'react'
|
||||
import styled, { ThemeContext } from 'styled-components'
|
||||
import useDebounce from '../../hooks/useDebounce'
|
||||
|
||||
import { isAddress } from '../../utils'
|
||||
import { useActiveWeb3React } from '../../hooks'
|
||||
import { ExternalLink, TYPE } from '../../theme'
|
||||
import { AutoColumn } from '../Column'
|
||||
import { RowBetween } from '../Row'
|
||||
import { getEtherscanLink } from '../../utils'
|
||||
|
||||
const InputPanel = styled.div`
|
||||
${({ theme }) => theme.flexColumnNoWrap}
|
||||
position: relative;
|
||||
border-radius: 1.25rem;
|
||||
background-color: ${({ theme }) => theme.bg1};
|
||||
z-index: 1;
|
||||
width: 100%;
|
||||
`
|
||||
|
||||
const ContainerRow = styled.div<{ error: boolean }>`
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border-radius: 1.25rem;
|
||||
border: 1px solid ${({ error, theme }) => (error ? theme.red1 : theme.bg2)};
|
||||
background-color: ${({ theme }) => theme.bg1};
|
||||
`
|
||||
|
||||
const InputContainer = styled.div`
|
||||
flex: 1;
|
||||
padding: 1rem;
|
||||
`
|
||||
|
||||
const Input = styled.input<{ error?: boolean }>`
|
||||
font-size: 1.25rem;
|
||||
outline: none;
|
||||
border: none;
|
||||
flex: 1 1 auto;
|
||||
width: 0;
|
||||
background-color: ${({ theme }) => theme.bg1};
|
||||
color: ${({ error, theme }) => (error ? theme.red1 : theme.primary1)};
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
font-weight: 500;
|
||||
width: 100%;
|
||||
::placeholder {
|
||||
color: ${({ theme }) => theme.text4};
|
||||
}
|
||||
padding: 0px;
|
||||
-webkit-appearance: textfield;
|
||||
|
||||
::-webkit-search-decoration {
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
::-webkit-outer-spin-button,
|
||||
::-webkit-inner-spin-button {
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
::placeholder {
|
||||
color: ${({ theme }) => theme.text4};
|
||||
}
|
||||
`
|
||||
|
||||
export default function AddressInputPanel({
|
||||
initialInput = '',
|
||||
onChange,
|
||||
onError
|
||||
}: {
|
||||
initialInput?: string
|
||||
onChange: (val: { address: string; name?: string }) => void
|
||||
onError: (error: boolean, input: string) => void
|
||||
}) {
|
||||
const { chainId, library } = useActiveWeb3React()
|
||||
const theme = useContext(ThemeContext)
|
||||
|
||||
const [input, setInput] = useState(initialInput ? initialInput : '')
|
||||
const debouncedInput = useDebounce(input, 200)
|
||||
|
||||
const [data, setData] = useState<{ address: string; name: string }>({ address: undefined, name: undefined })
|
||||
const [error, setError] = useState<boolean>(false)
|
||||
|
||||
// keep data and errors in sync
|
||||
useEffect(() => {
|
||||
onChange({ address: data.address, name: data.name })
|
||||
}, [onChange, data.address, data.name])
|
||||
useEffect(() => {
|
||||
onError(error, input)
|
||||
}, [onError, error, input])
|
||||
|
||||
// run parser on debounced input
|
||||
useEffect(() => {
|
||||
let stale = false
|
||||
// if the input is an address, try to look up its name
|
||||
if (isAddress(debouncedInput)) {
|
||||
library
|
||||
.lookupAddress(debouncedInput)
|
||||
.then(name => {
|
||||
if (stale) return
|
||||
// if an ENS name exists, set it as the destination
|
||||
if (name) {
|
||||
setInput(name)
|
||||
} else {
|
||||
setData({ address: debouncedInput, name: '' })
|
||||
setError(null)
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
if (stale) return
|
||||
setData({ address: debouncedInput, name: '' })
|
||||
setError(null)
|
||||
})
|
||||
}
|
||||
// otherwise try to look up the address of the input, treated as an ENS name
|
||||
else {
|
||||
if (debouncedInput !== '') {
|
||||
library
|
||||
.resolveName(debouncedInput)
|
||||
.then(address => {
|
||||
if (stale) return
|
||||
// if the debounced input name resolves to an address
|
||||
if (address) {
|
||||
setData({ address: address, name: debouncedInput })
|
||||
setError(null)
|
||||
} else {
|
||||
setError(true)
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
if (stale) return
|
||||
setError(true)
|
||||
})
|
||||
} else if (debouncedInput === '') {
|
||||
setError(true)
|
||||
}
|
||||
}
|
||||
|
||||
return () => {
|
||||
stale = true
|
||||
}
|
||||
}, [debouncedInput, library])
|
||||
|
||||
function onInput(event) {
|
||||
setData({ address: undefined, name: undefined })
|
||||
setError(false)
|
||||
const input = event.target.value
|
||||
const checksummedInput = isAddress(input.replace(/\s/g, '')) // delete whitespace
|
||||
setInput(checksummedInput || input)
|
||||
}
|
||||
|
||||
return (
|
||||
<InputPanel>
|
||||
<ContainerRow error={input !== '' && error}>
|
||||
<InputContainer>
|
||||
<AutoColumn gap="md">
|
||||
<RowBetween>
|
||||
<TYPE.black color={theme.text2} fontWeight={500} fontSize={14}>
|
||||
Recipient
|
||||
</TYPE.black>
|
||||
{data.address && (
|
||||
<ExternalLink
|
||||
href={getEtherscanLink(chainId, data.name || data.address, 'address')}
|
||||
style={{ fontSize: '14px' }}
|
||||
>
|
||||
(View on Etherscan)
|
||||
</ExternalLink>
|
||||
)}
|
||||
</RowBetween>
|
||||
<Input
|
||||
type="text"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
placeholder="Wallet Address or ENS name"
|
||||
error={input !== '' && error}
|
||||
onChange={onInput}
|
||||
value={input}
|
||||
/>
|
||||
</AutoColumn>
|
||||
</InputContainer>
|
||||
</ContainerRow>
|
||||
</InputPanel>
|
||||
)
|
||||
}
|
||||
295
src/components/Button/index.tsx
Normal file
@@ -0,0 +1,295 @@
|
||||
import React from 'react'
|
||||
import styled from 'styled-components'
|
||||
import { darken, lighten } from 'polished'
|
||||
|
||||
import { RowBetween } from '../Row'
|
||||
import { ChevronDown } from 'react-feather'
|
||||
import { Button as RebassButton, ButtonProps } from 'rebass/styled-components'
|
||||
|
||||
const Base = styled(RebassButton)<{
|
||||
padding?: string
|
||||
width?: string
|
||||
borderRadius?: string
|
||||
altDisbaledStyle?: boolean
|
||||
}>`
|
||||
padding: ${({ padding }) => (padding ? padding : '18px')};
|
||||
width: ${({ width }) => (width ? width : '100%')};
|
||||
font-weight: 500;
|
||||
text-align: center;
|
||||
border-radius: 20px;
|
||||
border-radius: ${({ borderRadius }) => borderRadius && borderRadius};
|
||||
outline: none;
|
||||
border: 1px solid transparent;
|
||||
color: white;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
flex-wrap: nowrap;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
&:disabled {
|
||||
cursor: auto;
|
||||
}
|
||||
|
||||
> * {
|
||||
user-select: none;
|
||||
}
|
||||
`
|
||||
|
||||
export const ButtonPrimary = styled(Base)`
|
||||
background-color: ${({ theme }) => theme.primary1};
|
||||
color: white;
|
||||
&:focus {
|
||||
box-shadow: 0 0 0 1pt ${({ theme }) => darken(0.05, theme.primary1)};
|
||||
background-color: ${({ theme }) => darken(0.05, theme.primary1)};
|
||||
}
|
||||
&:hover {
|
||||
background-color: ${({ theme }) => darken(0.05, theme.primary1)};
|
||||
}
|
||||
&:active {
|
||||
box-shadow: 0 0 0 1pt ${({ theme }) => darken(0.1, theme.primary1)};
|
||||
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)}
|
||||
cursor: auto;
|
||||
box-shadow: none;
|
||||
border: 1px solid transparent;;
|
||||
outline: none;
|
||||
}
|
||||
`
|
||||
|
||||
export const ButtonLight = styled(Base)`
|
||||
background-color: ${({ theme }) => theme.primary5};
|
||||
color: ${({ theme }) => theme.primaryText1};
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
&:focus {
|
||||
box-shadow: 0 0 0 1pt ${({ theme, disabled }) => !disabled && darken(0.03, theme.primary5)};
|
||||
background-color: ${({ theme, disabled }) => !disabled && darken(0.03, theme.primary5)};
|
||||
}
|
||||
&:hover {
|
||||
background-color: ${({ theme, disabled }) => !disabled && darken(0.03, theme.primary5)};
|
||||
}
|
||||
&:active {
|
||||
box-shadow: 0 0 0 1pt ${({ theme, disabled }) => !disabled && darken(0.05, theme.primary5)};
|
||||
background-color: ${({ theme, disabled }) => !disabled && darken(0.05, theme.primary5)};
|
||||
}
|
||||
:disabled {
|
||||
opacity: 0.4;
|
||||
:hover {
|
||||
cursor: auto;
|
||||
background-color: ${({ theme }) => theme.primary5};
|
||||
box-shadow: none;
|
||||
border: 1px solid transparent;
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const ButtonGray = styled(Base)`
|
||||
background-color: ${({ theme }) => theme.bg3};
|
||||
color: ${({ theme }) => theme.text2};
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
&:focus {
|
||||
box-shadow: 0 0 0 1pt ${({ theme, disabled }) => !disabled && darken(0.05, theme.bg2)};
|
||||
background-color: ${({ theme, disabled }) => !disabled && darken(0.05, theme.bg2)};
|
||||
}
|
||||
&:hover {
|
||||
background-color: ${({ theme, disabled }) => !disabled && darken(0.05, theme.bg2)};
|
||||
}
|
||||
&:active {
|
||||
box-shadow: 0 0 0 1pt ${({ theme, disabled }) => !disabled && darken(0.1, theme.bg2)};
|
||||
background-color: ${({ theme, disabled }) => !disabled && darken(0.1, theme.bg2)};
|
||||
}
|
||||
`
|
||||
|
||||
export const ButtonSecondary = styled(Base)`
|
||||
background-color: ${({ theme }) => theme.primary5};
|
||||
color: ${({ theme }) => theme.primaryText1};
|
||||
font-size: 16px;
|
||||
border-radius: 8px;
|
||||
padding: ${({ padding }) => (padding ? padding : '10px')};
|
||||
|
||||
&:focus {
|
||||
box-shadow: 0 0 0 1pt ${({ theme }) => theme.primary4};
|
||||
background-color: ${({ theme }) => theme.primary4};
|
||||
}
|
||||
&:hover {
|
||||
background-color: ${({ theme }) => theme.primary4};
|
||||
}
|
||||
&:active {
|
||||
box-shadow: 0 0 0 1pt ${({ theme }) => theme.primary4};
|
||||
background-color: ${({ theme }) => theme.primary4};
|
||||
}
|
||||
&:disabled {
|
||||
background-color: ${({ theme }) => theme.primary5};
|
||||
opacity: 50%;
|
||||
cursor: auto;
|
||||
}
|
||||
`
|
||||
|
||||
export const ButtonPink = styled(Base)`
|
||||
background-color: ${({ theme }) => theme.primary1};
|
||||
color: white;
|
||||
|
||||
&:focus {
|
||||
box-shadow: 0 0 0 1pt ${({ theme }) => darken(0.05, theme.primary1)};
|
||||
background-color: ${({ theme }) => darken(0.05, theme.primary1)};
|
||||
}
|
||||
&:hover {
|
||||
background-color: ${({ theme }) => darken(0.05, theme.primary1)};
|
||||
}
|
||||
&:active {
|
||||
box-shadow: 0 0 0 1pt ${({ theme }) => darken(0.1, theme.primary1)};
|
||||
background-color: ${({ theme }) => darken(0.1, theme.primary1)};
|
||||
}
|
||||
&:disabled {
|
||||
background-color: ${({ theme }) => theme.primary1};
|
||||
opacity: 50%;
|
||||
cursor: auto;
|
||||
}
|
||||
`
|
||||
|
||||
export const ButtonOutlined = styled(Base)`
|
||||
border: 1px solid ${({ theme }) => theme.bg2};
|
||||
background-color: transparent;
|
||||
color: ${({ theme }) => theme.text1};
|
||||
|
||||
&:focus {
|
||||
box-shadow: 0 0 0 1px ${({ theme }) => theme.bg4};
|
||||
}
|
||||
&:hover {
|
||||
box-shadow: 0 0 0 1px ${({ theme }) => theme.bg4};
|
||||
}
|
||||
&:active {
|
||||
box-shadow: 0 0 0 1px ${({ theme }) => theme.bg4};
|
||||
}
|
||||
&:disabled {
|
||||
opacity: 50%;
|
||||
cursor: auto;
|
||||
}
|
||||
`
|
||||
|
||||
export const ButtonEmpty = styled(Base)`
|
||||
background-color: transparent;
|
||||
color: ${({ theme }) => theme.primary1};
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
&:focus {
|
||||
background-color: ${({ theme }) => theme.advancedBG};
|
||||
}
|
||||
&:hover {
|
||||
background-color: ${({ theme }) => theme.advancedBG};
|
||||
}
|
||||
&:active {
|
||||
background-color: ${({ theme }) => theme.advancedBG};
|
||||
}
|
||||
&:disabled {
|
||||
opacity: 50%;
|
||||
cursor: auto;
|
||||
}
|
||||
`
|
||||
|
||||
export const ButtonWhite = styled(Base)`
|
||||
border: 1px solid #edeef2;
|
||||
background-color: ${({ theme }) => theme.bg1};
|
||||
};
|
||||
color: black;
|
||||
|
||||
&:focus {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
box-shadow: 0 0 0 1pt ${darken(0.05, '#edeef2')};
|
||||
}
|
||||
&:hover {
|
||||
box-shadow: 0 0 0 1pt ${darken(0.1, '#edeef2')};
|
||||
}
|
||||
&:active {
|
||||
box-shadow: 0 0 0 1pt ${darken(0.1, '#edeef2')};
|
||||
}
|
||||
&:disabled {
|
||||
opacity: 50%;
|
||||
cursor: auto;
|
||||
}
|
||||
`
|
||||
|
||||
const ButtonConfirmedStyle = styled(Base)`
|
||||
background-color: ${({ theme }) => lighten(0.5, theme.green1)};
|
||||
color: ${({ theme }) => theme.green1};
|
||||
border: 1px solid ${({ theme }) => theme.green1};
|
||||
|
||||
&:disabled {
|
||||
opacity: 50%;
|
||||
cursor: auto;
|
||||
}
|
||||
`
|
||||
|
||||
const ButtonErrorStyle = styled(Base)`
|
||||
background-color: ${({ theme }) => theme.red1};
|
||||
border: 1px solid ${({ theme }) => theme.red1};
|
||||
|
||||
&:focus {
|
||||
box-shadow: 0 0 0 1pt ${({ theme }) => darken(0.05, theme.red1)};
|
||||
background-color: ${({ theme }) => darken(0.05, theme.red1)};
|
||||
}
|
||||
&:hover {
|
||||
background-color: ${({ theme }) => darken(0.05, theme.red1)};
|
||||
}
|
||||
&:active {
|
||||
box-shadow: 0 0 0 1pt ${({ theme }) => darken(0.1, theme.red1)};
|
||||
background-color: ${({ theme }) => darken(0.1, theme.red1)};
|
||||
}
|
||||
&:disabled {
|
||||
opacity: 50%;
|
||||
cursor: auto;
|
||||
}
|
||||
`
|
||||
|
||||
export function ButtonConfirmed({ confirmed, ...rest }: { confirmed?: boolean } & ButtonProps) {
|
||||
if (confirmed) {
|
||||
return <ButtonConfirmedStyle {...rest} />
|
||||
} else {
|
||||
return <ButtonPrimary {...rest} />
|
||||
}
|
||||
}
|
||||
|
||||
export function ButtonError({ error, ...rest }: { error?: boolean } & ButtonProps) {
|
||||
if (error) {
|
||||
return <ButtonErrorStyle {...rest} />
|
||||
} else {
|
||||
return <ButtonPrimary {...rest} />
|
||||
}
|
||||
}
|
||||
|
||||
export function ButtonDropwdown({ disabled = false, children, ...rest }: { disabled?: boolean } & ButtonProps) {
|
||||
return (
|
||||
<ButtonPrimary {...rest} disabled={disabled}>
|
||||
<RowBetween>
|
||||
<div style={{ display: 'flex', alignItems: 'center' }}>{children}</div>
|
||||
<ChevronDown size={24} />
|
||||
</RowBetween>
|
||||
</ButtonPrimary>
|
||||
)
|
||||
}
|
||||
|
||||
export function ButtonDropwdownLight({ disabled = false, children, ...rest }: { disabled?: boolean } & ButtonProps) {
|
||||
return (
|
||||
<ButtonOutlined {...rest} disabled={disabled}>
|
||||
<RowBetween>
|
||||
<div style={{ display: 'flex', alignItems: 'center' }}>{children}</div>
|
||||
<ChevronDown size={24} />
|
||||
</RowBetween>
|
||||
</ButtonOutlined>
|
||||
)
|
||||
}
|
||||
|
||||
export function ButtonRadio({ active, ...rest }: { active?: boolean } & ButtonProps) {
|
||||
if (!active) {
|
||||
return <ButtonWhite {...rest} />
|
||||
} else {
|
||||
return <ButtonPrimary {...rest} />
|
||||
}
|
||||
}
|
||||
56
src/components/Card/index.tsx
Normal file
@@ -0,0 +1,56 @@
|
||||
import React from 'react'
|
||||
import styled from 'styled-components'
|
||||
import { CardProps, Text } from 'rebass'
|
||||
import { Box } from 'rebass/styled-components'
|
||||
|
||||
const Card = styled(Box)<{ padding?: string; border?: string; borderRadius?: string }>`
|
||||
width: 100%;
|
||||
border-radius: 16px;
|
||||
padding: 1.25rem;
|
||||
padding: ${({ padding }) => padding};
|
||||
border: ${({ border }) => border};
|
||||
border-radius: ${({ borderRadius }) => borderRadius};
|
||||
`
|
||||
export default Card
|
||||
|
||||
export const LightCard = styled(Card)`
|
||||
border: 1px solid ${({ theme }) => theme.bg2};
|
||||
background-color: ${({ theme }) => theme.bg1};
|
||||
`
|
||||
|
||||
export const GreyCard = styled(Card)`
|
||||
background-color: ${({ theme }) => theme.advancedBG};
|
||||
`
|
||||
|
||||
export const OutlineCard = styled(Card)`
|
||||
border: 1px solid ${({ theme }) => theme.advancedBG};
|
||||
`
|
||||
|
||||
export const YellowCard = styled(Card)`
|
||||
background-color: rgba(243, 132, 30, 0.05);
|
||||
color: ${({ theme }) => theme.yellow2};
|
||||
font-weight: 500;
|
||||
`
|
||||
|
||||
export const PinkCard = styled(Card)`
|
||||
background-color: rgba(255, 0, 122, 0.03);
|
||||
color: ${({ theme }) => theme.primary1};
|
||||
font-weight: 500;
|
||||
`
|
||||
|
||||
const BlueCardStyled = styled(Card)`
|
||||
background-color: ${({ theme }) => theme.primary5};
|
||||
color: ${({ theme }) => theme.primary1};
|
||||
border-radius: 12px;
|
||||
width: fit-content;
|
||||
`
|
||||
|
||||
export const BlueCard = ({ children, ...rest }: CardProps) => {
|
||||
return (
|
||||
<BlueCardStyled {...rest}>
|
||||
<Text fontWeight={500} color="#2172E5">
|
||||
{children}
|
||||
</Text>
|
||||
</BlueCardStyled>
|
||||
)
|
||||
}
|
||||
23
src/components/Column/index.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
import styled from 'styled-components'
|
||||
|
||||
const Column = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
`
|
||||
export const ColumnCenter = styled(Column)`
|
||||
width: 100%;
|
||||
align-items: center;
|
||||
`
|
||||
|
||||
export const AutoColumn = styled.div<{
|
||||
gap?: 'sm' | 'md' | 'lg' | string
|
||||
justify?: 'stretch' | 'center' | 'start' | 'end' | 'flex-start' | 'flex-end' | 'space-between'
|
||||
}>`
|
||||
display: grid;
|
||||
grid-auto-rows: auto;
|
||||
grid-row-gap: ${({ gap }) => (gap === 'sm' && '8px') || (gap === 'md' && '12px') || (gap === 'lg' && '24px') || gap};
|
||||
justify-items: ${({ justify }) => justify && justify};
|
||||
`
|
||||
|
||||
export default Column
|
||||
133
src/components/ConfirmationModal/index.tsx
Normal file
@@ -0,0 +1,133 @@
|
||||
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>
|
||||
)
|
||||
}
|
||||
@@ -1,128 +0,0 @@
|
||||
import React, { Component } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import ReactGA from 'react-ga'
|
||||
import { ReactComponent as Dropup } from '../../assets/images/dropup-blue.svg'
|
||||
import { ReactComponent as Dropdown } from '../../assets/images/dropdown-blue.svg'
|
||||
|
||||
const SummaryWrapper = styled.div`
|
||||
color: ${({ error, theme }) => (error ? theme.salmonRed : theme.doveGray)};
|
||||
font-size: 0.75rem;
|
||||
text-align: center;
|
||||
margin-top: 1rem;
|
||||
padding-top: 1rem;
|
||||
`
|
||||
|
||||
const Details = styled.div`
|
||||
background-color: ${({ theme }) => theme.concreteGray};
|
||||
padding: 1.5rem;
|
||||
border-radius: 1rem;
|
||||
font-size: 0.75rem;
|
||||
margin-top: 1rem;
|
||||
`
|
||||
|
||||
const SummaryWrapperContainer = styled.div`
|
||||
${({ theme }) => theme.flexRowNoWrap};
|
||||
color: ${({ theme }) => theme.royalBlue};
|
||||
text-align: center;
|
||||
margin-top: 1rem;
|
||||
padding-top: 1rem;
|
||||
cursor: pointer;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 0.75rem;
|
||||
|
||||
span {
|
||||
margin-right: 12px;
|
||||
}
|
||||
|
||||
img {
|
||||
height: 0.75rem;
|
||||
width: 0.75rem;
|
||||
}
|
||||
`
|
||||
|
||||
const WrappedDropup = ({ isError, highSlippageWarning, ...rest }) => <Dropup {...rest} />
|
||||
const ColoredDropup = styled(WrappedDropup)`
|
||||
path {
|
||||
stroke: ${({ theme }) => theme.royalBlue};
|
||||
}
|
||||
`
|
||||
|
||||
const WrappedDropdown = ({ isError, highSlippageWarning, ...rest }) => <Dropdown {...rest} />
|
||||
const ColoredDropdown = styled(WrappedDropdown)`
|
||||
path {
|
||||
stroke: ${({ theme }) => theme.royalBlue};
|
||||
}
|
||||
`
|
||||
|
||||
class ContextualInfo extends Component {
|
||||
static propTypes = {
|
||||
openDetailsText: PropTypes.string,
|
||||
renderTransactionDetails: PropTypes.func,
|
||||
contextualInfo: PropTypes.string,
|
||||
isError: PropTypes.bool
|
||||
}
|
||||
|
||||
static defaultProps = {
|
||||
openDetailsText: 'Advanced Details',
|
||||
closeDetailsText: 'Hide Advanced',
|
||||
renderTransactionDetails() {},
|
||||
contextualInfo: '',
|
||||
isError: false
|
||||
}
|
||||
|
||||
state = {
|
||||
showDetails: false
|
||||
}
|
||||
|
||||
renderDetails() {
|
||||
if (!this.state.showDetails) {
|
||||
return null
|
||||
}
|
||||
|
||||
return <Details>{this.props.renderTransactionDetails()}</Details>
|
||||
}
|
||||
|
||||
render() {
|
||||
const { openDetailsText, closeDetailsText, contextualInfo, isError } = this.props
|
||||
|
||||
if (contextualInfo) {
|
||||
return <SummaryWrapper error={isError}>{contextualInfo}</SummaryWrapper>
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<SummaryWrapperContainer
|
||||
onClick={() => {
|
||||
!this.state.showDetails &&
|
||||
ReactGA.event({
|
||||
category: 'Advanced Interaction',
|
||||
action: 'Open Advanced Details',
|
||||
label: 'Pool Page Details'
|
||||
})
|
||||
this.setState(prevState => {
|
||||
return { showDetails: !prevState.showDetails }
|
||||
})
|
||||
}}
|
||||
>
|
||||
{!this.state.showDetails ? (
|
||||
<>
|
||||
<span>{openDetailsText}</span>
|
||||
<ColoredDropup />
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<span>{closeDetailsText}</span>
|
||||
<ColoredDropdown />
|
||||
</>
|
||||
)}
|
||||
</SummaryWrapperContainer>
|
||||
{this.renderDetails()}
|
||||
</>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default ContextualInfo
|
||||
@@ -1,133 +0,0 @@
|
||||
import React, { useState } from 'react'
|
||||
import styled, { css } from 'styled-components'
|
||||
import { transparentize } from 'polished'
|
||||
import ReactGA from 'react-ga'
|
||||
import { ReactComponent as Dropup } from '../../assets/images/dropup-blue.svg'
|
||||
import { ReactComponent as Dropdown } from '../../assets/images/dropdown-blue.svg'
|
||||
|
||||
const SummaryWrapper = styled.div`
|
||||
color: ${({ error, brokenTokenWarning, theme }) => (error || brokenTokenWarning ? theme.salmonRed : theme.doveGray)};
|
||||
font-size: 0.75rem;
|
||||
text-align: center;
|
||||
margin-top: 1rem;
|
||||
padding-top: 1rem;
|
||||
`
|
||||
|
||||
const SummaryWrapperContainer = styled.div`
|
||||
${({ theme }) => theme.flexRowNoWrap};
|
||||
color: ${({ theme }) => theme.royalBlue};
|
||||
text-align: center;
|
||||
margin-top: 1rem;
|
||||
padding-top: 1rem;
|
||||
cursor: pointer;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 0.75rem;
|
||||
|
||||
img {
|
||||
height: 0.75rem;
|
||||
width: 0.75rem;
|
||||
}
|
||||
`
|
||||
|
||||
const Details = styled.div`
|
||||
background-color: ${({ theme }) => theme.concreteGray};
|
||||
/* padding: 1.25rem 1.25rem 1rem 1.25rem; */
|
||||
border-radius: 1rem;
|
||||
font-size: 0.75rem;
|
||||
margin: 1rem 0.5rem 0 0.5rem;
|
||||
`
|
||||
|
||||
const ErrorSpan = styled.span`
|
||||
margin-right: 12px;
|
||||
font-size: 0.75rem;
|
||||
line-height: 0.75rem;
|
||||
|
||||
color: ${({ isError, theme }) => isError && theme.salmonRed};
|
||||
${({ slippageWarning, highSlippageWarning, theme }) =>
|
||||
highSlippageWarning
|
||||
? css`
|
||||
color: ${theme.salmonRed};
|
||||
font-weight: 600;
|
||||
`
|
||||
: slippageWarning &&
|
||||
css`
|
||||
background-color: ${transparentize(0.6, theme.warningYellow)};
|
||||
font-weight: 600;
|
||||
padding: 0.25rem;
|
||||
`}
|
||||
`
|
||||
|
||||
const WrappedDropup = ({ isError, highSlippageWarning, ...rest }) => <Dropup {...rest} />
|
||||
const ColoredDropup = styled(WrappedDropup)`
|
||||
path {
|
||||
stroke: ${({ isError, theme }) => (isError ? theme.salmonRed : theme.royalBlue)};
|
||||
|
||||
${({ highSlippageWarning, theme }) =>
|
||||
highSlippageWarning &&
|
||||
css`
|
||||
stroke: ${theme.salmonRed};
|
||||
`}
|
||||
}
|
||||
`
|
||||
|
||||
const WrappedDropdown = ({ isError, highSlippageWarning, ...rest }) => <Dropdown {...rest} />
|
||||
const ColoredDropdown = styled(WrappedDropdown)`
|
||||
path {
|
||||
stroke: ${({ isError, theme }) => (isError ? theme.salmonRed : theme.royalBlue)};
|
||||
|
||||
${({ highSlippageWarning, theme }) =>
|
||||
highSlippageWarning &&
|
||||
css`
|
||||
stroke: ${theme.salmonRed};
|
||||
`}
|
||||
}
|
||||
`
|
||||
|
||||
export default function ContextualInfo({
|
||||
openDetailsText = 'Advanced Details',
|
||||
closeDetailsText = 'Hide Advanced',
|
||||
contextualInfo = '',
|
||||
allowExpand = false,
|
||||
isError = false,
|
||||
slippageWarning,
|
||||
highSlippageWarning,
|
||||
brokenTokenWarning,
|
||||
dropDownContent
|
||||
}) {
|
||||
const [showDetails, setShowDetails] = useState(false)
|
||||
return !allowExpand ? (
|
||||
<SummaryWrapper brokenTokenWarning={brokenTokenWarning}>{contextualInfo}</SummaryWrapper>
|
||||
) : (
|
||||
<>
|
||||
<SummaryWrapperContainer
|
||||
onClick={() => {
|
||||
!showDetails &&
|
||||
ReactGA.event({
|
||||
category: 'Advanced Interaction',
|
||||
action: 'Open Advanced Details',
|
||||
label: 'Swap/Send Page Details'
|
||||
})
|
||||
setShowDetails(s => !s)
|
||||
}}
|
||||
>
|
||||
<>
|
||||
<ErrorSpan isError={isError} slippageWarning={slippageWarning} highSlippageWarning={highSlippageWarning}>
|
||||
{(slippageWarning || highSlippageWarning) && (
|
||||
<span role="img" aria-label="warning">
|
||||
⚠️
|
||||
</span>
|
||||
)}
|
||||
{contextualInfo ? contextualInfo : showDetails ? closeDetailsText : openDetailsText}
|
||||
</ErrorSpan>
|
||||
{showDetails ? (
|
||||
<ColoredDropup isError={isError} highSlippageWarning={highSlippageWarning} />
|
||||
) : (
|
||||
<ColoredDropdown isError={isError} highSlippageWarning={highSlippageWarning} />
|
||||
)}
|
||||
</>
|
||||
</SummaryWrapperContainer>
|
||||
{showDetails && <Details>{dropDownContent()}</Details>}
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -1,684 +0,0 @@
|
||||
import React, { useState, useRef, useMemo } from 'react'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { ethers } from 'ethers'
|
||||
import { BigNumber } from '@uniswap/sdk'
|
||||
import styled from 'styled-components'
|
||||
import escapeStringRegex from 'escape-string-regexp'
|
||||
import { darken } from 'polished'
|
||||
import Tooltip from '@reach/tooltip'
|
||||
import '@reach/tooltip/styles.css'
|
||||
import { isMobile } from 'react-device-detect'
|
||||
|
||||
import { BorderlessInput } from '../../theme'
|
||||
import { useWeb3React, useTokenContract } from '../../hooks'
|
||||
import { isAddress, calculateGasMargin, formatToUsd, formatTokenBalance, formatEthBalance } from '../../utils'
|
||||
import { ReactComponent as DropDown } from '../../assets/images/dropdown.svg'
|
||||
import Modal from '../Modal'
|
||||
import TokenLogo from '../TokenLogo'
|
||||
import SearchIcon from '../../assets/images/magnifying-glass.svg'
|
||||
import { useTransactionAdder, usePendingApproval } from '../../contexts/Transactions'
|
||||
import { useTokenDetails, useAllTokenDetails, INITIAL_TOKENS_CONTEXT } from '../../contexts/Tokens'
|
||||
import { useAddressBalance } from '../../contexts/Balances'
|
||||
import { ReactComponent as Close } from '../../assets/images/x.svg'
|
||||
import { transparentize } from 'polished'
|
||||
import { Spinner } from '../../theme'
|
||||
import Circle from '../../assets/images/circle-grey.svg'
|
||||
import { useETHPriceInUSD, useAllBalances } from '../../contexts/Balances'
|
||||
|
||||
const GAS_MARGIN = ethers.utils.bigNumberify(1000)
|
||||
|
||||
const SubCurrencySelect = styled.button`
|
||||
${({ theme }) => theme.flexRowNoWrap}
|
||||
padding: 4px 50px 4px 15px;
|
||||
margin-right: -40px;
|
||||
line-height: 0;
|
||||
height: 2rem;
|
||||
align-items: center;
|
||||
border-radius: 2.5rem;
|
||||
outline: none;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
background: ${({ theme }) => theme.zumthorBlue};
|
||||
border: 1px solid ${({ theme }) => theme.royalBlue};
|
||||
color: ${({ theme }) => theme.royalBlue};
|
||||
`
|
||||
|
||||
const InputRow = styled.div`
|
||||
${({ theme }) => theme.flexRowNoWrap}
|
||||
align-items: center;
|
||||
|
||||
padding: 0.25rem 0.85rem 0.75rem;
|
||||
`
|
||||
|
||||
const Input = styled(BorderlessInput)`
|
||||
font-size: 1.5rem;
|
||||
color: ${({ error, theme }) => error && theme.salmonRed};
|
||||
background-color: ${({ theme }) => theme.inputBackground};
|
||||
-moz-appearance: textfield;
|
||||
`
|
||||
|
||||
const StyledBorderlessInput = styled(BorderlessInput)`
|
||||
min-height: 2.5rem;
|
||||
flex-shrink: 0;
|
||||
text-align: left;
|
||||
padding-left: 1.6rem;
|
||||
background-color: ${({ theme }) => theme.concreteGray};
|
||||
`
|
||||
|
||||
const CurrencySelect = styled.button`
|
||||
align-items: center;
|
||||
font-size: 1rem;
|
||||
color: ${({ selected, theme }) => (selected ? theme.textColor : theme.royalBlue)};
|
||||
height: 2rem;
|
||||
border: 1px solid ${({ selected, theme }) => (selected ? theme.mercuryGray : theme.royalBlue)};
|
||||
border-radius: 2.5rem;
|
||||
background-color: ${({ selected, theme }) => (selected ? theme.concreteGray : theme.zumthorBlue)};
|
||||
outline: none;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
|
||||
:hover {
|
||||
border: 1px solid
|
||||
${({ selected, theme }) => (selected ? darken(0.1, theme.mercuryGray) : darken(0.1, theme.royalBlue))};
|
||||
}
|
||||
|
||||
:focus {
|
||||
border: 1px solid ${({ theme }) => darken(0.1, theme.royalBlue)};
|
||||
}
|
||||
|
||||
:active {
|
||||
background-color: ${({ theme }) => theme.zumthorBlue};
|
||||
}
|
||||
`
|
||||
|
||||
const Aligner = styled.span`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
`
|
||||
|
||||
const StyledDropDown = styled(DropDown)`
|
||||
margin: 0 0.5rem 0 0.5rem;
|
||||
height: 35%;
|
||||
|
||||
path {
|
||||
stroke: ${({ selected, theme }) => (selected ? theme.textColor : theme.royalBlue)};
|
||||
}
|
||||
`
|
||||
|
||||
const InputPanel = styled.div`
|
||||
${({ theme }) => theme.flexColumnNoWrap}
|
||||
box-shadow: 0 4px 8px 0 ${({ theme }) => transparentize(0.95, theme.shadowColor)};
|
||||
position: relative;
|
||||
border-radius: 1.25rem;
|
||||
background-color: ${({ theme }) => theme.inputBackground};
|
||||
z-index: 1;
|
||||
`
|
||||
|
||||
const Container = styled.div`
|
||||
border-radius: 1.25rem;
|
||||
border: 1px solid ${({ error, theme }) => (error ? theme.salmonRed : theme.mercuryGray)};
|
||||
|
||||
background-color: ${({ theme }) => theme.inputBackground};
|
||||
:focus-within {
|
||||
border: 1px solid ${({ theme }) => theme.malibuBlue};
|
||||
}
|
||||
`
|
||||
|
||||
const LabelRow = styled.div`
|
||||
${({ theme }) => theme.flexRowNoWrap}
|
||||
align-items: center;
|
||||
color: ${({ theme }) => theme.doveGray};
|
||||
font-size: 0.75rem;
|
||||
line-height: 1rem;
|
||||
padding: 0.75rem 1rem;
|
||||
span:hover {
|
||||
cursor: pointer;
|
||||
color: ${({ theme }) => darken(0.2, theme.doveGray)};
|
||||
}
|
||||
`
|
||||
|
||||
const LabelContainer = styled.div`
|
||||
flex: 1 1 auto;
|
||||
width: 0;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
`
|
||||
|
||||
const ErrorSpan = styled.span`
|
||||
color: ${({ error, theme }) => error && theme.salmonRed};
|
||||
:hover {
|
||||
cursor: pointer;
|
||||
color: ${({ error, theme }) => error && darken(0.1, theme.salmonRed)};
|
||||
}
|
||||
`
|
||||
|
||||
const TokenModal = styled.div`
|
||||
${({ theme }) => theme.flexColumnNoWrap}
|
||||
width: 100%;
|
||||
`
|
||||
|
||||
const ModalHeader = styled.div`
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
padding: 0px 0px 0px 1rem;
|
||||
height: 60px;
|
||||
`
|
||||
|
||||
const CloseColor = styled(Close)`
|
||||
path {
|
||||
stroke: ${({ theme }) => theme.textColor};
|
||||
}
|
||||
`
|
||||
|
||||
const CloseIcon = styled.div`
|
||||
position: absolute;
|
||||
right: 1rem;
|
||||
top: 14px;
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
opacity: 0.6;
|
||||
}
|
||||
`
|
||||
|
||||
const SearchContainer = styled.div`
|
||||
${({ theme }) => theme.flexRowNoWrap}
|
||||
justify-content: flex-start;
|
||||
padding: 0.5rem 1.5rem;
|
||||
background-color: ${({ theme }) => theme.concreteGray};
|
||||
`
|
||||
|
||||
const TokenModalInfo = styled.div`
|
||||
${({ theme }) => theme.flexRowNoWrap}
|
||||
align-items: center;
|
||||
padding: 1rem 1.5rem;
|
||||
margin: 0.25rem 0.5rem;
|
||||
justify-content: center;
|
||||
user-select: none;
|
||||
`
|
||||
|
||||
const TokenList = styled.div`
|
||||
flex-grow: 1;
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
`
|
||||
|
||||
const TokenModalRow = styled.div`
|
||||
${({ theme }) => theme.flexRowNoWrap}
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 1rem;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
|
||||
#symbol {
|
||||
color: ${({ theme }) => theme.doveGrey};
|
||||
}
|
||||
|
||||
:hover {
|
||||
background-color: ${({ theme }) => theme.tokenRowHover};
|
||||
}
|
||||
|
||||
${({ theme }) => theme.mediaWidth.upToMedium`
|
||||
padding: 0.8rem 1rem;
|
||||
padding-right: 2rem;
|
||||
`}
|
||||
`
|
||||
|
||||
const TokenRowLeft = styled.div`
|
||||
${({ theme }) => theme.flexRowNoWrap}
|
||||
align-items : center;
|
||||
`
|
||||
|
||||
const TokenSymbolGroup = styled.div`
|
||||
${({ theme }) => theme.flexColumnNoWrap};
|
||||
margin-left: 1rem;
|
||||
`
|
||||
|
||||
const TokenFullName = styled.div`
|
||||
color: ${({ theme }) => theme.chaliceGray};
|
||||
`
|
||||
|
||||
const FadedSpan = styled.span`
|
||||
color: ${({ theme }) => theme.royalBlue};
|
||||
`
|
||||
|
||||
const TokenRowBalance = styled.div`
|
||||
font-size: 1rem;
|
||||
line-height: 20px;
|
||||
`
|
||||
|
||||
const TokenRowUsd = styled.div`
|
||||
font-size: 1rem;
|
||||
line-height: 1.5rem;
|
||||
color: ${({ theme }) => theme.chaliceGray};
|
||||
`
|
||||
|
||||
const TokenRowRight = styled.div`
|
||||
${({ theme }) => theme.flexColumnNoWrap};
|
||||
align-items: flex-end;
|
||||
`
|
||||
|
||||
const StyledTokenName = styled.span`
|
||||
margin: 0 0.25rem 0 0.25rem;
|
||||
`
|
||||
|
||||
const SpinnerWrapper = styled(Spinner)`
|
||||
margin: 0 0.25rem 0 0.25rem;
|
||||
color: ${({ theme }) => theme.chaliceGray};
|
||||
opacity: 0.6;
|
||||
`
|
||||
|
||||
export default function CurrencyInputPanel({
|
||||
onValueChange = () => {},
|
||||
allBalances,
|
||||
renderInput,
|
||||
onCurrencySelected = () => {},
|
||||
title,
|
||||
description,
|
||||
extraText,
|
||||
extraTextClickHander = () => {},
|
||||
errorMessage,
|
||||
disableUnlock,
|
||||
disableTokenSelect,
|
||||
selectedTokenAddress = '',
|
||||
showUnlock,
|
||||
value,
|
||||
urlAddedTokens
|
||||
}) {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const [modalIsOpen, setModalIsOpen] = useState(false)
|
||||
|
||||
const tokenContract = useTokenContract(selectedTokenAddress)
|
||||
const { exchangeAddress: selectedTokenExchangeAddress } = useTokenDetails(selectedTokenAddress)
|
||||
|
||||
const pendingApproval = usePendingApproval(selectedTokenAddress)
|
||||
|
||||
const addTransaction = useTransactionAdder()
|
||||
|
||||
const allTokens = useAllTokenDetails()
|
||||
|
||||
const { account } = useWeb3React()
|
||||
|
||||
const userTokenBalance = useAddressBalance(account, selectedTokenAddress)
|
||||
|
||||
function renderUnlockButton() {
|
||||
if (disableUnlock || !showUnlock || selectedTokenAddress === 'ETH' || !selectedTokenAddress) {
|
||||
return null
|
||||
} else {
|
||||
if (!pendingApproval) {
|
||||
return (
|
||||
<SubCurrencySelect
|
||||
onClick={async () => {
|
||||
let estimatedGas
|
||||
let useUserBalance = false
|
||||
estimatedGas = await tokenContract.estimate
|
||||
.approve(selectedTokenExchangeAddress, ethers.constants.MaxUint256)
|
||||
.catch(e => {
|
||||
console.log('Error setting max token approval.')
|
||||
})
|
||||
if (!estimatedGas) {
|
||||
// general fallback for tokens who restrict approval amounts
|
||||
estimatedGas = await tokenContract.estimate.approve(selectedTokenExchangeAddress, userTokenBalance)
|
||||
useUserBalance = true
|
||||
}
|
||||
tokenContract
|
||||
.approve(
|
||||
selectedTokenExchangeAddress,
|
||||
useUserBalance ? userTokenBalance : ethers.constants.MaxUint256,
|
||||
{
|
||||
gasLimit: calculateGasMargin(estimatedGas, GAS_MARGIN)
|
||||
}
|
||||
)
|
||||
.then(response => {
|
||||
addTransaction(response, { approval: selectedTokenAddress })
|
||||
})
|
||||
}}
|
||||
>
|
||||
{t('unlock')}
|
||||
</SubCurrencySelect>
|
||||
)
|
||||
} else {
|
||||
return <SubCurrencySelect>{t('pending')}</SubCurrencySelect>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function _renderInput() {
|
||||
if (typeof renderInput === 'function') {
|
||||
return renderInput()
|
||||
}
|
||||
|
||||
return (
|
||||
<InputRow>
|
||||
<Input
|
||||
type="number"
|
||||
min="0"
|
||||
step="0.000000000000000001"
|
||||
error={!!errorMessage}
|
||||
placeholder="0.0"
|
||||
onChange={e => onValueChange(e.target.value)}
|
||||
onKeyPress={e => {
|
||||
const charCode = e.which ? e.which : e.keyCode
|
||||
|
||||
// Prevent 'minus' character
|
||||
if (charCode === 45) {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
}
|
||||
}}
|
||||
value={value}
|
||||
/>
|
||||
{renderUnlockButton()}
|
||||
<CurrencySelect
|
||||
selected={!!selectedTokenAddress}
|
||||
onClick={() => {
|
||||
if (!disableTokenSelect) {
|
||||
setModalIsOpen(true)
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Aligner>
|
||||
{selectedTokenAddress ? <TokenLogo address={selectedTokenAddress} /> : null}
|
||||
{
|
||||
<StyledTokenName>
|
||||
{(allTokens[selectedTokenAddress] && allTokens[selectedTokenAddress].symbol) || t('selectToken')}
|
||||
</StyledTokenName>
|
||||
}
|
||||
{!disableTokenSelect && <StyledDropDown selected={!!selectedTokenAddress} />}
|
||||
</Aligner>
|
||||
</CurrencySelect>
|
||||
</InputRow>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<InputPanel>
|
||||
<Container error={!!errorMessage}>
|
||||
<LabelRow>
|
||||
<LabelContainer>
|
||||
<span>{title}</span> <span>{description}</span>
|
||||
</LabelContainer>
|
||||
|
||||
<ErrorSpan
|
||||
data-tip={'Enter max'}
|
||||
error={!!errorMessage}
|
||||
onClick={() => {
|
||||
extraTextClickHander()
|
||||
}}
|
||||
>
|
||||
<Tooltip
|
||||
label="Enter Max"
|
||||
style={{
|
||||
background: 'hsla(0, 0%, 0%, 0.75)',
|
||||
color: 'white',
|
||||
border: 'none',
|
||||
borderRadius: '24px',
|
||||
padding: '0.5em 1em',
|
||||
marginTop: '-64px'
|
||||
}}
|
||||
>
|
||||
<span>{extraText}</span>
|
||||
</Tooltip>
|
||||
</ErrorSpan>
|
||||
</LabelRow>
|
||||
{_renderInput()}
|
||||
</Container>
|
||||
{!disableTokenSelect && (
|
||||
<CurrencySelectModal
|
||||
isOpen={modalIsOpen}
|
||||
onDismiss={() => {
|
||||
setModalIsOpen(false)
|
||||
}}
|
||||
urlAddedTokens={urlAddedTokens}
|
||||
onTokenSelect={onCurrencySelected}
|
||||
allBalances={allBalances}
|
||||
/>
|
||||
)}
|
||||
</InputPanel>
|
||||
)
|
||||
}
|
||||
|
||||
function CurrencySelectModal({ isOpen, onDismiss, onTokenSelect, urlAddedTokens }) {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const [searchQuery, setSearchQuery] = useState('')
|
||||
const { exchangeAddress } = useTokenDetails(searchQuery)
|
||||
|
||||
const allTokens = useAllTokenDetails()
|
||||
|
||||
const { account, chainId } = useWeb3React()
|
||||
|
||||
// BigNumber.js instance
|
||||
const ethPrice = useETHPriceInUSD()
|
||||
|
||||
// all balances for both account and exchanges
|
||||
let allBalances = useAllBalances()
|
||||
|
||||
const _usdAmounts = Object.keys(allTokens).map(k => {
|
||||
if (ethPrice && allBalances[account] && allBalances[account][k] && allBalances[account][k].value) {
|
||||
let ethRate = 1 // default for ETH
|
||||
let exchangeDetails = allBalances[allTokens[k].exchangeAddress]
|
||||
if (
|
||||
exchangeDetails &&
|
||||
exchangeDetails[k] &&
|
||||
exchangeDetails[k].value &&
|
||||
exchangeDetails['ETH'] &&
|
||||
exchangeDetails['ETH'].value
|
||||
) {
|
||||
const tokenBalance = new BigNumber(exchangeDetails[k].value.toString())
|
||||
const ethBalance = new BigNumber(exchangeDetails['ETH'].value.toString())
|
||||
ethRate = ethBalance.div(tokenBalance)
|
||||
}
|
||||
const USDRate = ethPrice
|
||||
.times(ethRate)
|
||||
.times(new BigNumber(10).pow(allTokens[k].decimals).div(new BigNumber(10).pow(18)))
|
||||
const balanceBigNumber = new BigNumber(allBalances[account][k].value.toString())
|
||||
const usdBalance = balanceBigNumber.times(USDRate).div(new BigNumber(10).pow(allTokens[k].decimals))
|
||||
return usdBalance
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
})
|
||||
const usdAmounts =
|
||||
_usdAmounts &&
|
||||
Object.keys(allTokens).reduce(
|
||||
(accumulator, currentValue, i) => Object.assign({ [currentValue]: _usdAmounts[i] }, accumulator),
|
||||
{}
|
||||
)
|
||||
|
||||
const tokenList = useMemo(() => {
|
||||
return Object.keys(allTokens)
|
||||
.sort((a, b) => {
|
||||
if (allTokens[a].symbol && allTokens[b].symbol) {
|
||||
const aSymbol = allTokens[a].symbol.toLowerCase()
|
||||
const bSymbol = allTokens[b].symbol.toLowerCase()
|
||||
|
||||
// pin ETH to top
|
||||
if (aSymbol === 'ETH'.toLowerCase() || bSymbol === 'ETH'.toLowerCase()) {
|
||||
return aSymbol === bSymbol ? 0 : aSymbol === 'ETH'.toLowerCase() ? -1 : 1
|
||||
}
|
||||
|
||||
// then tokens with balance
|
||||
if (usdAmounts[a] && !usdAmounts[b]) {
|
||||
return -1
|
||||
} else if (usdAmounts[b] && !usdAmounts[a]) {
|
||||
return 1
|
||||
}
|
||||
|
||||
// sort by balance
|
||||
if (usdAmounts[a] && usdAmounts[b]) {
|
||||
const aUSD = usdAmounts[a]
|
||||
const bUSD = usdAmounts[b]
|
||||
|
||||
return aUSD.gt(bUSD) ? -1 : aUSD.lt(bUSD) ? 1 : 0
|
||||
}
|
||||
|
||||
// sort alphabetically
|
||||
return aSymbol < bSymbol ? -1 : aSymbol > bSymbol ? 1 : 0
|
||||
} else {
|
||||
return 0
|
||||
}
|
||||
})
|
||||
.map(k => {
|
||||
let balance
|
||||
let usdBalance
|
||||
// only update if we have data
|
||||
if (k === 'ETH' && allBalances[account] && allBalances[account][k] && allBalances[account][k].value) {
|
||||
balance = formatEthBalance(allBalances[account][k].value)
|
||||
usdBalance = usdAmounts[k]
|
||||
} else if (allBalances[account] && allBalances[account][k] && allBalances[account][k].value) {
|
||||
balance = formatTokenBalance(allBalances[account][k].value, allTokens[k].decimals)
|
||||
usdBalance = usdAmounts[k]
|
||||
}
|
||||
return {
|
||||
name: allTokens[k].name,
|
||||
symbol: allTokens[k].symbol,
|
||||
address: k,
|
||||
balance: balance,
|
||||
usdBalance: usdBalance
|
||||
}
|
||||
})
|
||||
}, [allBalances, allTokens, usdAmounts, account])
|
||||
|
||||
const filteredTokenList = useMemo(() => {
|
||||
return tokenList.filter(tokenEntry => {
|
||||
const inputIsAddress = searchQuery.slice(0, 2) === '0x'
|
||||
|
||||
// check the regex for each field
|
||||
const regexMatches = Object.keys(tokenEntry).map(tokenEntryKey => {
|
||||
// if address field only search if input starts with 0x
|
||||
if (tokenEntryKey === 'address') {
|
||||
return (
|
||||
inputIsAddress &&
|
||||
typeof tokenEntry[tokenEntryKey] === 'string' &&
|
||||
!!tokenEntry[tokenEntryKey].match(new RegExp(escapeStringRegex(searchQuery), 'i'))
|
||||
)
|
||||
}
|
||||
return (
|
||||
typeof tokenEntry[tokenEntryKey] === 'string' &&
|
||||
!!tokenEntry[tokenEntryKey].match(new RegExp(escapeStringRegex(searchQuery), 'i'))
|
||||
)
|
||||
})
|
||||
return regexMatches.some(m => m)
|
||||
})
|
||||
}, [tokenList, searchQuery])
|
||||
|
||||
function _onTokenSelect(address) {
|
||||
setSearchQuery('')
|
||||
onTokenSelect(address)
|
||||
onDismiss()
|
||||
}
|
||||
|
||||
function renderTokenList() {
|
||||
if (isAddress(searchQuery) && exchangeAddress === undefined) {
|
||||
return <TokenModalInfo>Searching for Exchange...</TokenModalInfo>
|
||||
}
|
||||
if (isAddress(searchQuery) && exchangeAddress === ethers.constants.AddressZero) {
|
||||
return (
|
||||
<>
|
||||
<TokenModalInfo>{t('noExchange')}</TokenModalInfo>
|
||||
<TokenModalInfo>
|
||||
<Link to={`/create-exchange/${searchQuery}`}>{t('createExchange')}</Link>
|
||||
</TokenModalInfo>
|
||||
</>
|
||||
)
|
||||
}
|
||||
if (!filteredTokenList.length) {
|
||||
return <TokenModalInfo>{t('noExchange')}</TokenModalInfo>
|
||||
}
|
||||
|
||||
return filteredTokenList.map(({ address, symbol, name, balance, usdBalance }) => {
|
||||
const urlAdded = urlAddedTokens && urlAddedTokens.hasOwnProperty(address)
|
||||
const customAdded =
|
||||
address !== 'ETH' &&
|
||||
INITIAL_TOKENS_CONTEXT[chainId] &&
|
||||
!INITIAL_TOKENS_CONTEXT[chainId].hasOwnProperty(address) &&
|
||||
!urlAdded
|
||||
|
||||
return (
|
||||
<TokenModalRow key={address} onClick={() => _onTokenSelect(address)}>
|
||||
<TokenRowLeft>
|
||||
<TokenLogo address={address} size={'2rem'} />
|
||||
<TokenSymbolGroup>
|
||||
<div>
|
||||
<span id="symbol">{symbol}</span>
|
||||
<FadedSpan>
|
||||
{urlAdded && '(Added by URL)'} {customAdded && '(Added by user)'}
|
||||
</FadedSpan>
|
||||
</div>
|
||||
<TokenFullName> {name}</TokenFullName>
|
||||
</TokenSymbolGroup>
|
||||
</TokenRowLeft>
|
||||
<TokenRowRight>
|
||||
{balance ? (
|
||||
<TokenRowBalance>{balance && (balance > 0 || balance === '<0.0001') ? balance : '-'}</TokenRowBalance>
|
||||
) : account ? (
|
||||
<SpinnerWrapper src={Circle} alt="loader" />
|
||||
) : (
|
||||
'-'
|
||||
)}
|
||||
<TokenRowUsd>
|
||||
{usdBalance && !usdBalance.isNaN()
|
||||
? usdBalance.isZero()
|
||||
? ''
|
||||
: usdBalance.lt(0.01)
|
||||
? '<$0.01'
|
||||
: '$' + formatToUsd(usdBalance)
|
||||
: ''}
|
||||
</TokenRowUsd>
|
||||
</TokenRowRight>
|
||||
</TokenModalRow>
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
// manage focus on modal show
|
||||
const inputRef = useRef()
|
||||
|
||||
function onInput(event) {
|
||||
const input = event.target.value
|
||||
const checksummedInput = isAddress(input)
|
||||
setSearchQuery(checksummedInput || input)
|
||||
}
|
||||
|
||||
function clearInputAndDismiss() {
|
||||
setSearchQuery('')
|
||||
onDismiss()
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
onDismiss={clearInputAndDismiss}
|
||||
minHeight={60}
|
||||
maxHeight={50}
|
||||
initialFocusRef={isMobile ? undefined : inputRef}
|
||||
>
|
||||
<TokenModal>
|
||||
<ModalHeader>
|
||||
<p>Select Token</p>
|
||||
<CloseIcon onClick={clearInputAndDismiss}>
|
||||
<CloseColor alt={'close icon'} />
|
||||
</CloseIcon>
|
||||
</ModalHeader>
|
||||
<SearchContainer>
|
||||
<img src={SearchIcon} alt="search" />
|
||||
<StyledBorderlessInput
|
||||
ref={inputRef}
|
||||
type="text"
|
||||
placeholder={isMobile ? t('searchOrPasteMobile') : t('searchOrPaste')}
|
||||
onChange={onInput}
|
||||
/>
|
||||
</SearchContainer>
|
||||
<TokenList>{renderTokenList()}</TokenList>
|
||||
</TokenModal>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
253
src/components/CurrencyInputPanel/index.tsx
Normal file
@@ -0,0 +1,253 @@
|
||||
import { Pair, Token } from '@uniswap/sdk'
|
||||
import React, { useState, useContext } from 'react'
|
||||
import styled, { ThemeContext } from 'styled-components'
|
||||
import { darken } from 'polished'
|
||||
import { Field } from '../../state/swap/actions'
|
||||
import { useTokenBalanceTreatingWETHasETH } from '../../state/wallet/hooks'
|
||||
import TokenLogo from '../TokenLogo'
|
||||
import DoubleLogo from '../DoubleLogo'
|
||||
import SearchModal from '../SearchModal'
|
||||
import { RowBetween } from '../Row'
|
||||
import { TYPE, CursorPointer } from '../../theme'
|
||||
import { Input as NumericalInput } from '../NumericalInput'
|
||||
import { ReactComponent as DropDown } from '../../assets/images/dropdown.svg'
|
||||
|
||||
import { useActiveWeb3React } from '../../hooks'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
const InputRow = styled.div<{ selected: boolean }>`
|
||||
${({ theme }) => theme.flexRowNoWrap}
|
||||
align-items: center;
|
||||
padding: ${({ selected }) => (selected ? '0.75rem 0.5rem 0.75rem 1rem' : '0.75rem 0.75rem 0.75rem 1rem')};
|
||||
`
|
||||
|
||||
const CurrencySelect = styled.button<{ selected: boolean }>`
|
||||
align-items: center;
|
||||
height: 2.2rem;
|
||||
font-size: 20px;
|
||||
font-weight: 500;
|
||||
background-color: ${({ selected, theme }) => (selected ? theme.bg1 : theme.primary1)};
|
||||
color: ${({ selected, theme }) => (selected ? theme.text1 : theme.white)};
|
||||
border-radius: 12px;
|
||||
box-shadow: ${({ selected }) => (selected ? 'none' : '0px 6px 10px rgba(0, 0, 0, 0.075)')};
|
||||
outline: none;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
border: none;
|
||||
padding: 0 0.5rem;
|
||||
|
||||
:focus,
|
||||
:hover {
|
||||
background-color: ${({ selected, theme }) => (selected ? theme.bg2 : darken(0.05, theme.primary1))};
|
||||
}
|
||||
`
|
||||
|
||||
const LabelRow = styled.div`
|
||||
${({ theme }) => theme.flexRowNoWrap}
|
||||
align-items: center;
|
||||
color: ${({ theme }) => theme.text1};
|
||||
font-size: 0.75rem;
|
||||
line-height: 1rem;
|
||||
padding: 0.75rem 1rem 0 1rem;
|
||||
span:hover {
|
||||
cursor: pointer;
|
||||
color: ${({ theme }) => darken(0.2, theme.text2)};
|
||||
}
|
||||
`
|
||||
|
||||
const Aligner = styled.span`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
`
|
||||
|
||||
const StyledDropDown = styled(DropDown)<{ selected: boolean }>`
|
||||
margin: 0 0.25rem 0 0.5rem;
|
||||
height: 35%;
|
||||
|
||||
path {
|
||||
stroke: ${({ selected, theme }) => (selected ? theme.text1 : theme.white)};
|
||||
stroke-width: 1.5px;
|
||||
}
|
||||
`
|
||||
|
||||
const InputPanel = styled.div<{ hideInput?: boolean }>`
|
||||
${({ theme }) => theme.flexColumnNoWrap}
|
||||
position: relative;
|
||||
border-radius: ${({ hideInput }) => (hideInput ? '8px' : '20px')};
|
||||
background-color: ${({ theme }) => theme.bg2};
|
||||
z-index: 1;
|
||||
`
|
||||
|
||||
const Container = styled.div<{ hideInput: boolean }>`
|
||||
border-radius: ${({ hideInput }) => (hideInput ? '8px' : '20px')};
|
||||
border: 1px solid ${({ theme }) => theme.bg2};
|
||||
background-color: ${({ theme }) => theme.bg1};
|
||||
`
|
||||
|
||||
const StyledTokenName = styled.span<{ active?: boolean }>`
|
||||
${({ active }) => (active ? ' margin: 0 0.25rem 0 0.75rem;' : ' margin: 0 0.25rem 0 0.25rem;')}
|
||||
font-size: ${({ active }) => (active ? '20px' : '16px')};
|
||||
|
||||
`
|
||||
|
||||
const StyledBalanceMax = styled.button`
|
||||
height: 28px;
|
||||
background-color: ${({ theme }) => theme.primary5};
|
||||
border: 1px solid ${({ theme }) => theme.primary5};
|
||||
border-radius: 0.5rem;
|
||||
font-size: 0.875rem;
|
||||
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
margin-right: 0.5rem;
|
||||
color: ${({ theme }) => theme.primaryText1};
|
||||
:hover {
|
||||
border: 1px solid ${({ theme }) => theme.primary1};
|
||||
}
|
||||
:focus {
|
||||
border: 1px solid ${({ theme }) => theme.primary1};
|
||||
outline: none;
|
||||
}
|
||||
|
||||
${({ theme }) => theme.mediaWidth.upToExtraSmall`
|
||||
margin-right: 0.5rem;
|
||||
`};
|
||||
`
|
||||
|
||||
interface CurrencyInputPanelProps {
|
||||
value: string
|
||||
field: string
|
||||
onUserInput: (field: string, val: string) => void
|
||||
onMax?: () => void
|
||||
showMaxButton: boolean
|
||||
label?: string
|
||||
onTokenSelection?: (tokenAddress: string) => void
|
||||
token?: Token | null
|
||||
disableTokenSelect?: boolean
|
||||
hideBalance?: boolean
|
||||
isExchange?: boolean
|
||||
pair?: Pair | null
|
||||
hideInput?: boolean
|
||||
showSendWithSwap?: boolean
|
||||
otherSelectedTokenAddress?: string | null
|
||||
id: string
|
||||
}
|
||||
|
||||
export default function CurrencyInputPanel({
|
||||
value,
|
||||
field,
|
||||
onUserInput,
|
||||
onMax,
|
||||
showMaxButton,
|
||||
label = 'Input',
|
||||
onTokenSelection = null,
|
||||
token = null,
|
||||
disableTokenSelect = false,
|
||||
hideBalance = false,
|
||||
isExchange = false,
|
||||
pair = null, // used for double token logo
|
||||
hideInput = false,
|
||||
showSendWithSwap = false,
|
||||
otherSelectedTokenAddress = null,
|
||||
id
|
||||
}: CurrencyInputPanelProps) {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const [modalOpen, setModalOpen] = useState(false)
|
||||
const { account } = useActiveWeb3React()
|
||||
const userTokenBalance = useTokenBalanceTreatingWETHasETH(account, token)
|
||||
const theme = useContext(ThemeContext)
|
||||
|
||||
return (
|
||||
<InputPanel id={id}>
|
||||
<Container hideInput={hideInput}>
|
||||
{!hideInput && (
|
||||
<LabelRow>
|
||||
<RowBetween>
|
||||
<TYPE.body color={theme.text2} fontWeight={500} fontSize={14}>
|
||||
{label}
|
||||
</TYPE.body>
|
||||
{account && (
|
||||
<CursorPointer>
|
||||
<TYPE.body
|
||||
onClick={onMax}
|
||||
color={theme.text2}
|
||||
fontWeight={500}
|
||||
fontSize={14}
|
||||
style={{ display: 'inline' }}
|
||||
>
|
||||
{!hideBalance && !!token && userTokenBalance
|
||||
? 'Balance: ' + userTokenBalance?.toSignificant(6)
|
||||
: ' -'}
|
||||
</TYPE.body>
|
||||
</CursorPointer>
|
||||
)}
|
||||
</RowBetween>
|
||||
</LabelRow>
|
||||
)}
|
||||
<InputRow style={hideInput ? { padding: '0', borderRadius: '8px' } : {}} selected={disableTokenSelect}>
|
||||
{!hideInput && (
|
||||
<>
|
||||
<NumericalInput
|
||||
className="token-amount-input"
|
||||
value={value}
|
||||
onUserInput={val => {
|
||||
onUserInput(field, val)
|
||||
}}
|
||||
/>
|
||||
{account && !!token?.address && showMaxButton && label !== 'To' && (
|
||||
<StyledBalanceMax onClick={onMax}>MAX</StyledBalanceMax>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
<CurrencySelect
|
||||
selected={!!token}
|
||||
className="open-currency-select-button"
|
||||
onClick={() => {
|
||||
if (!disableTokenSelect) {
|
||||
setModalOpen(true)
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Aligner>
|
||||
{isExchange ? (
|
||||
<DoubleLogo a0={pair?.token0.address} a1={pair?.token1.address} size={24} margin={true} />
|
||||
) : token?.address ? (
|
||||
<TokenLogo address={token?.address} size={'24px'} />
|
||||
) : null}
|
||||
{isExchange ? (
|
||||
<StyledTokenName className="pair-name-container">
|
||||
{pair?.token0.symbol}:{pair?.token1.symbol}
|
||||
</StyledTokenName>
|
||||
) : (
|
||||
<StyledTokenName className="token-symbol-container" active={Boolean(token && token.symbol)}>
|
||||
{(token && token.symbol && token.symbol.length > 20
|
||||
? token.symbol.slice(0, 4) +
|
||||
'...' +
|
||||
token.symbol.slice(token.symbol.length - 5, token.symbol.length)
|
||||
: token?.symbol) || t('selectToken')}
|
||||
</StyledTokenName>
|
||||
)}
|
||||
{!disableTokenSelect && <StyledDropDown selected={!!token?.address} />}
|
||||
</Aligner>
|
||||
</CurrencySelect>
|
||||
</InputRow>
|
||||
</Container>
|
||||
{!disableTokenSelect && (
|
||||
<SearchModal
|
||||
isOpen={modalOpen}
|
||||
onDismiss={() => {
|
||||
setModalOpen(false)
|
||||
}}
|
||||
filterType="tokens"
|
||||
onTokenSelect={onTokenSelection}
|
||||
showSendWithSwap={showSendWithSwap}
|
||||
hiddenToken={token?.address}
|
||||
otherSelectedTokenAddress={otherSelectedTokenAddress}
|
||||
otherSelectedText={field === Field.INPUT ? 'Selected as output' : 'Selected as input'}
|
||||
/>
|
||||
)}
|
||||
</InputPanel>
|
||||
)
|
||||
}
|
||||
34
src/components/DoubleLogo/index.tsx
Normal file
@@ -0,0 +1,34 @@
|
||||
import React from 'react'
|
||||
import styled from 'styled-components'
|
||||
import TokenLogo from '../TokenLogo'
|
||||
|
||||
const TokenWrapper = styled.div<{ margin: boolean; sizeraw: number }>`
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
margin-right: ${({ sizeraw, margin }) => margin && (sizeraw / 3 + 8).toString() + 'px'};
|
||||
`
|
||||
|
||||
interface DoubleTokenLogoProps {
|
||||
margin?: boolean
|
||||
size?: number
|
||||
a0: string
|
||||
a1: string
|
||||
}
|
||||
|
||||
const HigherLogo = styled(TokenLogo)`
|
||||
z-index: 2;
|
||||
`
|
||||
const CoveredLogo = styled(TokenLogo)<{ sizeraw: number }>`
|
||||
position: absolute;
|
||||
left: ${({ sizeraw }) => (sizeraw / 2).toString() + 'px'};
|
||||
`
|
||||
|
||||
export default function DoubleTokenLogo({ a0, a1, size = 16, margin = false }: DoubleTokenLogoProps) {
|
||||
return (
|
||||
<TokenWrapper sizeraw={size} margin={margin}>
|
||||
<HigherLogo address={a0} size={size.toString() + 'px'} />
|
||||
<CoveredLogo address={a1} size={size.toString() + 'px'} sizeraw={size} />
|
||||
</TokenWrapper>
|
||||
)
|
||||
}
|
||||
@@ -1,935 +0,0 @@
|
||||
import React, { useState, useReducer, useEffect } from 'react'
|
||||
import ReactGA from 'react-ga'
|
||||
import { createBrowserHistory } from 'history'
|
||||
import { ethers } from 'ethers'
|
||||
import styled from 'styled-components'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import { useWeb3React } from '../../hooks'
|
||||
import { brokenTokens } from '../../constants'
|
||||
import { amountFormatter, calculateGasMargin, isAddress } from '../../utils'
|
||||
|
||||
import { useExchangeContract } from '../../hooks'
|
||||
import { useTokenDetails, INITIAL_TOKENS_CONTEXT } from '../../contexts/Tokens'
|
||||
import { useTransactionAdder } from '../../contexts/Transactions'
|
||||
import { useAddressBalance, useExchangeReserves } from '../../contexts/Balances'
|
||||
import { useAddressAllowance } from '../../contexts/Allowances'
|
||||
import { useWalletModalToggle } from '../../contexts/Application'
|
||||
|
||||
import { Button } from '../../theme'
|
||||
import CurrencyInputPanel from '../CurrencyInputPanel'
|
||||
import AddressInputPanel from '../AddressInputPanel'
|
||||
import OversizedPanel from '../OversizedPanel'
|
||||
import TransactionDetails from '../TransactionDetails'
|
||||
import ArrowDown from '../../assets/svg/SVGArrowDown'
|
||||
import WarningCard from '../WarningCard'
|
||||
|
||||
const INPUT = 0
|
||||
const OUTPUT = 1
|
||||
|
||||
const ETH_TO_TOKEN = 0
|
||||
const TOKEN_TO_ETH = 1
|
||||
const TOKEN_TO_TOKEN = 2
|
||||
|
||||
// denominated in bips
|
||||
const ALLOWED_SLIPPAGE_DEFAULT = 50
|
||||
const TOKEN_ALLOWED_SLIPPAGE_DEFAULT = 50
|
||||
|
||||
// 15 minutes, denominated in seconds
|
||||
const DEFAULT_DEADLINE_FROM_NOW = 60 * 15
|
||||
|
||||
// % above the calculated gas cost that we actually send, denominated in bips
|
||||
const GAS_MARGIN = ethers.utils.bigNumberify(1000)
|
||||
|
||||
const DownArrowBackground = styled.div`
|
||||
${({ theme }) => theme.flexRowNoWrap}
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
`
|
||||
|
||||
const WrappedArrowDown = ({ clickable, active, ...rest }) => <ArrowDown {...rest} />
|
||||
const DownArrow = styled(WrappedArrowDown)`
|
||||
color: ${({ theme, active }) => (active ? theme.royalBlue : theme.chaliceGray)};
|
||||
width: 0.625rem;
|
||||
height: 0.625rem;
|
||||
position: relative;
|
||||
padding: 0.875rem;
|
||||
cursor: ${({ clickable }) => clickable && 'pointer'};
|
||||
`
|
||||
|
||||
const ExchangeRateWrapper = styled.div`
|
||||
${({ theme }) => theme.flexRowNoWrap};
|
||||
align-items: center;
|
||||
color: ${({ theme }) => theme.doveGray};
|
||||
font-size: 0.75rem;
|
||||
padding: 0.5rem 1rem;
|
||||
`
|
||||
|
||||
const ExchangeRate = styled.span`
|
||||
flex: 1 1 auto;
|
||||
width: 0;
|
||||
color: ${({ theme }) => theme.doveGray};
|
||||
`
|
||||
|
||||
const Flex = styled.div`
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: 2rem;
|
||||
|
||||
button {
|
||||
max-width: 20rem;
|
||||
}
|
||||
`
|
||||
|
||||
function calculateSlippageBounds(value, token = false, tokenAllowedSlippage, allowedSlippage) {
|
||||
if (value) {
|
||||
const offset = value.mul(token ? tokenAllowedSlippage : allowedSlippage).div(ethers.utils.bigNumberify(10000))
|
||||
const minimum = value.sub(offset)
|
||||
const maximum = value.add(offset)
|
||||
return {
|
||||
minimum: minimum.lt(ethers.constants.Zero) ? ethers.constants.Zero : minimum,
|
||||
maximum: maximum.gt(ethers.constants.MaxUint256) ? ethers.constants.MaxUint256 : maximum
|
||||
}
|
||||
} else {
|
||||
return {}
|
||||
}
|
||||
}
|
||||
|
||||
function getSwapType(inputCurrency, outputCurrency) {
|
||||
if (!inputCurrency || !outputCurrency) {
|
||||
return null
|
||||
} else if (inputCurrency === 'ETH') {
|
||||
return ETH_TO_TOKEN
|
||||
} else if (outputCurrency === 'ETH') {
|
||||
return TOKEN_TO_ETH
|
||||
} else {
|
||||
return TOKEN_TO_TOKEN
|
||||
}
|
||||
}
|
||||
|
||||
// this mocks the getInputPrice function, and calculates the required output
|
||||
function calculateEtherTokenOutputFromInput(inputAmount, inputReserve, outputReserve) {
|
||||
const inputAmountWithFee = inputAmount.mul(ethers.utils.bigNumberify(997))
|
||||
const numerator = inputAmountWithFee.mul(outputReserve)
|
||||
const denominator = inputReserve.mul(ethers.utils.bigNumberify(1000)).add(inputAmountWithFee)
|
||||
return numerator.div(denominator)
|
||||
}
|
||||
|
||||
// this mocks the getOutputPrice function, and calculates the required input
|
||||
function calculateEtherTokenInputFromOutput(outputAmount, inputReserve, outputReserve) {
|
||||
const numerator = inputReserve.mul(outputAmount).mul(ethers.utils.bigNumberify(1000))
|
||||
const denominator = outputReserve.sub(outputAmount).mul(ethers.utils.bigNumberify(997))
|
||||
return numerator.div(denominator).add(ethers.constants.One)
|
||||
}
|
||||
|
||||
function getInitialSwapState(state) {
|
||||
return {
|
||||
independentValue: state.exactFieldURL && state.exactAmountURL ? state.exactAmountURL : '', // this is a user input
|
||||
dependentValue: '', // this is a calculated number
|
||||
independentField: state.exactFieldURL === 'output' ? OUTPUT : INPUT,
|
||||
inputCurrency: state.inputCurrencyURL ? state.inputCurrencyURL : 'ETH',
|
||||
outputCurrency: state.outputCurrencyURL
|
||||
? state.outputCurrencyURL === 'ETH'
|
||||
? state.inputCurrencyURL && state.inputCurrencyURL !== 'ETH'
|
||||
? 'ETH'
|
||||
: ''
|
||||
: state.outputCurrencyURL
|
||||
: state.initialCurrency
|
||||
? state.initialCurrency
|
||||
: ''
|
||||
}
|
||||
}
|
||||
|
||||
function swapStateReducer(state, action) {
|
||||
switch (action.type) {
|
||||
case 'FLIP_INDEPENDENT': {
|
||||
const { independentField, inputCurrency, outputCurrency } = state
|
||||
return {
|
||||
...state,
|
||||
dependentValue: '',
|
||||
independentField: independentField === INPUT ? OUTPUT : INPUT,
|
||||
inputCurrency: outputCurrency,
|
||||
outputCurrency: inputCurrency
|
||||
}
|
||||
}
|
||||
case 'SELECT_CURRENCY': {
|
||||
const { inputCurrency, outputCurrency } = state
|
||||
const { field, currency } = action.payload
|
||||
|
||||
const newInputCurrency = field === INPUT ? currency : inputCurrency
|
||||
const newOutputCurrency = field === OUTPUT ? currency : outputCurrency
|
||||
|
||||
if (newInputCurrency === newOutputCurrency) {
|
||||
return {
|
||||
...state,
|
||||
inputCurrency: field === INPUT ? currency : '',
|
||||
outputCurrency: field === OUTPUT ? currency : ''
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
...state,
|
||||
inputCurrency: newInputCurrency,
|
||||
outputCurrency: newOutputCurrency
|
||||
}
|
||||
}
|
||||
}
|
||||
case 'UPDATE_INDEPENDENT': {
|
||||
const { field, value } = action.payload
|
||||
const { dependentValue, independentValue } = state
|
||||
return {
|
||||
...state,
|
||||
independentValue: value,
|
||||
dependentValue: value === independentValue ? dependentValue : '',
|
||||
independentField: field
|
||||
}
|
||||
}
|
||||
case 'UPDATE_DEPENDENT': {
|
||||
return {
|
||||
...state,
|
||||
dependentValue: action.payload
|
||||
}
|
||||
}
|
||||
default: {
|
||||
return getInitialSwapState()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getExchangeRate(inputValue, inputDecimals, outputValue, outputDecimals, invert = false) {
|
||||
try {
|
||||
if (
|
||||
inputValue &&
|
||||
(inputDecimals || inputDecimals === 0) &&
|
||||
outputValue &&
|
||||
(outputDecimals || outputDecimals === 0)
|
||||
) {
|
||||
const factor = ethers.utils.bigNumberify(10).pow(ethers.utils.bigNumberify(18))
|
||||
|
||||
if (invert) {
|
||||
return inputValue
|
||||
.mul(factor)
|
||||
.mul(ethers.utils.bigNumberify(10).pow(ethers.utils.bigNumberify(outputDecimals)))
|
||||
.div(ethers.utils.bigNumberify(10).pow(ethers.utils.bigNumberify(inputDecimals)))
|
||||
.div(outputValue)
|
||||
} else {
|
||||
return outputValue
|
||||
.mul(factor)
|
||||
.mul(ethers.utils.bigNumberify(10).pow(ethers.utils.bigNumberify(inputDecimals)))
|
||||
.div(ethers.utils.bigNumberify(10).pow(ethers.utils.bigNumberify(outputDecimals)))
|
||||
.div(inputValue)
|
||||
}
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
|
||||
function getMarketRate(
|
||||
swapType,
|
||||
inputReserveETH,
|
||||
inputReserveToken,
|
||||
inputDecimals,
|
||||
outputReserveETH,
|
||||
outputReserveToken,
|
||||
outputDecimals,
|
||||
invert = false
|
||||
) {
|
||||
if (swapType === ETH_TO_TOKEN) {
|
||||
return getExchangeRate(outputReserveETH, 18, outputReserveToken, outputDecimals, invert)
|
||||
} else if (swapType === TOKEN_TO_ETH) {
|
||||
return getExchangeRate(inputReserveToken, inputDecimals, inputReserveETH, 18, invert)
|
||||
} else if (swapType === TOKEN_TO_TOKEN) {
|
||||
const factor = ethers.utils.bigNumberify(10).pow(ethers.utils.bigNumberify(18))
|
||||
const firstRate = getExchangeRate(inputReserveToken, inputDecimals, inputReserveETH, 18)
|
||||
const secondRate = getExchangeRate(outputReserveETH, 18, outputReserveToken, outputDecimals)
|
||||
try {
|
||||
return !!(firstRate && secondRate) ? firstRate.mul(secondRate).div(factor) : undefined
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
|
||||
export default function ExchangePage({ initialCurrency, sending = false, params }) {
|
||||
const { t } = useTranslation()
|
||||
const { account, chainId, error } = useWeb3React()
|
||||
|
||||
const urlAddedTokens = {}
|
||||
if (params.inputCurrency) {
|
||||
urlAddedTokens[params.inputCurrency] = true
|
||||
}
|
||||
if (params.outputCurrency) {
|
||||
urlAddedTokens[params.outputCurrency] = true
|
||||
}
|
||||
if (isAddress(initialCurrency)) {
|
||||
urlAddedTokens[initialCurrency] = true
|
||||
}
|
||||
|
||||
const addTransaction = useTransactionAdder()
|
||||
|
||||
// check if URL specifies valid slippage, if so use as default
|
||||
const initialSlippage = (token = false) => {
|
||||
let slippage = Number.parseInt(params.slippage)
|
||||
if (!isNaN(slippage) && (slippage === 0 || slippage >= 1)) {
|
||||
return slippage // round to match custom input availability
|
||||
}
|
||||
// check for token <-> token slippage option
|
||||
return token ? TOKEN_ALLOWED_SLIPPAGE_DEFAULT : ALLOWED_SLIPPAGE_DEFAULT
|
||||
}
|
||||
|
||||
// check URL params for recipient, only on send page
|
||||
const initialRecipient = () => {
|
||||
if (sending && params.recipient) {
|
||||
return params.recipient
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
const [brokenTokenWarning, setBrokenTokenWarning] = useState()
|
||||
|
||||
const [deadlineFromNow, setDeadlineFromNow] = useState(DEFAULT_DEADLINE_FROM_NOW)
|
||||
|
||||
const [rawSlippage, setRawSlippage] = useState(() => initialSlippage())
|
||||
const [rawTokenSlippage, setRawTokenSlippage] = useState(() => initialSlippage(true))
|
||||
|
||||
const allowedSlippageBig = ethers.utils.bigNumberify(rawSlippage)
|
||||
const tokenAllowedSlippageBig = ethers.utils.bigNumberify(rawTokenSlippage)
|
||||
|
||||
// analytics
|
||||
useEffect(() => {
|
||||
ReactGA.pageview(window.location.pathname + window.location.search)
|
||||
}, [])
|
||||
|
||||
// core swap state
|
||||
const [swapState, dispatchSwapState] = useReducer(
|
||||
swapStateReducer,
|
||||
{
|
||||
initialCurrency: initialCurrency,
|
||||
inputCurrencyURL: params.inputCurrency,
|
||||
outputCurrencyURL: params.outputCurrency,
|
||||
exactFieldURL: params.exactField,
|
||||
exactAmountURL: params.exactAmount
|
||||
},
|
||||
getInitialSwapState
|
||||
)
|
||||
|
||||
const { independentValue, dependentValue, independentField, inputCurrency, outputCurrency } = swapState
|
||||
|
||||
useEffect(() => {
|
||||
setBrokenTokenWarning(false)
|
||||
for (let i = 0; i < brokenTokens.length; i++) {
|
||||
if (
|
||||
brokenTokens[i].toLowerCase() === outputCurrency.toLowerCase() ||
|
||||
brokenTokens[i].toLowerCase() === inputCurrency.toLowerCase()
|
||||
) {
|
||||
setBrokenTokenWarning(true)
|
||||
}
|
||||
}
|
||||
}, [outputCurrency, inputCurrency])
|
||||
|
||||
const [recipient, setRecipient] = useState({
|
||||
address: initialRecipient(),
|
||||
name: ''
|
||||
})
|
||||
const [recipientError, setRecipientError] = useState()
|
||||
|
||||
// get swap type from the currency types
|
||||
const swapType = getSwapType(inputCurrency, outputCurrency)
|
||||
|
||||
// get decimals and exchange address for each of the currency types
|
||||
const { symbol: inputSymbol, decimals: inputDecimals, exchangeAddress: inputExchangeAddress } = useTokenDetails(
|
||||
inputCurrency
|
||||
)
|
||||
const { symbol: outputSymbol, decimals: outputDecimals, exchangeAddress: outputExchangeAddress } = useTokenDetails(
|
||||
outputCurrency
|
||||
)
|
||||
|
||||
const inputExchangeContract = useExchangeContract(inputExchangeAddress)
|
||||
const outputExchangeContract = useExchangeContract(outputExchangeAddress)
|
||||
const contract = swapType === ETH_TO_TOKEN ? outputExchangeContract : inputExchangeContract
|
||||
|
||||
// get input allowance
|
||||
const inputAllowance = useAddressAllowance(account, inputCurrency, inputExchangeAddress)
|
||||
|
||||
// fetch reserves for each of the currency types
|
||||
const { reserveETH: inputReserveETH, reserveToken: inputReserveToken } = useExchangeReserves(inputCurrency)
|
||||
const { reserveETH: outputReserveETH, reserveToken: outputReserveToken } = useExchangeReserves(outputCurrency)
|
||||
|
||||
// get balances for each of the currency types
|
||||
const inputBalance = useAddressBalance(account, inputCurrency)
|
||||
const outputBalance = useAddressBalance(account, outputCurrency)
|
||||
const inputBalanceFormatted = !!(inputBalance && Number.isInteger(inputDecimals))
|
||||
? amountFormatter(inputBalance, inputDecimals, Math.min(4, inputDecimals))
|
||||
: ''
|
||||
const outputBalanceFormatted = !!(outputBalance && Number.isInteger(outputDecimals))
|
||||
? amountFormatter(outputBalance, outputDecimals, Math.min(4, outputDecimals))
|
||||
: ''
|
||||
|
||||
// compute useful transforms of the data above
|
||||
const independentDecimals = independentField === INPUT ? inputDecimals : outputDecimals
|
||||
const dependentDecimals = independentField === OUTPUT ? inputDecimals : outputDecimals
|
||||
|
||||
// declare/get parsed and formatted versions of input/output values
|
||||
const [independentValueParsed, setIndependentValueParsed] = useState()
|
||||
const dependentValueFormatted = !!(dependentValue && (dependentDecimals || dependentDecimals === 0))
|
||||
? amountFormatter(dependentValue, dependentDecimals, Math.min(4, dependentDecimals), false)
|
||||
: ''
|
||||
const inputValueParsed = independentField === INPUT ? independentValueParsed : dependentValue
|
||||
const inputValueFormatted = independentField === INPUT ? independentValue : dependentValueFormatted
|
||||
const outputValueParsed = independentField === OUTPUT ? independentValueParsed : dependentValue
|
||||
const outputValueFormatted = independentField === OUTPUT ? independentValue : dependentValueFormatted
|
||||
|
||||
// validate + parse independent value
|
||||
const [independentError, setIndependentError] = useState()
|
||||
useEffect(() => {
|
||||
if (independentValue && (independentDecimals || independentDecimals === 0)) {
|
||||
try {
|
||||
const parsedValue = ethers.utils.parseUnits(independentValue, independentDecimals)
|
||||
|
||||
if (parsedValue.lte(ethers.constants.Zero) || parsedValue.gte(ethers.constants.MaxUint256)) {
|
||||
throw Error()
|
||||
} else {
|
||||
setIndependentValueParsed(parsedValue)
|
||||
setIndependentError(null)
|
||||
}
|
||||
} catch {
|
||||
setIndependentError(t('inputNotValid'))
|
||||
}
|
||||
|
||||
return () => {
|
||||
setIndependentValueParsed()
|
||||
setIndependentError()
|
||||
}
|
||||
}
|
||||
}, [independentValue, independentDecimals, t])
|
||||
|
||||
// calculate slippage from target rate
|
||||
const { minimum: dependentValueMinumum, maximum: dependentValueMaximum } = calculateSlippageBounds(
|
||||
dependentValue,
|
||||
swapType === TOKEN_TO_TOKEN,
|
||||
tokenAllowedSlippageBig,
|
||||
allowedSlippageBig
|
||||
)
|
||||
|
||||
// validate input allowance + balance
|
||||
const [inputError, setInputError] = useState()
|
||||
const [showUnlock, setShowUnlock] = useState(false)
|
||||
useEffect(() => {
|
||||
const inputValueCalculation = independentField === INPUT ? independentValueParsed : dependentValueMaximum
|
||||
if (inputBalance && (inputAllowance || inputCurrency === 'ETH') && inputValueCalculation) {
|
||||
if (inputBalance.lt(inputValueCalculation)) {
|
||||
setInputError(t('insufficientBalance'))
|
||||
} else if (inputCurrency !== 'ETH' && inputAllowance.lt(inputValueCalculation)) {
|
||||
setInputError(t('unlockTokenCont'))
|
||||
setShowUnlock(true)
|
||||
} else {
|
||||
setInputError(null)
|
||||
setShowUnlock(false)
|
||||
}
|
||||
return () => {
|
||||
setInputError()
|
||||
setShowUnlock(false)
|
||||
}
|
||||
}
|
||||
}, [independentField, independentValueParsed, dependentValueMaximum, inputBalance, inputCurrency, inputAllowance, t])
|
||||
|
||||
// calculate dependent value
|
||||
useEffect(() => {
|
||||
const amount = independentValueParsed
|
||||
|
||||
if (swapType === ETH_TO_TOKEN) {
|
||||
const reserveETH = outputReserveETH
|
||||
const reserveToken = outputReserveToken
|
||||
|
||||
if (amount && reserveETH && reserveToken) {
|
||||
try {
|
||||
const calculatedDependentValue =
|
||||
independentField === INPUT
|
||||
? calculateEtherTokenOutputFromInput(amount, reserveETH, reserveToken)
|
||||
: calculateEtherTokenInputFromOutput(amount, reserveETH, reserveToken)
|
||||
|
||||
if (calculatedDependentValue.lte(ethers.constants.Zero)) {
|
||||
throw Error()
|
||||
}
|
||||
|
||||
dispatchSwapState({
|
||||
type: 'UPDATE_DEPENDENT',
|
||||
payload: calculatedDependentValue
|
||||
})
|
||||
} catch {
|
||||
setIndependentError(t('insufficientLiquidity'))
|
||||
}
|
||||
return () => {
|
||||
dispatchSwapState({ type: 'UPDATE_DEPENDENT', payload: '' })
|
||||
}
|
||||
}
|
||||
} else if (swapType === TOKEN_TO_ETH) {
|
||||
const reserveETH = inputReserveETH
|
||||
const reserveToken = inputReserveToken
|
||||
|
||||
if (amount && reserveETH && reserveToken) {
|
||||
try {
|
||||
const calculatedDependentValue =
|
||||
independentField === INPUT
|
||||
? calculateEtherTokenOutputFromInput(amount, reserveToken, reserveETH)
|
||||
: calculateEtherTokenInputFromOutput(amount, reserveToken, reserveETH)
|
||||
|
||||
if (calculatedDependentValue.lte(ethers.constants.Zero)) {
|
||||
throw Error()
|
||||
}
|
||||
|
||||
dispatchSwapState({
|
||||
type: 'UPDATE_DEPENDENT',
|
||||
payload: calculatedDependentValue
|
||||
})
|
||||
} catch {
|
||||
setIndependentError(t('insufficientLiquidity'))
|
||||
}
|
||||
return () => {
|
||||
dispatchSwapState({ type: 'UPDATE_DEPENDENT', payload: '' })
|
||||
}
|
||||
}
|
||||
} else if (swapType === TOKEN_TO_TOKEN) {
|
||||
const reserveETHFirst = inputReserveETH
|
||||
const reserveTokenFirst = inputReserveToken
|
||||
|
||||
const reserveETHSecond = outputReserveETH
|
||||
const reserveTokenSecond = outputReserveToken
|
||||
|
||||
if (amount && reserveETHFirst && reserveTokenFirst && reserveETHSecond && reserveTokenSecond) {
|
||||
try {
|
||||
if (independentField === INPUT) {
|
||||
const intermediateValue = calculateEtherTokenOutputFromInput(amount, reserveTokenFirst, reserveETHFirst)
|
||||
if (intermediateValue.lte(ethers.constants.Zero)) {
|
||||
throw Error()
|
||||
}
|
||||
const calculatedDependentValue = calculateEtherTokenOutputFromInput(
|
||||
intermediateValue,
|
||||
reserveETHSecond,
|
||||
reserveTokenSecond
|
||||
)
|
||||
if (calculatedDependentValue.lte(ethers.constants.Zero)) {
|
||||
throw Error()
|
||||
}
|
||||
dispatchSwapState({
|
||||
type: 'UPDATE_DEPENDENT',
|
||||
payload: calculatedDependentValue
|
||||
})
|
||||
} else {
|
||||
const intermediateValue = calculateEtherTokenInputFromOutput(amount, reserveETHSecond, reserveTokenSecond)
|
||||
if (intermediateValue.lte(ethers.constants.Zero)) {
|
||||
throw Error()
|
||||
}
|
||||
const calculatedDependentValue = calculateEtherTokenInputFromOutput(
|
||||
intermediateValue,
|
||||
reserveTokenFirst,
|
||||
reserveETHFirst
|
||||
)
|
||||
if (calculatedDependentValue.lte(ethers.constants.Zero)) {
|
||||
throw Error()
|
||||
}
|
||||
dispatchSwapState({
|
||||
type: 'UPDATE_DEPENDENT',
|
||||
payload: calculatedDependentValue
|
||||
})
|
||||
}
|
||||
} catch {
|
||||
setIndependentError(t('insufficientLiquidity'))
|
||||
}
|
||||
return () => {
|
||||
dispatchSwapState({ type: 'UPDATE_DEPENDENT', payload: '' })
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [
|
||||
independentValueParsed,
|
||||
swapType,
|
||||
outputReserveETH,
|
||||
outputReserveToken,
|
||||
inputReserveETH,
|
||||
inputReserveToken,
|
||||
independentField,
|
||||
t
|
||||
])
|
||||
|
||||
useEffect(() => {
|
||||
const history = createBrowserHistory()
|
||||
history.push(window.location.pathname + '')
|
||||
}, [])
|
||||
|
||||
const [inverted, setInverted] = useState(false)
|
||||
const exchangeRate = getExchangeRate(inputValueParsed, inputDecimals, outputValueParsed, outputDecimals)
|
||||
const exchangeRateInverted = getExchangeRate(inputValueParsed, inputDecimals, outputValueParsed, outputDecimals, true)
|
||||
|
||||
const marketRate = getMarketRate(
|
||||
swapType,
|
||||
inputReserveETH,
|
||||
inputReserveToken,
|
||||
inputDecimals,
|
||||
outputReserveETH,
|
||||
outputReserveToken,
|
||||
outputDecimals
|
||||
)
|
||||
|
||||
const percentSlippage =
|
||||
exchangeRate && marketRate && !marketRate.isZero()
|
||||
? exchangeRate
|
||||
.sub(marketRate)
|
||||
.abs()
|
||||
.mul(ethers.utils.bigNumberify(10).pow(ethers.utils.bigNumberify(18)))
|
||||
.div(marketRate)
|
||||
.sub(ethers.utils.bigNumberify(3).mul(ethers.utils.bigNumberify(10).pow(ethers.utils.bigNumberify(15))))
|
||||
: undefined
|
||||
const percentSlippageFormatted = percentSlippage && amountFormatter(percentSlippage, 16, 2)
|
||||
const slippageWarning =
|
||||
percentSlippage &&
|
||||
percentSlippage.gte(ethers.utils.parseEther('.05')) &&
|
||||
percentSlippage.lt(ethers.utils.parseEther('.2')) // [5% - 20%)
|
||||
const highSlippageWarning = percentSlippage && percentSlippage.gte(ethers.utils.parseEther('.2')) // [20+%
|
||||
|
||||
const isValid = sending
|
||||
? exchangeRate && inputError === null && independentError === null && recipientError === null && deadlineFromNow
|
||||
: exchangeRate && inputError === null && independentError === null && deadlineFromNow
|
||||
|
||||
const estimatedText = `(${t('estimated')})`
|
||||
function formatBalance(value) {
|
||||
return `Balance: ${value}`
|
||||
}
|
||||
|
||||
async function onSwap() {
|
||||
//if user changed deadline, log new one in minutes
|
||||
if (deadlineFromNow !== DEFAULT_DEADLINE_FROM_NOW) {
|
||||
ReactGA.event({
|
||||
category: 'Advanced Interaction',
|
||||
action: 'Set Custom Deadline',
|
||||
value: deadlineFromNow / 60
|
||||
})
|
||||
}
|
||||
|
||||
const deadline = Math.ceil(Date.now() / 1000) + deadlineFromNow
|
||||
|
||||
// if user has changed slippage, log
|
||||
if (swapType === TOKEN_TO_TOKEN) {
|
||||
if (parseInt(tokenAllowedSlippageBig.toString()) !== TOKEN_ALLOWED_SLIPPAGE_DEFAULT) {
|
||||
ReactGA.event({
|
||||
category: 'Advanced Interaction',
|
||||
action: 'Set Custom Slippage',
|
||||
value: parseInt(tokenAllowedSlippageBig.toString())
|
||||
})
|
||||
}
|
||||
} else {
|
||||
if (parseInt(allowedSlippageBig.toString()) !== ALLOWED_SLIPPAGE_DEFAULT) {
|
||||
ReactGA.event({
|
||||
category: 'Advanced Interaction',
|
||||
action: 'Set Custom Slippage',
|
||||
value: parseInt(allowedSlippageBig.toString())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
let estimate, method, args, value
|
||||
|
||||
let inputEthPerToken = 1
|
||||
if (inputCurrency !== 'ETH') {
|
||||
inputEthPerToken = inputReserveToken && inputReserveETH ? inputReserveETH / inputReserveToken : null
|
||||
}
|
||||
let ethTransactionSize = inputEthPerToken * inputValueFormatted
|
||||
|
||||
// params for GA event
|
||||
let action = ''
|
||||
let label = ''
|
||||
|
||||
if (independentField === INPUT) {
|
||||
// set GA params
|
||||
action = sending ? 'SendInput' : 'SwapInput'
|
||||
label = outputCurrency
|
||||
|
||||
if (swapType === ETH_TO_TOKEN) {
|
||||
estimate = sending ? contract.estimate.ethToTokenTransferInput : contract.estimate.ethToTokenSwapInput
|
||||
method = sending ? contract.ethToTokenTransferInput : contract.ethToTokenSwapInput
|
||||
args = sending ? [dependentValueMinumum, deadline, recipient.address] : [dependentValueMinumum, deadline]
|
||||
value = independentValueParsed
|
||||
} else if (swapType === TOKEN_TO_ETH) {
|
||||
estimate = sending ? contract.estimate.tokenToEthTransferInput : contract.estimate.tokenToEthSwapInput
|
||||
method = sending ? contract.tokenToEthTransferInput : contract.tokenToEthSwapInput
|
||||
args = sending
|
||||
? [independentValueParsed, dependentValueMinumum, deadline, recipient.address]
|
||||
: [independentValueParsed, dependentValueMinumum, deadline]
|
||||
value = ethers.constants.Zero
|
||||
} else if (swapType === TOKEN_TO_TOKEN) {
|
||||
estimate = sending ? contract.estimate.tokenToTokenTransferInput : contract.estimate.tokenToTokenSwapInput
|
||||
method = sending ? contract.tokenToTokenTransferInput : contract.tokenToTokenSwapInput
|
||||
args = sending
|
||||
? [
|
||||
independentValueParsed,
|
||||
dependentValueMinumum,
|
||||
ethers.constants.One,
|
||||
deadline,
|
||||
recipient.address,
|
||||
outputCurrency
|
||||
]
|
||||
: [independentValueParsed, dependentValueMinumum, ethers.constants.One, deadline, outputCurrency]
|
||||
value = ethers.constants.Zero
|
||||
}
|
||||
} else if (independentField === OUTPUT) {
|
||||
// set GA params
|
||||
action = sending ? 'SendOutput' : 'SwapOutput'
|
||||
label = outputCurrency
|
||||
|
||||
if (swapType === ETH_TO_TOKEN) {
|
||||
estimate = sending ? contract.estimate.ethToTokenTransferOutput : contract.estimate.ethToTokenSwapOutput
|
||||
method = sending ? contract.ethToTokenTransferOutput : contract.ethToTokenSwapOutput
|
||||
args = sending ? [independentValueParsed, deadline, recipient.address] : [independentValueParsed, deadline]
|
||||
value = dependentValueMaximum
|
||||
} else if (swapType === TOKEN_TO_ETH) {
|
||||
estimate = sending ? contract.estimate.tokenToEthTransferOutput : contract.estimate.tokenToEthSwapOutput
|
||||
method = sending ? contract.tokenToEthTransferOutput : contract.tokenToEthSwapOutput
|
||||
args = sending
|
||||
? [independentValueParsed, dependentValueMaximum, deadline, recipient.address]
|
||||
: [independentValueParsed, dependentValueMaximum, deadline]
|
||||
value = ethers.constants.Zero
|
||||
} else if (swapType === TOKEN_TO_TOKEN) {
|
||||
estimate = sending ? contract.estimate.tokenToTokenTransferOutput : contract.estimate.tokenToTokenSwapOutput
|
||||
method = sending ? contract.tokenToTokenTransferOutput : contract.tokenToTokenSwapOutput
|
||||
args = sending
|
||||
? [
|
||||
independentValueParsed,
|
||||
dependentValueMaximum,
|
||||
ethers.constants.MaxUint256,
|
||||
deadline,
|
||||
recipient.address,
|
||||
outputCurrency
|
||||
]
|
||||
: [independentValueParsed, dependentValueMaximum, ethers.constants.MaxUint256, deadline, outputCurrency]
|
||||
value = ethers.constants.Zero
|
||||
}
|
||||
}
|
||||
|
||||
const estimatedGasLimit = await estimate(...args, { value })
|
||||
method(...args, {
|
||||
value,
|
||||
gasLimit: calculateGasMargin(estimatedGasLimit, GAS_MARGIN)
|
||||
}).then(response => {
|
||||
addTransaction(response)
|
||||
ReactGA.event({
|
||||
category: 'Transaction',
|
||||
action: action,
|
||||
label: label,
|
||||
value: ethTransactionSize,
|
||||
dimension1: response.hash
|
||||
})
|
||||
ReactGA.event({
|
||||
category: 'Hash',
|
||||
action: response.hash,
|
||||
label: ethTransactionSize.toString()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const [customSlippageError, setcustomSlippageError] = useState('')
|
||||
|
||||
const toggleWalletModal = useWalletModalToggle()
|
||||
|
||||
const newInputDetected =
|
||||
inputCurrency !== 'ETH' && inputCurrency && !INITIAL_TOKENS_CONTEXT[chainId].hasOwnProperty(inputCurrency)
|
||||
|
||||
const newOutputDetected =
|
||||
outputCurrency !== 'ETH' && outputCurrency && !INITIAL_TOKENS_CONTEXT[chainId].hasOwnProperty(outputCurrency)
|
||||
|
||||
const [showInputWarning, setShowInputWarning] = useState(false)
|
||||
const [showOutputWarning, setShowOutputWarning] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
if (newInputDetected) {
|
||||
setShowInputWarning(true)
|
||||
} else {
|
||||
setShowInputWarning(false)
|
||||
}
|
||||
}, [newInputDetected, setShowInputWarning])
|
||||
|
||||
useEffect(() => {
|
||||
if (newOutputDetected) {
|
||||
setShowOutputWarning(true)
|
||||
} else {
|
||||
setShowOutputWarning(false)
|
||||
}
|
||||
}, [newOutputDetected, setShowOutputWarning])
|
||||
|
||||
return (
|
||||
<>
|
||||
{showInputWarning && (
|
||||
<WarningCard
|
||||
onDismiss={() => {
|
||||
setShowInputWarning(false)
|
||||
}}
|
||||
urlAddedTokens={urlAddedTokens}
|
||||
currency={inputCurrency}
|
||||
/>
|
||||
)}
|
||||
{showOutputWarning && (
|
||||
<WarningCard
|
||||
onDismiss={() => {
|
||||
setShowOutputWarning(false)
|
||||
}}
|
||||
urlAddedTokens={urlAddedTokens}
|
||||
currency={outputCurrency}
|
||||
/>
|
||||
)}
|
||||
<CurrencyInputPanel
|
||||
title={t('input')}
|
||||
urlAddedTokens={urlAddedTokens}
|
||||
description={inputValueFormatted && independentField === OUTPUT ? estimatedText : ''}
|
||||
extraText={inputBalanceFormatted && formatBalance(inputBalanceFormatted)}
|
||||
extraTextClickHander={() => {
|
||||
if (inputBalance && inputDecimals) {
|
||||
const valueToSet = inputCurrency === 'ETH' ? inputBalance.sub(ethers.utils.parseEther('.1')) : inputBalance
|
||||
if (valueToSet.gt(ethers.constants.Zero)) {
|
||||
dispatchSwapState({
|
||||
type: 'UPDATE_INDEPENDENT',
|
||||
payload: {
|
||||
value: amountFormatter(valueToSet, inputDecimals, inputDecimals, false),
|
||||
field: INPUT
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}}
|
||||
onCurrencySelected={inputCurrency => {
|
||||
dispatchSwapState({
|
||||
type: 'SELECT_CURRENCY',
|
||||
payload: { currency: inputCurrency, field: INPUT }
|
||||
})
|
||||
}}
|
||||
onValueChange={inputValue => {
|
||||
dispatchSwapState({
|
||||
type: 'UPDATE_INDEPENDENT',
|
||||
payload: { value: inputValue, field: INPUT }
|
||||
})
|
||||
}}
|
||||
showUnlock={showUnlock}
|
||||
selectedTokens={[inputCurrency, outputCurrency]}
|
||||
selectedTokenAddress={inputCurrency}
|
||||
value={inputValueFormatted}
|
||||
errorMessage={inputError ? inputError : independentField === INPUT ? independentError : ''}
|
||||
/>
|
||||
<OversizedPanel>
|
||||
<DownArrowBackground>
|
||||
<DownArrow
|
||||
onClick={() => {
|
||||
dispatchSwapState({ type: 'FLIP_INDEPENDENT' })
|
||||
}}
|
||||
clickable
|
||||
alt="swap"
|
||||
active={isValid}
|
||||
/>
|
||||
</DownArrowBackground>
|
||||
</OversizedPanel>
|
||||
<CurrencyInputPanel
|
||||
title={t('output')}
|
||||
description={outputValueFormatted && independentField === INPUT ? estimatedText : ''}
|
||||
extraText={outputBalanceFormatted && formatBalance(outputBalanceFormatted)}
|
||||
urlAddedTokens={urlAddedTokens}
|
||||
onCurrencySelected={outputCurrency => {
|
||||
dispatchSwapState({
|
||||
type: 'SELECT_CURRENCY',
|
||||
payload: { currency: outputCurrency, field: OUTPUT }
|
||||
})
|
||||
}}
|
||||
onValueChange={outputValue => {
|
||||
dispatchSwapState({
|
||||
type: 'UPDATE_INDEPENDENT',
|
||||
payload: { value: outputValue, field: OUTPUT }
|
||||
})
|
||||
}}
|
||||
selectedTokens={[inputCurrency, outputCurrency]}
|
||||
selectedTokenAddress={outputCurrency}
|
||||
value={outputValueFormatted}
|
||||
errorMessage={independentField === OUTPUT ? independentError : ''}
|
||||
disableUnlock
|
||||
/>
|
||||
{sending ? (
|
||||
<>
|
||||
<OversizedPanel>
|
||||
<DownArrowBackground>
|
||||
<DownArrow active={isValid} alt="arrow" />
|
||||
</DownArrowBackground>
|
||||
</OversizedPanel>
|
||||
<AddressInputPanel onChange={setRecipient} onError={setRecipientError} initialInput={recipient} />
|
||||
</>
|
||||
) : (
|
||||
''
|
||||
)}
|
||||
<OversizedPanel hideBottom>
|
||||
<ExchangeRateWrapper
|
||||
onClick={() => {
|
||||
setInverted(inverted => !inverted)
|
||||
}}
|
||||
>
|
||||
<ExchangeRate>{t('exchangeRate')}</ExchangeRate>
|
||||
{inverted ? (
|
||||
<span>
|
||||
{exchangeRate
|
||||
? `1 ${inputSymbol} = ${amountFormatter(exchangeRate, 18, 6, false)} ${outputSymbol}`
|
||||
: ' - '}
|
||||
</span>
|
||||
) : (
|
||||
<span>
|
||||
{exchangeRate
|
||||
? `1 ${outputSymbol} = ${amountFormatter(exchangeRateInverted, 18, 6, false)} ${inputSymbol}`
|
||||
: ' - '}
|
||||
</span>
|
||||
)}
|
||||
</ExchangeRateWrapper>
|
||||
</OversizedPanel>
|
||||
<TransactionDetails
|
||||
account={account}
|
||||
setRawSlippage={setRawSlippage}
|
||||
setRawTokenSlippage={setRawTokenSlippage}
|
||||
rawSlippage={rawSlippage}
|
||||
slippageWarning={slippageWarning}
|
||||
highSlippageWarning={highSlippageWarning}
|
||||
brokenTokenWarning={brokenTokenWarning}
|
||||
setDeadline={setDeadlineFromNow}
|
||||
deadline={deadlineFromNow}
|
||||
inputError={inputError}
|
||||
independentError={independentError}
|
||||
inputCurrency={inputCurrency}
|
||||
outputCurrency={outputCurrency}
|
||||
independentValue={independentValue}
|
||||
independentValueParsed={independentValueParsed}
|
||||
independentField={independentField}
|
||||
INPUT={INPUT}
|
||||
inputValueParsed={inputValueParsed}
|
||||
outputValueParsed={outputValueParsed}
|
||||
inputSymbol={inputSymbol}
|
||||
outputSymbol={outputSymbol}
|
||||
dependentValueMinumum={dependentValueMinumum}
|
||||
dependentValueMaximum={dependentValueMaximum}
|
||||
dependentDecimals={dependentDecimals}
|
||||
independentDecimals={independentDecimals}
|
||||
percentSlippageFormatted={percentSlippageFormatted}
|
||||
setcustomSlippageError={setcustomSlippageError}
|
||||
recipientAddress={recipient.address}
|
||||
sending={sending}
|
||||
/>
|
||||
<Flex>
|
||||
<Button
|
||||
disabled={
|
||||
brokenTokenWarning ? true : !account && !error ? false : !isValid || customSlippageError === 'invalid'
|
||||
}
|
||||
onClick={account && !error ? onSwap : toggleWalletModal}
|
||||
warning={highSlippageWarning || customSlippageError === 'warning'}
|
||||
loggedOut={!account}
|
||||
>
|
||||
{brokenTokenWarning
|
||||
? 'Swap'
|
||||
: !account
|
||||
? 'Connect to a Wallet'
|
||||
: sending
|
||||
? highSlippageWarning || customSlippageError === 'warning'
|
||||
? t('sendAnyway')
|
||||
: t('send')
|
||||
: highSlippageWarning || customSlippageError === 'warning'
|
||||
? t('swapAnyway')
|
||||
: t('swap')}
|
||||
</Button>
|
||||
</Flex>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -1,118 +0,0 @@
|
||||
import React from 'react'
|
||||
import ReactGA from 'react-ga'
|
||||
import styled from 'styled-components'
|
||||
import { darken, transparentize } from 'polished'
|
||||
import Toggle from 'react-switch'
|
||||
|
||||
import { Link } from '../../theme'
|
||||
import { useDarkModeManager } from '../../contexts/LocalStorage'
|
||||
|
||||
const FooterFrame = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
`
|
||||
|
||||
const FooterElement = styled.div`
|
||||
margin: 1.25rem;
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
`
|
||||
|
||||
const Title = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: ${({ theme }) => theme.uniswapPink};
|
||||
|
||||
:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
#link {
|
||||
text-decoration-color: ${({ theme }) => theme.uniswapPink};
|
||||
}
|
||||
|
||||
#title {
|
||||
display: inline;
|
||||
font-size: 0.825rem;
|
||||
margin-right: 12px;
|
||||
font-weight: 400;
|
||||
color: ${({ theme }) => theme.uniswapPink};
|
||||
:hover {
|
||||
color: ${({ theme }) => darken(0.2, theme.uniswapPink)};
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const StyledToggle = styled(Toggle)`
|
||||
margin-right: 24px;
|
||||
|
||||
.react-switch-bg[style] {
|
||||
background-color: ${({ theme }) => darken(0.05, theme.inputBackground)} !important;
|
||||
border: 1px solid ${({ theme }) => theme.concreteGray} !important;
|
||||
}
|
||||
|
||||
.react-switch-handle[style] {
|
||||
background-color: ${({ theme }) => theme.inputBackground};
|
||||
box-shadow: 0 4px 8px 0 ${({ theme }) => transparentize(0.93, theme.shadowColor)};
|
||||
border: 1px solid ${({ theme }) => theme.mercuryGray};
|
||||
border-color: ${({ theme }) => theme.mercuryGray} !important;
|
||||
top: 2px !important;
|
||||
}
|
||||
`
|
||||
|
||||
const EmojiToggle = styled.span`
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
font-family: Arial sans-serif;
|
||||
`
|
||||
|
||||
export default function Footer() {
|
||||
const [isDark, toggleDarkMode] = useDarkModeManager()
|
||||
|
||||
return (
|
||||
<FooterFrame>
|
||||
<FooterElement>
|
||||
<Title>
|
||||
<Link id="link" href="https://uniswap.io/">
|
||||
<h1 id="title">About</h1>
|
||||
</Link>
|
||||
<Link id="link" href="https://docs.uniswap.io/">
|
||||
<h1 id="title">Docs</h1>
|
||||
</Link>
|
||||
<Link id="link" href="https://github.com/Uniswap">
|
||||
<h1 id="title">Code</h1>
|
||||
</Link>
|
||||
</Title>
|
||||
</FooterElement>
|
||||
|
||||
<StyledToggle
|
||||
checked={!isDark}
|
||||
uncheckedIcon={
|
||||
<EmojiToggle role="img" aria-label="moon">
|
||||
{/* eslint-disable-line jsx-a11y/accessible-emoji */}
|
||||
🌙️
|
||||
</EmojiToggle>
|
||||
}
|
||||
checkedIcon={
|
||||
<EmojiToggle role="img" aria-label="sun">
|
||||
{/* eslint-disable-line jsx-a11y/accessible-emoji */}
|
||||
{'☀️'}
|
||||
</EmojiToggle>
|
||||
}
|
||||
onChange={() => {
|
||||
ReactGA.event({
|
||||
category: 'Advanced Interaction',
|
||||
action: 'Toggle Theme',
|
||||
label: isDark ? 'Light' : 'Dark'
|
||||
})
|
||||
toggleDarkMode()
|
||||
}}
|
||||
/>
|
||||
</FooterFrame>
|
||||
)
|
||||
}
|
||||
35
src/components/Footer/index.tsx
Normal file
@@ -0,0 +1,35 @@
|
||||
import React from 'react'
|
||||
import styled from 'styled-components'
|
||||
import { Send, Sun, Moon } from 'react-feather'
|
||||
import { useDarkModeManager } from '../../state/user/hooks'
|
||||
|
||||
import { ButtonSecondary } from '../Button'
|
||||
|
||||
const FooterFrame = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
position: fixed;
|
||||
right: 1rem;
|
||||
bottom: 1rem;
|
||||
${({ theme }) => theme.mediaWidth.upToExtraSmall`
|
||||
display: none;
|
||||
`};
|
||||
`
|
||||
|
||||
export default function Footer() {
|
||||
const [darkMode, toggleDarkMode] = useDarkModeManager()
|
||||
|
||||
return (
|
||||
<FooterFrame>
|
||||
<form action="https://forms.gle/DaLuqvJsVhVaAM3J9" target="_blank">
|
||||
<ButtonSecondary p="8px 12px">
|
||||
<Send size={16} style={{ marginRight: '8px' }} /> Feedback
|
||||
</ButtonSecondary>
|
||||
</form>
|
||||
<ButtonSecondary onClick={toggleDarkMode} p="8px 12px" ml="0.5rem" width="min-content">
|
||||
{darkMode ? <Sun size={16} /> : <Moon size={16} />}
|
||||
</ButtonSecondary>
|
||||
</FooterFrame>
|
||||
)
|
||||
}
|
||||
@@ -1,77 +0,0 @@
|
||||
import React from 'react'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import { Link } from '../../theme'
|
||||
import Web3Status from '../Web3Status'
|
||||
import { darken } from 'polished'
|
||||
|
||||
const HeaderFrame = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
`
|
||||
|
||||
const HeaderElement = styled.div`
|
||||
margin: 1.25rem;
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
`
|
||||
|
||||
const Nod = styled.span`
|
||||
transform: rotate(0deg);
|
||||
transition: transform 150ms ease-out;
|
||||
|
||||
:hover {
|
||||
transform: rotate(-10deg);
|
||||
}
|
||||
`
|
||||
|
||||
const Title = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#link {
|
||||
text-decoration-color: ${({ theme }) => theme.UniswapPink};
|
||||
}
|
||||
|
||||
#title {
|
||||
display: inline;
|
||||
font-size: 1rem;
|
||||
font-weight: 500;
|
||||
color: ${({ theme }) => theme.wisteriaPurple};
|
||||
:hover {
|
||||
color: ${({ theme }) => darken(0.1, theme.wisteriaPurple)};
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export default function Header() {
|
||||
return (
|
||||
<HeaderFrame>
|
||||
<HeaderElement>
|
||||
<Title>
|
||||
<Nod>
|
||||
<Link id="link" href="https://uniswap.io">
|
||||
<span role="img" aria-label="unicorn">
|
||||
🦄{' '}
|
||||
</span>
|
||||
</Link>
|
||||
</Nod>
|
||||
<Link id="link" href="https://uniswap.io">
|
||||
<h1 id="title">Uniswap</h1>
|
||||
</Link>
|
||||
</Title>
|
||||
</HeaderElement>
|
||||
<HeaderElement>
|
||||
<Web3Status />
|
||||
</HeaderElement>
|
||||
</HeaderFrame>
|
||||
)
|
||||
}
|
||||
221
src/components/Header/index.tsx
Normal file
@@ -0,0 +1,221 @@
|
||||
import React from 'react'
|
||||
import { Link as HistoryLink } from 'react-router-dom'
|
||||
|
||||
import styled from 'styled-components'
|
||||
import { useTokenBalanceTreatingWETHasETH } from '../../state/wallet/hooks'
|
||||
|
||||
import Row from '../Row'
|
||||
import Menu from '../Menu'
|
||||
import Web3Status from '../Web3Status'
|
||||
|
||||
import { ExternalLink, StyledInternalLink } from '../../theme'
|
||||
import { Text } from 'rebass'
|
||||
import { WETH, ChainId } from '@uniswap/sdk'
|
||||
import { isMobile } from 'react-device-detect'
|
||||
import { YellowCard } from '../Card'
|
||||
import { useActiveWeb3React } from '../../hooks'
|
||||
import { useDarkModeManager } from '../../state/user/hooks'
|
||||
|
||||
import Logo from '../../assets/svg/logo.svg'
|
||||
import Wordmark from '../../assets/svg/wordmark.svg'
|
||||
import LogoDark from '../../assets/svg/logo_white.svg'
|
||||
import WordmarkDark from '../../assets/svg/wordmark_white.svg'
|
||||
import { AutoColumn } from '../Column'
|
||||
import { RowBetween } from '../Row'
|
||||
|
||||
const HeaderFrame = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
top: 0;
|
||||
position: absolute;
|
||||
|
||||
pointer-events: none;
|
||||
|
||||
${({ theme }) => theme.mediaWidth.upToExtraSmall`
|
||||
padding: 12px 0 0 0;
|
||||
width: calc(100%);
|
||||
position: relative;
|
||||
`};
|
||||
z-index: 2;
|
||||
`
|
||||
|
||||
const HeaderElement = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
`
|
||||
|
||||
const Title = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
pointer-events: auto;
|
||||
|
||||
:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
`
|
||||
|
||||
const TitleText = styled(Row)`
|
||||
width: fit-content;
|
||||
white-space: nowrap;
|
||||
${({ theme }) => theme.mediaWidth.upToExtraSmall`
|
||||
display: none;
|
||||
`};
|
||||
`
|
||||
|
||||
const AccountElement = styled.div<{ active: boolean }>`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
background-color: ${({ theme, active }) => (!active ? theme.bg1 : theme.bg3)};
|
||||
border-radius: 12px;
|
||||
white-space: nowrap;
|
||||
|
||||
:focus {
|
||||
border: 1px solid blue;
|
||||
}
|
||||
`
|
||||
|
||||
const TestnetWrapper = styled.div`
|
||||
white-space: nowrap;
|
||||
width: fit-content;
|
||||
margin-left: 10px;
|
||||
|
||||
${({ theme }) => theme.mediaWidth.upToSmall`
|
||||
display: none;
|
||||
`};
|
||||
`
|
||||
|
||||
const NetworkCard = styled(YellowCard)`
|
||||
width: fit-content;
|
||||
margin-right: 10px;
|
||||
border-radius: 12px;
|
||||
padding: 8px 12px;
|
||||
`
|
||||
|
||||
const UniIcon = styled(HistoryLink)<{ to: string }>`
|
||||
transition: transform 0.3s ease;
|
||||
:hover {
|
||||
transform: rotate(-5deg);
|
||||
}
|
||||
`
|
||||
|
||||
const MigrateBanner = styled(AutoColumn)`
|
||||
width: 100%;
|
||||
padding: 12px 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
background-color: ${({ theme }) => theme.primary5};
|
||||
color: ${({ theme }) => theme.primaryText1};
|
||||
font-weight: 400;
|
||||
text-align: center;
|
||||
pointer-events: auto;
|
||||
a {
|
||||
color: ${({ theme }) => theme.primaryText1};
|
||||
}
|
||||
|
||||
${({ theme }) => theme.mediaWidth.upToSmall`
|
||||
padding: 0;
|
||||
display: none;
|
||||
`};
|
||||
`
|
||||
|
||||
const VersionLabel = styled.span<{ isV2?: boolean }>`
|
||||
padding: ${({ isV2 }) => (isV2 ? '0.15rem 0.5rem 0.16rem 0.45rem' : '0.15rem 0.5rem 0.16rem 0.35rem')};
|
||||
border-radius: 14px;
|
||||
background: ${({ theme, isV2 }) => (isV2 ? theme.primary1 : 'none')};
|
||||
color: ${({ theme, isV2 }) => (isV2 ? theme.white : theme.primary1)};
|
||||
font-size: 0.825rem;
|
||||
font-weight: 400;
|
||||
:hover {
|
||||
user-select: ${({ isV2 }) => (isV2 ? 'none' : 'initial')};
|
||||
background: ${({ theme, isV2 }) => (isV2 ? theme.primary1 : 'none')};
|
||||
color: ${({ theme, isV2 }) => (isV2 ? theme.white : theme.primary3)};
|
||||
}
|
||||
`
|
||||
|
||||
const VersionToggle = styled.a`
|
||||
border-radius: 16px;
|
||||
background: ${({ theme }) => theme.primary5};
|
||||
border: 1px solid ${({ theme }) => theme.primary4};
|
||||
color: ${({ theme }) => theme.primary1};
|
||||
display: flex;
|
||||
width: fit-content;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
`
|
||||
|
||||
export default function Header() {
|
||||
const { account, chainId } = useActiveWeb3React()
|
||||
|
||||
const userEthBalance = useTokenBalanceTreatingWETHasETH(account, WETH[chainId])
|
||||
const [isDark] = useDarkModeManager()
|
||||
|
||||
return (
|
||||
<HeaderFrame>
|
||||
<MigrateBanner>
|
||||
Uniswap V2 is live! Read the
|
||||
<ExternalLink href="https://uniswap.org/blog/launch-uniswap-v2/">
|
||||
<b>blog post ↗</b>
|
||||
</ExternalLink>
|
||||
or
|
||||
<StyledInternalLink to="/migrate/v1">
|
||||
<b>migrate your liquidity ↗</b>
|
||||
</StyledInternalLink>
|
||||
.
|
||||
</MigrateBanner>
|
||||
<RowBetween padding="1rem">
|
||||
<HeaderElement>
|
||||
<Title>
|
||||
<UniIcon id="link" to="/">
|
||||
<img src={isDark ? LogoDark : Logo} alt="logo" />
|
||||
</UniIcon>
|
||||
{!isMobile && (
|
||||
<TitleText>
|
||||
<HistoryLink id="link" to="/">
|
||||
<img
|
||||
style={{ marginLeft: '4px', marginTop: '4px' }}
|
||||
src={isDark ? WordmarkDark : Wordmark}
|
||||
alt="logo"
|
||||
/>
|
||||
</HistoryLink>
|
||||
</TitleText>
|
||||
)}
|
||||
</Title>
|
||||
<TestnetWrapper style={{ pointerEvents: 'auto' }}>
|
||||
{!isMobile && (
|
||||
<VersionToggle target="_self" href="https://v1.uniswap.exchange">
|
||||
<VersionLabel isV2={true}>V2</VersionLabel>
|
||||
<VersionLabel isV2={false}>V1</VersionLabel>
|
||||
</VersionToggle>
|
||||
)}
|
||||
</TestnetWrapper>
|
||||
</HeaderElement>
|
||||
<HeaderElement>
|
||||
<TestnetWrapper>
|
||||
{!isMobile && chainId === ChainId.ROPSTEN && <NetworkCard>Ropsten</NetworkCard>}
|
||||
{!isMobile && chainId === ChainId.RINKEBY && <NetworkCard>Rinkeby</NetworkCard>}
|
||||
{!isMobile && chainId === ChainId.GÖRLI && <NetworkCard>Görli</NetworkCard>}
|
||||
{!isMobile && chainId === ChainId.KOVAN && <NetworkCard>Kovan</NetworkCard>}
|
||||
</TestnetWrapper>
|
||||
<AccountElement active={!!account} style={{ pointerEvents: 'auto' }}>
|
||||
{account && userEthBalance ? (
|
||||
<Text style={{ flexShrink: 0 }} pl="0.75rem" pr="0.5rem" fontWeight={500}>
|
||||
{userEthBalance?.toSignificant(4)} ETH
|
||||
</Text>
|
||||
) : null}
|
||||
<Web3Status />
|
||||
</AccountElement>
|
||||
<div style={{ pointerEvents: 'auto' }}>
|
||||
<Menu />
|
||||
</div>
|
||||
</HeaderElement>
|
||||
</RowBetween>
|
||||
</HeaderFrame>
|
||||
)
|
||||
}
|
||||
@@ -2,20 +2,20 @@ import React, { useEffect, useRef } from 'react'
|
||||
|
||||
import styled from 'styled-components'
|
||||
|
||||
import { useWeb3React } from '../../hooks'
|
||||
import { useActiveWeb3React } from '../../hooks'
|
||||
import Jazzicon from 'jazzicon'
|
||||
|
||||
const StyledIdenticon = styled.div`
|
||||
height: 1rem;
|
||||
width: 1rem;
|
||||
border-radius: 1.125rem;
|
||||
background-color: ${({ theme }) => theme.silverGray};
|
||||
background-color: ${({ theme }) => theme.bg4};
|
||||
`
|
||||
|
||||
export default function Identicon() {
|
||||
const ref = useRef()
|
||||
const ref = useRef<HTMLDivElement>()
|
||||
|
||||
const { account } = useWeb3React()
|
||||
const { account } = useActiveWeb3React()
|
||||
|
||||
useEffect(() => {
|
||||
if (account && ref.current) {
|
||||
38
src/components/Loader/index.tsx
Normal file
@@ -0,0 +1,38 @@
|
||||
import React from 'react'
|
||||
|
||||
import styled, { keyframes } from 'styled-components'
|
||||
|
||||
const rotate = keyframes`
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
`
|
||||
|
||||
const StyledSVG = styled.svg<{ size: string; stroke?: string }>`
|
||||
animation: 2s ${rotate} linear infinite;
|
||||
height: ${({ size }) => size};
|
||||
width: ${({ size }) => size};
|
||||
path {
|
||||
stroke: ${({ stroke, theme }) => stroke ?? theme.primary1};
|
||||
}
|
||||
`
|
||||
|
||||
/**
|
||||
* 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 }) {
|
||||
return (
|
||||
<StyledSVG viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" size={size} stroke={stroke} {...rest}>
|
||||
<path
|
||||
d="M12 2C6.47715 2 2 6.47715 2 12C2 17.5228 6.47715 22 12 22C17.5228 22 22 17.5228 22 12C22 9.27455 20.9097 6.80375 19.1414 5"
|
||||
strokeWidth="2.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</StyledSVG>
|
||||
)
|
||||
}
|
||||
138
src/components/Menu/index.tsx
Normal file
@@ -0,0 +1,138 @@
|
||||
import React, { useRef, useEffect } 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 useToggle from '../../hooks/useToggle'
|
||||
|
||||
import { ExternalLink } from '../../theme'
|
||||
|
||||
const StyledMenuIcon = styled(MenuIcon)`
|
||||
path {
|
||||
stroke: ${({ theme }) => theme.text1};
|
||||
}
|
||||
`
|
||||
|
||||
const StyledMenuButton = styled.button`
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: none;
|
||||
background-color: transparent;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
height: 35px;
|
||||
background-color: ${({ theme }) => theme.bg3};
|
||||
|
||||
padding: 0.15rem 0.5rem;
|
||||
border-radius: 0.5rem;
|
||||
|
||||
:hover,
|
||||
:focus {
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
background-color: ${({ theme }) => theme.bg4};
|
||||
}
|
||||
|
||||
svg {
|
||||
margin-top: 2px;
|
||||
}
|
||||
`
|
||||
|
||||
const StyledMenu = styled.div`
|
||||
margin-left: 0.5rem;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
border: none;
|
||||
text-align: left;
|
||||
`
|
||||
|
||||
const MenuFlyout = styled.span`
|
||||
min-width: 8.125rem;
|
||||
background-color: ${({ 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);
|
||||
border-radius: 0.5rem;
|
||||
padding: 0.5rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
font-size: 1rem;
|
||||
position: absolute;
|
||||
top: 3rem;
|
||||
right: 0rem;
|
||||
z-index: 100;
|
||||
`
|
||||
|
||||
const MenuItem = styled(ExternalLink)`
|
||||
flex: 1;
|
||||
padding: 0.5rem 0.5rem;
|
||||
color: ${({ theme }) => theme.text2};
|
||||
:hover {
|
||||
color: ${({ theme }) => theme.text1};
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
}
|
||||
> svg {
|
||||
margin-right: 8px;
|
||||
}
|
||||
`
|
||||
|
||||
const CODE_LINK = !!process.env.REACT_APP_GIT_COMMIT_HASH
|
||||
? `https://github.com/Uniswap/uniswap-frontend/tree/${process.env.REACT_APP_GIT_COMMIT_HASH}`
|
||||
: 'https://github.com/Uniswap/uniswap-frontend'
|
||||
|
||||
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])
|
||||
|
||||
return (
|
||||
<StyledMenu ref={node}>
|
||||
<StyledMenuButton onClick={toggle}>
|
||||
<StyledMenuIcon />
|
||||
</StyledMenuButton>
|
||||
{open && (
|
||||
<MenuFlyout>
|
||||
<MenuItem id="link" href="https://uniswap.org/">
|
||||
<Info size={14} />
|
||||
About
|
||||
</MenuItem>
|
||||
<MenuItem id="link" href="https://uniswap.org/docs/v2">
|
||||
<BookOpen size={14} />
|
||||
Docs
|
||||
</MenuItem>
|
||||
<MenuItem id="link" href={CODE_LINK}>
|
||||
<Code size={14} />
|
||||
Code
|
||||
</MenuItem>
|
||||
<MenuItem id="link" href="https://discord.gg/vXCdddD">
|
||||
<MessageCircle size={14} />
|
||||
Discord
|
||||
</MenuItem>
|
||||
<MenuItem id="link" href="https://uniswap.info/">
|
||||
<PieChart size={14} />
|
||||
Analytics
|
||||
</MenuItem>
|
||||
</MenuFlyout>
|
||||
)}
|
||||
</StyledMenu>
|
||||
)
|
||||
}
|
||||
@@ -1,191 +0,0 @@
|
||||
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'
|
||||
import { transparentize } from 'polished'
|
||||
import { useGesture } from 'react-use-gesture'
|
||||
|
||||
const AnimatedDialogOverlay = animated(DialogOverlay)
|
||||
const WrappedDialogOverlay = ({ suppressClassNameWarning, mobile, ...rest }) => <AnimatedDialogOverlay {...rest} />
|
||||
const StyledDialogOverlay = styled(WrappedDialogOverlay).attrs({
|
||||
suppressClassNameWarning: true
|
||||
})`
|
||||
&[data-reach-dialog-overlay] {
|
||||
z-index: 2;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: ${({ theme }) => 'transparent'};
|
||||
|
||||
${({ mobile }) =>
|
||||
mobile &&
|
||||
css`
|
||||
align-items: flex-end;
|
||||
`}
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
background-color: ${({ theme }) => theme.modalBackground};
|
||||
opacity: 0.5;
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
/* position: absolute; */
|
||||
position: fixed;
|
||||
z-index: -1;
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const FilteredDialogContent = ({ minHeight, maxHeight, isOpen, slideInAnimation, mobile, ...rest }) => (
|
||||
<DialogContent {...rest} />
|
||||
)
|
||||
const StyledDialogContent = styled(FilteredDialogContent)`
|
||||
&[data-reach-dialog-content] {
|
||||
margin: 0 0 2rem 0;
|
||||
border: 1px solid ${({ theme }) => theme.concreteGray};
|
||||
background-color: ${({ theme }) => theme.inputBackground};
|
||||
box-shadow: 0 4px 8px 0 ${({ theme }) => transparentize(0.95, theme.shadowColor)};
|
||||
padding: 0px;
|
||||
width: 50vw;
|
||||
|
||||
max-width: 650px;
|
||||
${({ maxHeight }) =>
|
||||
maxHeight &&
|
||||
css`
|
||||
max-height: ${maxHeight}vh;
|
||||
`}
|
||||
${({ minHeight }) =>
|
||||
minHeight &&
|
||||
css`
|
||||
min-height: ${minHeight}vh;
|
||||
`}
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
border-radius: 10px;
|
||||
${({ theme }) => theme.mediaWidth.upToMedium`
|
||||
width: 65vw;
|
||||
max-height: 65vh;
|
||||
margin: 0;
|
||||
`}
|
||||
${({ theme, mobile, isOpen }) => theme.mediaWidth.upToSmall`
|
||||
width: 85vw;
|
||||
max-height: 66vh;
|
||||
${mobile &&
|
||||
css`
|
||||
width: 100vw;
|
||||
border-radius: 20px;
|
||||
border-bottom-left-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
`}
|
||||
`}
|
||||
}
|
||||
`
|
||||
|
||||
const HiddenCloseButton = styled.button`
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border: none;
|
||||
`
|
||||
|
||||
export default function Modal({ isOpen, onDismiss, minHeight = false, maxHeight = 50, initialFocusRef, children }) {
|
||||
const transitions = useTransition(isOpen, null, {
|
||||
config: { duration: 200 },
|
||||
from: { opacity: 0 },
|
||||
enter: { opacity: 1 },
|
||||
leave: { opacity: 0 }
|
||||
})
|
||||
|
||||
const [{ xy }, set] = useSpring(() => ({ xy: [0, 0] }))
|
||||
const bind = useGesture({
|
||||
onDrag: state => {
|
||||
let velocity = state.velocity
|
||||
if (velocity < 1) {
|
||||
velocity = 1
|
||||
}
|
||||
if (velocity > 8) {
|
||||
velocity = 8
|
||||
}
|
||||
set({
|
||||
xy: state.down ? state.movement : [0, 0],
|
||||
config: { mass: 1, tension: 210, friction: 20 }
|
||||
})
|
||||
if (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={isMobile}
|
||||
>
|
||||
<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: xy.interpolate((x, y) => `translate3d(${0}px,${y > 0 ? y : 0}px,0)`) }}
|
||||
>
|
||||
<StyledDialogContent
|
||||
style={props}
|
||||
hidden={true}
|
||||
minHeight={minHeight}
|
||||
maxHeight={maxHeight}
|
||||
mobile={isMobile}
|
||||
>
|
||||
<HiddenCloseButton onClick={onDismiss} />
|
||||
{children}
|
||||
</StyledDialogContent>
|
||||
</animated.div>
|
||||
)}
|
||||
</Spring>
|
||||
</StyledDialogOverlay>
|
||||
)
|
||||
)
|
||||
} else {
|
||||
return transitions.map(
|
||||
({ item, key, props }) =>
|
||||
item && (
|
||||
<StyledDialogOverlay
|
||||
key={key}
|
||||
style={props}
|
||||
onDismiss={onDismiss}
|
||||
initialFocusRef={initialFocusRef}
|
||||
mobile={isMobile}
|
||||
>
|
||||
<StyledDialogContent
|
||||
hidden={true}
|
||||
minHeight={minHeight}
|
||||
maxHeight={maxHeight}
|
||||
isOpen={isOpen}
|
||||
mobile={isMobile}
|
||||
>
|
||||
<HiddenCloseButton onClick={onDismiss} />
|
||||
{children}
|
||||
</StyledDialogContent>
|
||||
</StyledDialogOverlay>
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
205
src/components/Modal/index.tsx
Normal file
@@ -0,0 +1,205 @@
|
||||
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'
|
||||
import { transparentize } from 'polished'
|
||||
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 }>`
|
||||
&[data-reach-dialog-overlay] {
|
||||
z-index: 2;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: transparent;
|
||||
|
||||
${({ mobile }) =>
|
||||
mobile &&
|
||||
css`
|
||||
align-items: flex-end;
|
||||
`}
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
background-color: ${({ theme }) => theme.modalBG};
|
||||
opacity: 0.5;
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
position: fixed;
|
||||
z-index: -1;
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
// 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} />
|
||||
)).attrs({
|
||||
'aria-label': 'dialog'
|
||||
})`
|
||||
&[data-reach-dialog-content] {
|
||||
margin: 0 0 2rem 0;
|
||||
border: 1px solid ${({ theme }) => theme.bg1};
|
||||
background-color: ${({ theme }) => theme.bg1};
|
||||
box-shadow: 0 4px 8px 0 ${({ theme }) => transparentize(0.95, theme.shadow1)};
|
||||
padding: 0px;
|
||||
width: 50vw;
|
||||
|
||||
max-width: 420px;
|
||||
${({ maxHeight }) =>
|
||||
maxHeight &&
|
||||
css`
|
||||
max-height: ${maxHeight}vh;
|
||||
`}
|
||||
${({ minHeight }) =>
|
||||
minHeight &&
|
||||
css`
|
||||
min-height: ${minHeight}vh;
|
||||
`}
|
||||
display: flex;
|
||||
border-radius: 20px;
|
||||
${({ theme }) => theme.mediaWidth.upToMedium`
|
||||
width: 65vw;
|
||||
max-height: 65vh;
|
||||
margin: 0;
|
||||
`}
|
||||
${({ theme, mobile }) => theme.mediaWidth.upToSmall`
|
||||
width: 85vw;
|
||||
max-height: 66vh;
|
||||
${mobile &&
|
||||
css`
|
||||
width: 100vw;
|
||||
border-radius: 20px;
|
||||
border-bottom-left-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
`}
|
||||
`}
|
||||
}
|
||||
`
|
||||
|
||||
const HiddenCloseButton = styled.button`
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border: none;
|
||||
`
|
||||
|
||||
interface ModalProps {
|
||||
isOpen: boolean
|
||||
onDismiss: () => void
|
||||
minHeight?: number | false
|
||||
maxHeight?: number
|
||||
initialFocusRef?: React.RefObject<any>
|
||||
children?: React.ReactNode
|
||||
}
|
||||
|
||||
export default function Modal({
|
||||
isOpen,
|
||||
onDismiss,
|
||||
minHeight = false,
|
||||
maxHeight = 50,
|
||||
initialFocusRef = null,
|
||||
children
|
||||
}: ModalProps) {
|
||||
const transitions = useTransition(isOpen, null, {
|
||||
config: { duration: 200 },
|
||||
from: { opacity: 0 },
|
||||
enter: { opacity: 1 },
|
||||
leave: { opacity: 0 }
|
||||
})
|
||||
|
||||
const [{ xy }, set] = useSpring(() => ({ xy: [0, 0] }))
|
||||
const bind = useGesture({
|
||||
onDrag: state => {
|
||||
let velocity = state.velocity
|
||||
if (velocity < 1) {
|
||||
velocity = 1
|
||||
}
|
||||
if (velocity > 8) {
|
||||
velocity = 8
|
||||
}
|
||||
set({
|
||||
xy: state.down ? state.movement : [0, 0],
|
||||
config: { mass: 1, tension: 210, friction: 20 }
|
||||
})
|
||||
if (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}
|
||||
>
|
||||
<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: (xy as any).interpolate((x, y) => `translate3d(${0}px,${y > 0 ? y : 0}px,0)`)
|
||||
}}
|
||||
>
|
||||
<StyledDialogContent
|
||||
ariaLabel="test"
|
||||
style={props}
|
||||
hidden={true}
|
||||
minHeight={minHeight}
|
||||
maxHeight={maxHeight}
|
||||
mobile={isMobile ?? undefined}
|
||||
>
|
||||
<HiddenCloseButton onClick={onDismiss} />
|
||||
{children}
|
||||
</StyledDialogContent>
|
||||
</animated.div>
|
||||
)}
|
||||
</Spring>
|
||||
</StyledDialogOverlay>
|
||||
)
|
||||
)}
|
||||
</>
|
||||
)
|
||||
} else {
|
||||
return (
|
||||
<>
|
||||
{transitions.map(
|
||||
({ item, key, props }) =>
|
||||
item && (
|
||||
<StyledDialogOverlay key={key} style={props} onDismiss={onDismiss} initialFocusRef={initialFocusRef}>
|
||||
<StyledDialogContent hidden={true} minHeight={minHeight} maxHeight={maxHeight} isOpen={isOpen}>
|
||||
<HiddenCloseButton onClick={onDismiss} />
|
||||
{children}
|
||||
</StyledDialogContent>
|
||||
</StyledDialogOverlay>
|
||||
)
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,250 +0,0 @@
|
||||
import React, { useCallback } from 'react'
|
||||
import { withRouter, NavLink } from 'react-router-dom'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
import { transparentize, darken } from 'polished'
|
||||
|
||||
import { useWeb3React, useBodyKeyDown } from '../../hooks'
|
||||
import { useAddressBalance } from '../../contexts/Balances'
|
||||
import { isAddress } from '../../utils'
|
||||
import {
|
||||
useBetaMessageManager,
|
||||
useSaiHolderMessageManager,
|
||||
useGeneralDaiMessageManager
|
||||
} from '../../contexts/LocalStorage'
|
||||
import { Link } from '../../theme/components'
|
||||
|
||||
const tabOrder = [
|
||||
{
|
||||
path: '/swap',
|
||||
textKey: 'swap',
|
||||
regex: /\/swap/
|
||||
},
|
||||
{
|
||||
path: '/send',
|
||||
textKey: 'send',
|
||||
regex: /\/send/
|
||||
},
|
||||
{
|
||||
path: '/add-liquidity',
|
||||
textKey: 'pool',
|
||||
regex: /\/add-liquidity|\/remove-liquidity|\/create-exchange.*/
|
||||
}
|
||||
]
|
||||
|
||||
const BetaMessage = styled.div`
|
||||
${({ theme }) => theme.flexRowNoWrap}
|
||||
cursor: pointer;
|
||||
flex: 1 0 auto;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
padding: 0.5rem 1rem;
|
||||
padding-right: 2rem;
|
||||
margin-bottom: 1rem;
|
||||
border: 1px solid ${({ theme }) => transparentize(0.6, theme.wisteriaPurple)};
|
||||
background-color: ${({ theme }) => transparentize(0.9, theme.wisteriaPurple)};
|
||||
border-radius: 1rem;
|
||||
font-size: 0.75rem;
|
||||
line-height: 1rem;
|
||||
text-align: left;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
color: ${({ theme }) => theme.wisteriaPurple};
|
||||
|
||||
&:after {
|
||||
content: '✕';
|
||||
top: 0.5rem;
|
||||
right: 1rem;
|
||||
position: absolute;
|
||||
color: ${({ theme }) => theme.wisteriaPurple};
|
||||
}
|
||||
`
|
||||
|
||||
const DaiMessage = styled(BetaMessage)`
|
||||
${({ theme }) => theme.flexColumnNoWrap}
|
||||
position: relative;
|
||||
word-wrap: wrap;
|
||||
overflow: visible;
|
||||
white-space: normal;
|
||||
padding: 1rem 1rem;
|
||||
padding-right: 2rem;
|
||||
line-height: 1.2rem;
|
||||
cursor: default;
|
||||
color: ${({ theme }) => theme.textColor};
|
||||
div {
|
||||
width: 100%;
|
||||
}
|
||||
&:after {
|
||||
content: '';
|
||||
}
|
||||
`
|
||||
|
||||
const CloseIcon = styled.div`
|
||||
width: 10px !important;
|
||||
top: 0.5rem;
|
||||
right: 1rem;
|
||||
position: absolute;
|
||||
color: ${({ theme }) => theme.wisteriaPurple};
|
||||
:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
`
|
||||
|
||||
const WarningHeader = styled.div`
|
||||
margin-bottom: 10px;
|
||||
font-weight: 500;
|
||||
color: ${({ theme }) => theme.uniswapPink};
|
||||
`
|
||||
|
||||
const WarningFooter = styled.div`
|
||||
margin-top: 10px;
|
||||
font-size: 10px;
|
||||
text-decoration: italic;
|
||||
color: ${({ theme }) => theme.greyText};
|
||||
`
|
||||
|
||||
const Tabs = styled.div`
|
||||
${({ theme }) => theme.flexRowNoWrap}
|
||||
align-items: center;
|
||||
height: 2.5rem;
|
||||
background-color: ${({ theme }) => theme.concreteGray};
|
||||
border-radius: 3rem;
|
||||
/* border: 1px solid ${({ theme }) => theme.mercuryGray}; */
|
||||
margin-bottom: 1rem;
|
||||
`
|
||||
|
||||
const activeClassName = 'ACTIVE'
|
||||
|
||||
const StyledNavLink = styled(NavLink).attrs({
|
||||
activeClassName
|
||||
})`
|
||||
${({ theme }) => theme.flexRowNoWrap}
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 2.5rem;
|
||||
border: 1px solid ${({ theme }) => transparentize(1, theme.mercuryGray)};
|
||||
flex: 1 0 auto;
|
||||
border-radius: 3rem;
|
||||
outline: none;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
color: ${({ theme }) => theme.doveGray};
|
||||
font-size: 1rem;
|
||||
box-sizing: border-box;
|
||||
|
||||
&.${activeClassName} {
|
||||
background-color: ${({ theme }) => theme.inputBackground};
|
||||
border-radius: 3rem;
|
||||
border: 1px solid ${({ theme }) => theme.mercuryGray};
|
||||
box-shadow: 0 4px 8px 0 ${({ theme }) => transparentize(0.95, theme.shadowColor)};
|
||||
box-sizing: border-box;
|
||||
font-weight: 500;
|
||||
color: ${({ theme }) => theme.royalBlue};
|
||||
:hover {
|
||||
/* border: 1px solid ${({ theme }) => darken(0.1, theme.mercuryGray)}; */
|
||||
background-color: ${({ theme }) => darken(0.01, theme.inputBackground)};
|
||||
}
|
||||
}
|
||||
|
||||
:hover,
|
||||
:focus {
|
||||
color: ${({ theme }) => darken(0.1, theme.royalBlue)};
|
||||
}
|
||||
`
|
||||
|
||||
function NavigationTabs({ location: { pathname }, history }) {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const [showBetaMessage, dismissBetaMessage] = useBetaMessageManager()
|
||||
|
||||
const [showGeneralDaiMessage, dismissGeneralDaiMessage] = useGeneralDaiMessageManager()
|
||||
|
||||
const [showSaiHolderMessage, dismissSaiHolderMessage] = useSaiHolderMessageManager()
|
||||
|
||||
const { account } = useWeb3React()
|
||||
|
||||
const daiBalance = useAddressBalance(account, isAddress('0x89d24A6b4CcB1B6fAA2625fE562bDD9a23260359'))
|
||||
|
||||
const daiPoolTokenBalance = useAddressBalance(account, isAddress('0x09cabEC1eAd1c0Ba254B09efb3EE13841712bE14'))
|
||||
|
||||
const onLiquidityPage = pathname === '/pool' || pathname === '/add-liquidity' || pathname === '/remove-liquidity'
|
||||
|
||||
const navigate = useCallback(
|
||||
direction => {
|
||||
const tabIndex = tabOrder.findIndex(({ regex }) => pathname.match(regex))
|
||||
history.push(tabOrder[(tabIndex + tabOrder.length + direction) % tabOrder.length].path)
|
||||
},
|
||||
[pathname, history]
|
||||
)
|
||||
const navigateRight = useCallback(() => {
|
||||
navigate(1)
|
||||
}, [navigate])
|
||||
const navigateLeft = useCallback(() => {
|
||||
navigate(-1)
|
||||
}, [navigate])
|
||||
|
||||
useBodyKeyDown('ArrowRight', navigateRight)
|
||||
useBodyKeyDown('ArrowLeft', navigateLeft)
|
||||
|
||||
const providerMessage =
|
||||
showSaiHolderMessage && daiPoolTokenBalance && !daiPoolTokenBalance.isZero() && onLiquidityPage
|
||||
const generalMessage = showGeneralDaiMessage && daiBalance && !daiBalance.isZero()
|
||||
|
||||
return (
|
||||
<>
|
||||
<Tabs>
|
||||
{tabOrder.map(({ path, textKey, regex }) => (
|
||||
<StyledNavLink key={path} to={path} isActive={(_, { pathname }) => pathname.match(regex)}>
|
||||
{t(textKey)}
|
||||
</StyledNavLink>
|
||||
))}
|
||||
</Tabs>
|
||||
{providerMessage && (
|
||||
<DaiMessage>
|
||||
<CloseIcon onClick={dismissSaiHolderMessage}>✕</CloseIcon>
|
||||
<WarningHeader>Missing your DAI?</WarningHeader>
|
||||
<div>
|
||||
Don’t worry, check the{' '}
|
||||
<Link href={'/remove-liquidity?poolTokenAddress=0x89d24A6b4CcB1B6fAA2625fE562bDD9a23260359'}>
|
||||
SAI liquidity pool.
|
||||
</Link>{' '}
|
||||
Your old DAI is now SAI. If you want to migrate,{' '}
|
||||
<Link href="/remove-liquidity?poolTokenAddress=0x89d24A6b4CcB1B6fAA2625fE562bDD9a23260359">
|
||||
remove your SAI liquidity,
|
||||
</Link>{' '}
|
||||
migrate using the <Link href="https://migrate.makerdao.com/">migration tool</Link> then add your migrated
|
||||
DAI to the{' '}
|
||||
<Link href="add-liquidity?token=0x6B175474E89094C44Da98b954EedeAC495271d0F">new DAI liquidity pool.</Link>
|
||||
</div>
|
||||
<WarningFooter>
|
||||
<Link href="https://blog.makerdao.com/looking-ahead-how-to-upgrade-to-multi-collateral-dai/">
|
||||
Read more
|
||||
</Link>{' '}
|
||||
about this change on the official Maker blog.
|
||||
</WarningFooter>
|
||||
</DaiMessage>
|
||||
)}
|
||||
{generalMessage && !providerMessage && (
|
||||
<DaiMessage>
|
||||
<CloseIcon onClick={dismissGeneralDaiMessage}>✕</CloseIcon>
|
||||
<WarningHeader>DAI has upgraded!</WarningHeader>
|
||||
<div>
|
||||
Your old DAI is now SAI. To upgrade use the{' '}
|
||||
<Link href="https://migrate.makerdao.com/">migration tool.</Link>
|
||||
</div>
|
||||
</DaiMessage>
|
||||
)}
|
||||
{showBetaMessage && (
|
||||
<BetaMessage onClick={dismissBetaMessage}>
|
||||
<span role="img" aria-label="warning">
|
||||
💀
|
||||
</span>{' '}
|
||||
{t('betaWarning')}
|
||||
</BetaMessage>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default withRouter(NavigationTabs)
|
||||
156
src/components/NavigationTabs/index.tsx
Normal file
@@ -0,0 +1,156 @@
|
||||
import React, { useCallback } from 'react'
|
||||
import styled from 'styled-components'
|
||||
import { darken } from 'polished'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { withRouter, NavLink, Link as HistoryLink, RouteComponentProps } from 'react-router-dom'
|
||||
import useBodyKeyDown from '../../hooks/useBodyKeyDown'
|
||||
|
||||
import { CursorPointer } from '../../theme'
|
||||
import { ArrowLeft } from 'react-feather'
|
||||
import { RowBetween } from '../Row'
|
||||
import QuestionHelper from '../QuestionHelper'
|
||||
|
||||
const tabOrder = [
|
||||
{
|
||||
path: '/swap',
|
||||
textKey: 'swap',
|
||||
regex: /\/swap/
|
||||
},
|
||||
{
|
||||
path: '/send',
|
||||
textKey: 'send',
|
||||
regex: /\/send/
|
||||
},
|
||||
{
|
||||
path: '/pool',
|
||||
textKey: 'pool',
|
||||
regex: /\/pool/
|
||||
}
|
||||
]
|
||||
|
||||
const Tabs = styled.div`
|
||||
${({ theme }) => theme.flexRowNoWrap}
|
||||
align-items: center;
|
||||
border-radius: 3rem;
|
||||
`
|
||||
|
||||
const activeClassName = 'ACTIVE'
|
||||
|
||||
const StyledNavLink = styled(NavLink).attrs({
|
||||
activeClassName
|
||||
})`
|
||||
${({ theme }) => theme.flexRowNoWrap}
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 3rem;
|
||||
flex: 1 0 auto;
|
||||
border-radius: 3rem;
|
||||
outline: none;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
color: ${({ theme }) => theme.text3};
|
||||
font-size: 20px;
|
||||
|
||||
&.${activeClassName} {
|
||||
border-radius: 12px;
|
||||
font-weight: 500;
|
||||
color: ${({ theme }) => theme.text1};
|
||||
}
|
||||
|
||||
:hover,
|
||||
:focus {
|
||||
color: ${({ theme }) => darken(0.1, theme.text1)};
|
||||
}
|
||||
`
|
||||
|
||||
const ActiveText = styled.div`
|
||||
font-weight: 500;
|
||||
font-size: 20px;
|
||||
`
|
||||
|
||||
const ArrowLink = styled(ArrowLeft)`
|
||||
color: ${({ theme }) => theme.text1};
|
||||
`
|
||||
|
||||
function NavigationTabs({ location: { pathname }, history }: RouteComponentProps<{}>) {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const navigate = useCallback(
|
||||
direction => {
|
||||
const tabIndex = tabOrder.findIndex(({ regex }) => pathname.match(regex))
|
||||
history.push(tabOrder[(tabIndex + tabOrder.length + direction) % tabOrder.length].path)
|
||||
},
|
||||
[pathname, history]
|
||||
)
|
||||
const navigateRight = useCallback(() => {
|
||||
navigate(1)
|
||||
}, [navigate])
|
||||
const navigateLeft = useCallback(() => {
|
||||
navigate(-1)
|
||||
}, [navigate])
|
||||
|
||||
useBodyKeyDown('ArrowRight', navigateRight)
|
||||
useBodyKeyDown('ArrowLeft', navigateLeft)
|
||||
|
||||
const adding = pathname.match('/add')
|
||||
const removing = pathname.match('/remove')
|
||||
const finding = pathname.match('/find')
|
||||
const creating = pathname.match('/create')
|
||||
|
||||
return (
|
||||
<>
|
||||
{adding || removing ? (
|
||||
<Tabs>
|
||||
<RowBetween style={{ padding: '1rem' }}>
|
||||
<CursorPointer onClick={() => history.push('/pool')}>
|
||||
<ArrowLink />
|
||||
</CursorPointer>
|
||||
<ActiveText>{adding ? 'Add' : 'Remove'} Liquidity</ActiveText>
|
||||
<QuestionHelper
|
||||
text={
|
||||
adding
|
||||
? 'When you add liquidity, you are given pool tokens representing your position. These tokens automatically earn fees proportional to your share of the pool, and can be redeemed at any time.'
|
||||
: 'Removing pool tokens converts your position back into underlying tokens at the current rate, proportional to your share of the pool. Accrued fees are included in the amounts you receive.'
|
||||
}
|
||||
/>
|
||||
</RowBetween>
|
||||
</Tabs>
|
||||
) : finding ? (
|
||||
<Tabs>
|
||||
<RowBetween style={{ padding: '1rem' }}>
|
||||
<HistoryLink to="/pool">
|
||||
<ArrowLink />
|
||||
</HistoryLink>
|
||||
<ActiveText>Import Pool</ActiveText>
|
||||
<QuestionHelper text={"Use this tool to find pairs that don't automatically appear in the interface."} />
|
||||
</RowBetween>
|
||||
</Tabs>
|
||||
) : creating ? (
|
||||
<Tabs>
|
||||
<RowBetween style={{ padding: '1rem' }}>
|
||||
<HistoryLink to="/pool">
|
||||
<ArrowLink />
|
||||
</HistoryLink>
|
||||
<ActiveText>Create Pool</ActiveText>
|
||||
<QuestionHelper text={'Use this interface to create a new pool.'} />
|
||||
</RowBetween>
|
||||
</Tabs>
|
||||
) : (
|
||||
<Tabs style={{ marginBottom: '20px' }}>
|
||||
{tabOrder.map(({ path, textKey, regex }) => (
|
||||
<StyledNavLink
|
||||
id={`${textKey}-nav-link`}
|
||||
key={path}
|
||||
to={path}
|
||||
isActive={(_, { pathname }) => !!pathname.match(regex)}
|
||||
>
|
||||
{t(textKey)}
|
||||
</StyledNavLink>
|
||||
))}
|
||||
</Tabs>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default withRouter(NavigationTabs)
|
||||
86
src/components/NumericalInput/index.tsx
Normal file
@@ -0,0 +1,86 @@
|
||||
import React from 'react'
|
||||
import styled from 'styled-components'
|
||||
import { escapeRegExp } from '../../utils'
|
||||
|
||||
const StyledInput = styled.input<{ error?: boolean; fontSize?: string; align?: string }>`
|
||||
color: ${({ error, theme }) => (error ? theme.red1 : theme.text1)};
|
||||
width: 0;
|
||||
position: relative;
|
||||
font-weight: 500;
|
||||
outline: none;
|
||||
border: none;
|
||||
flex: 1 1 auto;
|
||||
background-color: ${({ theme }) => theme.bg1};
|
||||
font-size: ${({ fontSize }) => fontSize ?? '24px'};
|
||||
text-align: ${({ align }) => align && align};
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
padding: 0px;
|
||||
-webkit-appearance: textfield;
|
||||
|
||||
::-webkit-search-decoration {
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
[type='number'] {
|
||||
-moz-appearance: textfield;
|
||||
}
|
||||
|
||||
::-webkit-outer-spin-button,
|
||||
::-webkit-inner-spin-button {
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
::placeholder {
|
||||
color: ${({ theme }) => theme.text4};
|
||||
}
|
||||
`
|
||||
|
||||
const inputRegex = RegExp(`^\\d*(?:\\\\[.])?\\d*$`) // match escaped "." characters via in a non-capturing group
|
||||
|
||||
export const Input = React.memo(function InnerInput({
|
||||
value,
|
||||
onUserInput,
|
||||
placeholder,
|
||||
...rest
|
||||
}: {
|
||||
value: string | number
|
||||
onUserInput: (string) => void
|
||||
error?: boolean
|
||||
fontSize?: string
|
||||
align?: 'right' | 'left'
|
||||
} & Omit<React.HTMLProps<HTMLInputElement>, 'ref' | 'onChange' | 'as'>) {
|
||||
const enforcer = (nextUserInput: string) => {
|
||||
if (nextUserInput === '' || inputRegex.test(escapeRegExp(nextUserInput))) {
|
||||
onUserInput(nextUserInput)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<StyledInput
|
||||
{...rest}
|
||||
value={value}
|
||||
onChange={event => {
|
||||
// replace commas with periods, because uniswap exclusively uses period as the decimal separator
|
||||
enforcer(event.target.value.replace(/,/g, '.'))
|
||||
}}
|
||||
// universal input options
|
||||
inputMode="decimal"
|
||||
title="Token Amount"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
// text-specific options
|
||||
type="text"
|
||||
pattern="^[0-9]*[.,]?[0-9]*$"
|
||||
placeholder={placeholder || '0.0'}
|
||||
minLength={1}
|
||||
maxLength={79}
|
||||
spellCheck="false"
|
||||
/>
|
||||
)
|
||||
})
|
||||
|
||||
export default Input
|
||||
|
||||
// const inputRegex = RegExp(`^\\d*(?:\\\\[.])?\\d*$`) // match escaped "." characters via in a non-capturing group
|
||||
@@ -1,39 +0,0 @@
|
||||
import React from 'react'
|
||||
import styled from 'styled-components'
|
||||
|
||||
const Panel = styled.div`
|
||||
position: relative;
|
||||
background-color: ${({ theme }) => theme.concreteGray};
|
||||
width: calc(100% - 1rem);
|
||||
margin: 0 auto;
|
||||
border-radius: 0.625rem;
|
||||
`
|
||||
|
||||
const PanelTop = styled.div`
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: -0.5rem;
|
||||
left: 0;
|
||||
height: 1rem;
|
||||
width: 100%;
|
||||
background-color: ${({ theme }) => theme.concreteGray};
|
||||
`
|
||||
|
||||
const PanelBottom = styled.div`
|
||||
position: absolute;
|
||||
top: 80%;
|
||||
left: 0;
|
||||
height: 1rem;
|
||||
width: 100%;
|
||||
background-color: ${({ theme }) => theme.concreteGray};
|
||||
`
|
||||
|
||||
export default function OversizedPanel({ hideTop, hideBottom, children }) {
|
||||
return (
|
||||
<Panel>
|
||||
{hideTop || <PanelTop />}
|
||||
{children}
|
||||
{hideBottom || <PanelBottom />}
|
||||
</Panel>
|
||||
)
|
||||
}
|
||||
135
src/components/Popover/index.tsx
Normal file
@@ -0,0 +1,135 @@
|
||||
import { Placement } from '@popperjs/core'
|
||||
import { transparentize } from 'polished'
|
||||
import React, { useState } from 'react'
|
||||
import { usePopper } from 'react-popper'
|
||||
import styled, { keyframes } from 'styled-components'
|
||||
import useInterval from '../../hooks/useInterval'
|
||||
import Portal from '@reach/portal'
|
||||
|
||||
const fadeIn = keyframes`
|
||||
from {
|
||||
opacity : 0;
|
||||
}
|
||||
|
||||
to {
|
||||
opacity : 1;
|
||||
}
|
||||
`
|
||||
|
||||
const fadeOut = keyframes`
|
||||
from {
|
||||
opacity : 1;
|
||||
}
|
||||
|
||||
to {
|
||||
opacity : 0;
|
||||
}
|
||||
`
|
||||
|
||||
const PopoverContainer = styled.div<{ show: boolean }>`
|
||||
z-index: 9999;
|
||||
|
||||
visibility: ${props => (!props.show ? 'hidden' : 'visible')};
|
||||
animation: ${props => (!props.show ? fadeOut : fadeIn)} 150ms linear;
|
||||
transition: visibility 150ms linear;
|
||||
|
||||
background: ${({ theme }) => theme.bg2};
|
||||
border: 1px solid ${({ theme }) => theme.bg3};
|
||||
box-shadow: 0 4px 8px 0 ${({ theme }) => transparentize(0.9, theme.shadow1)};
|
||||
color: ${({ theme }) => theme.text2};
|
||||
border-radius: 8px;
|
||||
`
|
||||
|
||||
const ReferenceElement = styled.div`
|
||||
display: inline-block;
|
||||
`
|
||||
|
||||
const Arrow = styled.div`
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
z-index: 9998;
|
||||
|
||||
::before {
|
||||
position: absolute;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
z-index: 9998;
|
||||
|
||||
content: '';
|
||||
border: 1px solid ${({ theme }) => theme.bg3};
|
||||
transform: rotate(45deg);
|
||||
background: ${({ theme }) => theme.bg2};
|
||||
}
|
||||
|
||||
&.arrow-top {
|
||||
bottom: -5px;
|
||||
::before {
|
||||
border-top: none;
|
||||
border-left: none;
|
||||
}
|
||||
}
|
||||
|
||||
&.arrow-bottom {
|
||||
top: -5px;
|
||||
::before {
|
||||
border-bottom: none;
|
||||
border-right: none;
|
||||
}
|
||||
}
|
||||
|
||||
&.arrow-left {
|
||||
right: -5px;
|
||||
|
||||
::before {
|
||||
border-bottom: none;
|
||||
border-left: none;
|
||||
}
|
||||
}
|
||||
|
||||
&.arrow-right {
|
||||
left: -5px;
|
||||
::before {
|
||||
border-right: none;
|
||||
border-top: none;
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export interface PopoverProps {
|
||||
content: React.ReactNode
|
||||
show: boolean
|
||||
children: React.ReactNode
|
||||
placement?: Placement
|
||||
}
|
||||
|
||||
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 { styles, update, attributes } = usePopper(referenceElement, popperElement, {
|
||||
placement,
|
||||
strategy: 'fixed',
|
||||
modifiers: [
|
||||
{ name: 'offset', options: { offset: [8, 8] } },
|
||||
{ name: 'arrow', options: { element: arrowElement } }
|
||||
]
|
||||
})
|
||||
useInterval(update, show ? 100 : null)
|
||||
|
||||
return (
|
||||
<>
|
||||
<ReferenceElement ref={setReferenceElement}>{children}</ReferenceElement>
|
||||
<Portal>
|
||||
<PopoverContainer show={show} ref={setPopperElement} style={styles.popper} {...attributes.popper}>
|
||||
{content}
|
||||
<Arrow
|
||||
className={`arrow-${attributes.popper?.['data-popper-placement'] ?? ''}`}
|
||||
ref={setArrowElement}
|
||||
style={styles.arrow}
|
||||
{...attributes.arrow}
|
||||
/>
|
||||
</PopoverContainer>
|
||||
</Portal>
|
||||
</>
|
||||
)
|
||||
}
|
||||
166
src/components/Popups/index.tsx
Normal file
@@ -0,0 +1,166 @@
|
||||
import { ChainId, Pair, Token } from '@uniswap/sdk'
|
||||
import React, { useContext, useMemo } from 'react'
|
||||
import styled, { ThemeContext } from 'styled-components'
|
||||
import { useMediaLayout } from 'use-media'
|
||||
|
||||
import { X } from 'react-feather'
|
||||
import { PopupContent } from '../../state/application/actions'
|
||||
import { useActivePopups, useRemovePopup } from '../../state/application/hooks'
|
||||
import { ExternalLink } from '../../theme'
|
||||
import { AutoColumn } from '../Column'
|
||||
import DoubleTokenLogo from '../DoubleLogo'
|
||||
import Row from '../Row'
|
||||
import TxnPopup from '../TxnPopup'
|
||||
import { Text } from 'rebass'
|
||||
|
||||
const StyledClose = styled(X)`
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
top: 10px;
|
||||
|
||||
:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
`
|
||||
|
||||
const MobilePopupWrapper = styled.div<{ height: string | number }>`
|
||||
position: relative;
|
||||
max-width: 100%;
|
||||
height: ${({ height }) => height};
|
||||
margin: ${({ height }) => (height ? '0 auto;' : 0)};
|
||||
margin-bottom: ${({ height }) => (height ? '20px' : 0)}};
|
||||
`
|
||||
|
||||
const MobilePopupInner = styled.div`
|
||||
height: 99%;
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
`
|
||||
|
||||
const FixedPopupColumn = styled(AutoColumn)`
|
||||
position: absolute;
|
||||
top: 112px;
|
||||
right: 1rem;
|
||||
max-width: 355px !important;
|
||||
width: 100%;
|
||||
|
||||
${({ theme }) => theme.mediaWidth.upToSmall`
|
||||
display: none;
|
||||
`};
|
||||
`
|
||||
|
||||
const Popup = styled.div`
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
padding: 1em;
|
||||
background-color: ${({ theme }) => theme.bg1};
|
||||
position: relative;
|
||||
border-radius: 10px;
|
||||
padding: 20px;
|
||||
padding-right: 35px;
|
||||
z-index: 2;
|
||||
overflow: hidden;
|
||||
|
||||
${({ theme }) => theme.mediaWidth.upToSmall`
|
||||
min-width: 290px;
|
||||
`}
|
||||
`
|
||||
|
||||
function PoolPopup({
|
||||
token0,
|
||||
token1
|
||||
}: {
|
||||
token0: { address?: string; symbol?: string }
|
||||
token1: { address?: string; symbol?: string }
|
||||
}) {
|
||||
const pairAddress: string | null = useMemo(() => {
|
||||
if (!token0 || !token1) return null
|
||||
// just mock it out
|
||||
return Pair.getAddress(
|
||||
new Token(ChainId.MAINNET, token0.address, 18),
|
||||
new Token(ChainId.MAINNET, token1.address, 18)
|
||||
)
|
||||
}, [token0, token1])
|
||||
|
||||
return (
|
||||
<AutoColumn gap={'10px'}>
|
||||
<Text fontSize={20} fontWeight={500}>
|
||||
Pool Imported
|
||||
</Text>
|
||||
<Row>
|
||||
<DoubleTokenLogo a0={token0?.address ?? ''} a1={token1?.address ?? ''} margin={true} />
|
||||
<Text fontSize={16} fontWeight={500}>
|
||||
UNI {token0?.symbol} / {token1?.symbol}
|
||||
</Text>
|
||||
</Row>
|
||||
{pairAddress ? (
|
||||
<ExternalLink href={`https://uniswap.info/pair/${pairAddress}`}>View on Uniswap Info.</ExternalLink>
|
||||
) : null}
|
||||
</AutoColumn>
|
||||
)
|
||||
}
|
||||
|
||||
function PopupItem({ content, popKey }: { content: PopupContent; popKey: string }) {
|
||||
if ('txn' in content) {
|
||||
const {
|
||||
txn: { hash, success, summary }
|
||||
} = content
|
||||
return <TxnPopup popKey={popKey} hash={hash} success={success} summary={summary} />
|
||||
} else if ('poolAdded' in content) {
|
||||
const {
|
||||
poolAdded: { token0, token1 }
|
||||
} = content
|
||||
|
||||
return <PoolPopup token0={token0} token1={token1} />
|
||||
}
|
||||
}
|
||||
|
||||
export default function Popups() {
|
||||
const theme = useContext(ThemeContext)
|
||||
// get all popups
|
||||
const activePopups = useActivePopups()
|
||||
const removePopup = useRemovePopup()
|
||||
|
||||
// switch view settings on mobile
|
||||
const isMobile = useMediaLayout({ maxWidth: '600px' })
|
||||
|
||||
if (!isMobile) {
|
||||
return (
|
||||
<FixedPopupColumn gap="20px">
|
||||
{activePopups.map(item => {
|
||||
return (
|
||||
<Popup key={item.key}>
|
||||
<StyledClose color={theme.text2} onClick={() => removePopup(item.key)} />
|
||||
<PopupItem content={item.content} popKey={item.key} />
|
||||
</Popup>
|
||||
)
|
||||
})}
|
||||
</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 => {
|
||||
return (
|
||||
<Popup key={item.key}>
|
||||
<StyledClose color={theme.text2} onClick={() => removePopup(item.key)} />
|
||||
<PopupItem content={item.content} popKey={item.key} />
|
||||
</Popup>
|
||||
)
|
||||
})}
|
||||
</MobilePopupInner>
|
||||
</MobilePopupWrapper>
|
||||
)
|
||||
}
|
||||
236
src/components/PositionCard/index.tsx
Normal file
@@ -0,0 +1,236 @@
|
||||
import React, { useState } from 'react'
|
||||
import styled from 'styled-components'
|
||||
import { darken } from 'polished'
|
||||
import { RouteComponentProps, withRouter } from 'react-router-dom'
|
||||
import { Percent, Pair, JSBI } from '@uniswap/sdk'
|
||||
|
||||
import { useActiveWeb3React } from '../../hooks'
|
||||
import { useTotalSupply } from '../../data/TotalSupply'
|
||||
import { useTokenBalance } from '../../state/wallet/hooks'
|
||||
|
||||
import Card, { GreyCard } from '../Card'
|
||||
import TokenLogo from '../TokenLogo'
|
||||
import DoubleLogo from '../DoubleLogo'
|
||||
import { Text } from 'rebass'
|
||||
import { ExternalLink } from '../../theme/components'
|
||||
import { AutoColumn } from '../Column'
|
||||
import { ChevronDown, ChevronUp } from 'react-feather'
|
||||
import { ButtonSecondary } from '../Button'
|
||||
import { RowBetween, RowFixed, AutoRow } from '../Row'
|
||||
|
||||
const FixedHeightRow = styled(RowBetween)`
|
||||
height: 24px;
|
||||
`
|
||||
|
||||
const HoverCard = styled(Card)`
|
||||
border: 1px solid ${({ theme }) => theme.bg2};
|
||||
:hover {
|
||||
border: 1px solid ${({ theme }) => darken(0.06, theme.bg2)};
|
||||
}
|
||||
`
|
||||
|
||||
interface PositionCardProps extends RouteComponentProps<{}> {
|
||||
pair: Pair
|
||||
minimal?: boolean
|
||||
border?: string
|
||||
}
|
||||
|
||||
function PositionCard({ pair, history, border, minimal = false }: PositionCardProps) {
|
||||
const { account } = useActiveWeb3React()
|
||||
|
||||
const token0 = pair?.token0
|
||||
const token1 = pair?.token1
|
||||
|
||||
const [showMore, setShowMore] = useState(false)
|
||||
|
||||
const userPoolBalance = useTokenBalance(account, pair?.liquidityToken)
|
||||
const totalPoolTokens = useTotalSupply(pair?.liquidityToken)
|
||||
|
||||
const poolTokenPercentage =
|
||||
!!userPoolBalance && !!totalPoolTokens && JSBI.greaterThanOrEqual(totalPoolTokens.raw, userPoolBalance.raw)
|
||||
? new Percent(userPoolBalance.raw, totalPoolTokens.raw)
|
||||
: undefined
|
||||
|
||||
const [token0Deposited, token1Deposited] =
|
||||
!!pair &&
|
||||
!!totalPoolTokens &&
|
||||
!!userPoolBalance &&
|
||||
// this condition is a short-circuit in the case where useTokenBalance updates sooner than useTotalSupply
|
||||
JSBI.greaterThanOrEqual(totalPoolTokens.raw, userPoolBalance.raw)
|
||||
? [
|
||||
pair.getLiquidityValue(token0, totalPoolTokens, userPoolBalance, false),
|
||||
pair.getLiquidityValue(token1, totalPoolTokens, userPoolBalance, false)
|
||||
]
|
||||
: [undefined, undefined]
|
||||
|
||||
if (minimal) {
|
||||
return (
|
||||
<>
|
||||
{userPoolBalance && (
|
||||
<GreyCard border={border}>
|
||||
<AutoColumn gap="12px">
|
||||
<FixedHeightRow>
|
||||
<RowFixed>
|
||||
<Text fontWeight={500} fontSize={16}>
|
||||
Your current position
|
||||
</Text>
|
||||
</RowFixed>
|
||||
</FixedHeightRow>
|
||||
<FixedHeightRow onClick={() => setShowMore(!showMore)}>
|
||||
<RowFixed>
|
||||
<DoubleLogo a0={token0?.address || ''} a1={token1?.address || ''} margin={true} size={20} />
|
||||
<Text fontWeight={500} fontSize={20}>
|
||||
{token0?.symbol}/{token1?.symbol}
|
||||
</Text>
|
||||
</RowFixed>
|
||||
<RowFixed>
|
||||
<Text fontWeight={500} fontSize={20}>
|
||||
{userPoolBalance ? userPoolBalance.toSignificant(4) : '-'}
|
||||
</Text>
|
||||
</RowFixed>
|
||||
</FixedHeightRow>
|
||||
<AutoColumn gap="4px">
|
||||
<FixedHeightRow>
|
||||
<Text color="#888D9B" fontSize={16} fontWeight={500}>
|
||||
{token0?.symbol}:
|
||||
</Text>
|
||||
{token0Deposited ? (
|
||||
<RowFixed>
|
||||
{!minimal && <TokenLogo address={token0?.address} />}
|
||||
<Text color="#888D9B" fontSize={16} fontWeight={500} marginLeft={'6px'}>
|
||||
{token0Deposited?.toSignificant(6)}
|
||||
</Text>
|
||||
</RowFixed>
|
||||
) : (
|
||||
'-'
|
||||
)}
|
||||
</FixedHeightRow>
|
||||
<FixedHeightRow>
|
||||
<Text color="#888D9B" fontSize={16} fontWeight={500}>
|
||||
{token1?.symbol}:
|
||||
</Text>
|
||||
{token1Deposited ? (
|
||||
<RowFixed>
|
||||
{!minimal && <TokenLogo address={token1?.address} />}
|
||||
<Text color="#888D9B" fontSize={16} fontWeight={500} marginLeft={'6px'}>
|
||||
{token1Deposited?.toSignificant(6)}
|
||||
</Text>
|
||||
</RowFixed>
|
||||
) : (
|
||||
'-'
|
||||
)}
|
||||
</FixedHeightRow>
|
||||
</AutoColumn>
|
||||
</AutoColumn>
|
||||
</GreyCard>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
} else
|
||||
return (
|
||||
<HoverCard border={border}>
|
||||
<AutoColumn gap="12px">
|
||||
<FixedHeightRow onClick={() => setShowMore(!showMore)} style={{ cursor: 'pointer' }}>
|
||||
<RowFixed>
|
||||
<DoubleLogo a0={token0?.address || ''} a1={token1?.address || ''} margin={true} size={20} />
|
||||
<Text fontWeight={500} fontSize={20}>
|
||||
{token0?.symbol}/{token1?.symbol}
|
||||
</Text>
|
||||
</RowFixed>
|
||||
<RowFixed>
|
||||
{showMore ? (
|
||||
<ChevronUp size="20" style={{ marginLeft: '10px' }} />
|
||||
) : (
|
||||
<ChevronDown size="20" style={{ marginLeft: '10px' }} />
|
||||
)}
|
||||
</RowFixed>
|
||||
</FixedHeightRow>
|
||||
{showMore && (
|
||||
<AutoColumn gap="8px">
|
||||
<FixedHeightRow>
|
||||
<RowFixed>
|
||||
<Text fontSize={16} fontWeight={500}>
|
||||
Pooled {token0?.symbol}:
|
||||
</Text>
|
||||
</RowFixed>
|
||||
{token0Deposited ? (
|
||||
<RowFixed>
|
||||
<Text fontSize={16} fontWeight={500} marginLeft={'6px'}>
|
||||
{token0Deposited?.toSignificant(6)}
|
||||
</Text>
|
||||
{!minimal && <TokenLogo size="20px" style={{ marginLeft: '8px' }} address={token0?.address} />}
|
||||
</RowFixed>
|
||||
) : (
|
||||
'-'
|
||||
)}
|
||||
</FixedHeightRow>
|
||||
|
||||
<FixedHeightRow>
|
||||
<RowFixed>
|
||||
<Text fontSize={16} fontWeight={500}>
|
||||
Pooled {token1?.symbol}:
|
||||
</Text>
|
||||
</RowFixed>
|
||||
{token1Deposited ? (
|
||||
<RowFixed>
|
||||
<Text fontSize={16} fontWeight={500} marginLeft={'6px'}>
|
||||
{token1Deposited?.toSignificant(6)}
|
||||
</Text>
|
||||
{!minimal && <TokenLogo size="20px" style={{ marginLeft: '8px' }} address={token1?.address} />}
|
||||
</RowFixed>
|
||||
) : (
|
||||
'-'
|
||||
)}
|
||||
</FixedHeightRow>
|
||||
{!minimal && (
|
||||
<FixedHeightRow>
|
||||
<Text fontSize={16} fontWeight={500}>
|
||||
Your pool tokens:
|
||||
</Text>
|
||||
<Text fontSize={16} fontWeight={500}>
|
||||
{userPoolBalance ? userPoolBalance.toSignificant(4) : '-'}
|
||||
</Text>
|
||||
</FixedHeightRow>
|
||||
)}
|
||||
{!minimal && (
|
||||
<FixedHeightRow>
|
||||
<Text fontSize={16} fontWeight={500}>
|
||||
Your pool share
|
||||
</Text>
|
||||
<Text fontSize={16} fontWeight={500}>
|
||||
{poolTokenPercentage ? poolTokenPercentage.toFixed(2) + '%' : '-'}
|
||||
</Text>
|
||||
</FixedHeightRow>
|
||||
)}
|
||||
|
||||
<AutoRow justify="center" marginTop={'10px'}>
|
||||
<ExternalLink href={`https://uniswap.info/pair/${pair?.liquidityToken.address}`}>
|
||||
View pool information ↗
|
||||
</ExternalLink>
|
||||
</AutoRow>
|
||||
<RowBetween marginTop="10px">
|
||||
<ButtonSecondary
|
||||
width="48%"
|
||||
onClick={() => {
|
||||
history.push('/add/' + token0?.address + '-' + token1?.address)
|
||||
}}
|
||||
>
|
||||
Add
|
||||
</ButtonSecondary>
|
||||
<ButtonSecondary
|
||||
width="48%"
|
||||
onClick={() => {
|
||||
history.push('/remove/' + token0?.address + '-' + token1?.address)
|
||||
}}
|
||||
>
|
||||
Remove
|
||||
</ButtonSecondary>
|
||||
</RowBetween>
|
||||
</AutoColumn>
|
||||
)}
|
||||
</AutoColumn>
|
||||
</HoverCard>
|
||||
)
|
||||
}
|
||||
|
||||
export default withRouter(PositionCard)
|
||||
40
src/components/QuestionHelper/index.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
import React, { useCallback, useState } from 'react'
|
||||
import { HelpCircle as Question } from 'react-feather'
|
||||
import styled from 'styled-components'
|
||||
import Tooltip from '../Tooltip'
|
||||
|
||||
const QuestionWrapper = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0.2rem;
|
||||
border: none;
|
||||
background: none;
|
||||
outline: none;
|
||||
cursor: default;
|
||||
border-radius: 36px;
|
||||
background-color: ${({ theme }) => theme.bg2};
|
||||
color: ${({ theme }) => theme.text2};
|
||||
|
||||
:hover,
|
||||
:focus {
|
||||
opacity: 0.7;
|
||||
}
|
||||
`
|
||||
|
||||
export default function QuestionHelper({ text, disabled }: { text: string; disabled?: boolean }) {
|
||||
const [show, setShow] = useState<boolean>(false)
|
||||
|
||||
const open = useCallback(() => setShow(true), [setShow])
|
||||
const close = useCallback(() => setShow(false), [setShow])
|
||||
|
||||
return (
|
||||
<span style={{ marginLeft: 4 }}>
|
||||
<Tooltip text={text} show={show && !disabled}>
|
||||
<QuestionWrapper onClick={open} onMouseEnter={open} onMouseLeave={close}>
|
||||
<Question size={16} />
|
||||
</QuestionWrapper>
|
||||
</Tooltip>
|
||||
</span>
|
||||
)
|
||||
}
|
||||
39
src/components/Row/index.tsx
Normal file
@@ -0,0 +1,39 @@
|
||||
import styled from 'styled-components'
|
||||
import { Box } from 'rebass/styled-components'
|
||||
|
||||
const Row = styled(Box)<{ align?: string; padding?: string; border?: string; borderRadius?: string }>`
|
||||
width: 100%;
|
||||
display: flex;
|
||||
padding: 0;
|
||||
align-items: center;
|
||||
align-items: ${({ align }) => align && align};
|
||||
padding: ${({ padding }) => padding};
|
||||
border: ${({ border }) => border};
|
||||
border-radius: ${({ borderRadius }) => borderRadius};
|
||||
`
|
||||
|
||||
export const RowBetween = styled(Row)<{ align?: string; padding?: string; border?: string; borderRadius?: string }>`
|
||||
justify-content: space-between;
|
||||
`
|
||||
|
||||
export const RowFlat = styled.div`
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
`
|
||||
|
||||
export const AutoRow = styled(Row)<{ gap?: string; justify?: string }>`
|
||||
flex-wrap: wrap;
|
||||
margin: ${({ gap }) => gap && `-${gap}`};
|
||||
justify-content: ${({ justify }) => justify && justify};
|
||||
|
||||
& > * {
|
||||
margin: ${({ gap }) => gap} !important;
|
||||
}
|
||||
`
|
||||
|
||||
export const RowFixed = styled(Row)<{ gap?: string; justify?: string }>`
|
||||
width: fit-content;
|
||||
margin: ${({ gap }) => gap && `-${gap}`};
|
||||
`
|
||||
|
||||
export default Row
|
||||
48
src/components/SearchModal/CommonBases.tsx
Normal file
@@ -0,0 +1,48 @@
|
||||
import React from 'react'
|
||||
import { Text } from 'rebass'
|
||||
import { Token } from '@uniswap/sdk'
|
||||
|
||||
import { SUGGESTED_BASES } from '../../constants'
|
||||
import { AutoColumn } from '../Column'
|
||||
import QuestionHelper from '../QuestionHelper'
|
||||
import { AutoRow } from '../Row'
|
||||
import TokenLogo from '../TokenLogo'
|
||||
import { BaseWrapper } from './styleds'
|
||||
|
||||
export default function CommonBases({
|
||||
chainId,
|
||||
onSelect,
|
||||
selectedTokenAddress
|
||||
}: {
|
||||
chainId: number
|
||||
selectedTokenAddress: string
|
||||
onSelect: (tokenAddress: string) => void
|
||||
}) {
|
||||
return (
|
||||
<AutoColumn gap="md">
|
||||
<AutoRow>
|
||||
<Text fontWeight={500} fontSize={16}>
|
||||
Common Bases
|
||||
</Text>
|
||||
<QuestionHelper text="These tokens are commonly used in pairs." />
|
||||
</AutoRow>
|
||||
<AutoRow gap="10px">
|
||||
{(SUGGESTED_BASES[chainId] ?? []).map((token: Token) => {
|
||||
return (
|
||||
<BaseWrapper
|
||||
gap="6px"
|
||||
onClick={() => selectedTokenAddress !== token.address && onSelect(token.address)}
|
||||
disable={selectedTokenAddress === token.address}
|
||||
key={token.address}
|
||||
>
|
||||
<TokenLogo address={token.address} />
|
||||
<Text fontWeight={500} fontSize={16}>
|
||||
{token.symbol}
|
||||
</Text>
|
||||
</BaseWrapper>
|
||||
)
|
||||
})}
|
||||
</AutoRow>
|
||||
</AutoColumn>
|
||||
)
|
||||
}
|
||||
64
src/components/SearchModal/PairList.tsx
Normal file
@@ -0,0 +1,64 @@
|
||||
import { JSBI, Pair, TokenAmount } from '@uniswap/sdk'
|
||||
import React from 'react'
|
||||
import { FixedSizeList } from 'react-window'
|
||||
import { Text } from 'rebass'
|
||||
import { ButtonPrimary } from '../Button'
|
||||
import DoubleTokenLogo from '../DoubleLogo'
|
||||
import { RowFixed } from '../Row'
|
||||
import { MenuItem, ModalInfo } from './styleds'
|
||||
|
||||
export default function PairList({
|
||||
pairs,
|
||||
focusTokenAddress,
|
||||
pairBalances,
|
||||
onSelectPair,
|
||||
onAddLiquidity = onSelectPair
|
||||
}: {
|
||||
pairs: Pair[]
|
||||
focusTokenAddress?: string
|
||||
pairBalances: { [pairAddress: string]: TokenAmount }
|
||||
onSelectPair: (pair: Pair) => void
|
||||
onAddLiquidity: (pair: Pair) => void
|
||||
}) {
|
||||
if (pairs.length === 0) {
|
||||
return <ModalInfo>No Pools Found</ModalInfo>
|
||||
}
|
||||
|
||||
return (
|
||||
<FixedSizeList
|
||||
itemSize={54}
|
||||
height={500}
|
||||
itemCount={pairs.length}
|
||||
width="100%"
|
||||
style={{ flex: '1', minHeight: 200 }}
|
||||
>
|
||||
{({ index, style }) => {
|
||||
const pair = pairs[index]
|
||||
|
||||
// the focused token is shown first
|
||||
const tokenA = focusTokenAddress === pair.token1.address ? pair.token1 : pair.token0
|
||||
const tokenB = tokenA === pair.token0 ? pair.token1 : pair.token0
|
||||
|
||||
const pairAddress = pair.liquidityToken.address
|
||||
const balance = pairBalances[pairAddress]?.toSignificant(6)
|
||||
const zeroBalance = pairBalances[pairAddress]?.raw && JSBI.equal(pairBalances[pairAddress].raw, JSBI.BigInt(0))
|
||||
|
||||
const selectPair = () => onSelectPair(pair)
|
||||
const addLiquidity = () => onAddLiquidity(pair)
|
||||
|
||||
return (
|
||||
<MenuItem style={style} onClick={selectPair}>
|
||||
<RowFixed>
|
||||
<DoubleTokenLogo a0={tokenA.address} a1={tokenB.address} size={24} margin={true} />
|
||||
<Text fontWeight={500} fontSize={16}>{`${tokenA.symbol}/${tokenB.symbol}`}</Text>
|
||||
</RowFixed>
|
||||
|
||||
<ButtonPrimary padding={'6px 8px'} width={'fit-content'} borderRadius={'12px'} onClick={addLiquidity}>
|
||||
{balance ? (zeroBalance ? 'Join' : 'Add Liquidity') : 'Join'}
|
||||
</ButtonPrimary>
|
||||
</MenuItem>
|
||||
)
|
||||
}}
|
||||
</FixedSizeList>
|
||||
)
|
||||
}
|
||||
34
src/components/SearchModal/SortButton.tsx
Normal file
@@ -0,0 +1,34 @@
|
||||
import React from 'react'
|
||||
import { Text } from 'rebass'
|
||||
import styled from 'styled-components'
|
||||
import { RowFixed } from '../Row'
|
||||
|
||||
export const FilterWrapper = styled(RowFixed)`
|
||||
padding: 8px;
|
||||
background-color: ${({ theme }) => theme.bg2};
|
||||
color: ${({ theme }) => theme.text1};
|
||||
border-radius: 8px;
|
||||
user-select: none;
|
||||
& > * {
|
||||
user-select: none;
|
||||
}
|
||||
:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
`
|
||||
|
||||
export default function SortButton({
|
||||
toggleSortOrder,
|
||||
ascending
|
||||
}: {
|
||||
toggleSortOrder: () => void
|
||||
ascending: boolean
|
||||
}) {
|
||||
return (
|
||||
<FilterWrapper onClick={toggleSortOrder}>
|
||||
<Text fontSize={14} fontWeight={500}>
|
||||
{ascending ? '↑' : '↓'}
|
||||
</Text>
|
||||
</FilterWrapper>
|
||||
)
|
||||
}
|
||||
125
src/components/SearchModal/TokenList.tsx
Normal file
@@ -0,0 +1,125 @@
|
||||
import { ChainId, JSBI, Token, TokenAmount } from '@uniswap/sdk'
|
||||
import React, { useContext } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { FixedSizeList } from 'react-window'
|
||||
import { Text } from 'rebass'
|
||||
import { ThemeContext } from 'styled-components'
|
||||
import { ALL_TOKENS } from '../../constants/tokens'
|
||||
import { useActiveWeb3React } from '../../hooks'
|
||||
import { LinkStyledButton, TYPE } from '../../theme'
|
||||
import { isAddress } from '../../utils'
|
||||
import { ButtonSecondary } from '../Button'
|
||||
import Column, { AutoColumn } from '../Column'
|
||||
import { RowFixed } from '../Row'
|
||||
import TokenLogo from '../TokenLogo'
|
||||
import { FadedSpan, GreySpan, MenuItem, ModalInfo } from './styleds'
|
||||
import Loader from '../Loader'
|
||||
|
||||
function isDefaultToken(tokenAddress: string, chainId?: number): boolean {
|
||||
const address = isAddress(tokenAddress)
|
||||
return Boolean(chainId && address && ALL_TOKENS[chainId as ChainId]?.[tokenAddress])
|
||||
}
|
||||
|
||||
export default function TokenList({
|
||||
tokens,
|
||||
allTokenBalances,
|
||||
selectedToken,
|
||||
onTokenSelect,
|
||||
otherToken,
|
||||
showSendWithSwap,
|
||||
onRemoveAddedToken,
|
||||
otherSelectedText,
|
||||
hideRemove
|
||||
}: {
|
||||
tokens: Token[]
|
||||
selectedToken: string
|
||||
allTokenBalances: { [tokenAddress: string]: TokenAmount }
|
||||
onTokenSelect: (tokenAddress: string) => void
|
||||
onRemoveAddedToken: (chainId: number, tokenAddress: string) => void
|
||||
otherToken: string
|
||||
showSendWithSwap?: boolean
|
||||
otherSelectedText: string
|
||||
hideRemove?: boolean
|
||||
}) {
|
||||
const { t } = useTranslation()
|
||||
const { account, chainId } = useActiveWeb3React()
|
||||
const theme = useContext(ThemeContext)
|
||||
|
||||
if (tokens.length === 0) {
|
||||
return <ModalInfo>{t('noToken')}</ModalInfo>
|
||||
}
|
||||
return (
|
||||
<FixedSizeList
|
||||
width="100%"
|
||||
height={500}
|
||||
itemCount={tokens.length}
|
||||
itemSize={50}
|
||||
style={{ flex: '1', minHeight: 200 }}
|
||||
>
|
||||
{({ index, style }) => {
|
||||
const { address, symbol } = tokens[index]
|
||||
|
||||
const customAdded = !isDefaultToken(address, chainId)
|
||||
const balance = allTokenBalances[address]
|
||||
|
||||
const zeroBalance = balance && JSBI.equal(JSBI.BigInt(0), balance.raw)
|
||||
|
||||
return (
|
||||
<MenuItem
|
||||
style={style}
|
||||
key={address}
|
||||
className={`token-item-${address}`}
|
||||
onClick={() => (selectedToken && selectedToken === address ? null : onTokenSelect(address))}
|
||||
disabled={selectedToken && selectedToken === address}
|
||||
selected={otherToken === address}
|
||||
>
|
||||
<RowFixed>
|
||||
<TokenLogo address={address} size={'24px'} style={{ marginRight: '14px' }} />
|
||||
<Column>
|
||||
<Text fontWeight={500}>
|
||||
{symbol}
|
||||
{otherToken === address && <GreySpan> ({otherSelectedText})</GreySpan>}
|
||||
</Text>
|
||||
<FadedSpan>
|
||||
<TYPE.main fontWeight={500}>{customAdded && 'Added by user'}</TYPE.main>
|
||||
{customAdded && !hideRemove && (
|
||||
<LinkStyledButton
|
||||
onClick={event => {
|
||||
event.stopPropagation()
|
||||
onRemoveAddedToken(chainId, address)
|
||||
}}
|
||||
style={{ marginLeft: '4px', fontWeight: 400 }}
|
||||
>
|
||||
(Remove)
|
||||
</LinkStyledButton>
|
||||
)}
|
||||
</FadedSpan>
|
||||
</Column>
|
||||
</RowFixed>
|
||||
<AutoColumn gap="4px" justify="end">
|
||||
{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>
|
||||
)
|
||||
}}
|
||||
</FixedSizeList>
|
||||
)
|
||||
}
|
||||
60
src/components/SearchModal/filtering.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import { isAddress } from '../../utils'
|
||||
import { Pair, Token } from '@uniswap/sdk'
|
||||
|
||||
export function filterTokens(tokens: Token[], search: string): Token[] {
|
||||
if (search.length === 0) return tokens
|
||||
|
||||
const searchingAddress = isAddress(search)
|
||||
|
||||
if (searchingAddress) {
|
||||
return tokens.filter(token => token.address === searchingAddress)
|
||||
}
|
||||
|
||||
const lowerSearchParts = search
|
||||
.toLowerCase()
|
||||
.split(/\s+/)
|
||||
.filter(s => s.length > 0)
|
||||
|
||||
if (lowerSearchParts.length === 0) {
|
||||
return tokens
|
||||
}
|
||||
|
||||
const matchesSearch = (s: string): boolean => {
|
||||
const sParts = s
|
||||
.toLowerCase()
|
||||
.split(/\s+/)
|
||||
.filter(s => s.length > 0)
|
||||
|
||||
return lowerSearchParts.every(p => p.length === 0 || sParts.some(sp => sp.startsWith(p) || sp.endsWith(p)))
|
||||
}
|
||||
|
||||
return tokens.filter(token => {
|
||||
const { symbol, name } = token
|
||||
|
||||
return matchesSearch(symbol) || matchesSearch(name)
|
||||
})
|
||||
}
|
||||
|
||||
export function filterPairs(pairs: Pair[], search: string): Pair[] {
|
||||
if (search.trim().length === 0) return pairs
|
||||
|
||||
const addressSearch = isAddress(search)
|
||||
if (addressSearch) {
|
||||
return pairs.filter(p => {
|
||||
return (
|
||||
p.token0.address === addressSearch ||
|
||||
p.token1.address === addressSearch ||
|
||||
p.liquidityToken.address === addressSearch
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
const lowerSearch = search.toLowerCase()
|
||||
return pairs.filter(pair => {
|
||||
const pairExpressionA = `${pair.token0.symbol}/${pair.token1.symbol}`.toLowerCase()
|
||||
if (pairExpressionA.startsWith(lowerSearch)) return true
|
||||
const pairExpressionB = `${pair.token1.symbol}/${pair.token0.symbol}`.toLowerCase()
|
||||
if (pairExpressionB.startsWith(lowerSearch)) return true
|
||||
return filterTokens([pair.token0, pair.token1], search).length > 0
|
||||
})
|
||||
}
|
||||
236
src/components/SearchModal/index.tsx
Normal file
@@ -0,0 +1,236 @@
|
||||
import { Pair, Token } from '@uniswap/sdk'
|
||||
import React, { useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { isMobile } from 'react-device-detect'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { RouteComponentProps, withRouter } from 'react-router-dom'
|
||||
import { Text } from 'rebass'
|
||||
import { ThemeContext } from 'styled-components'
|
||||
import Card from '../../components/Card'
|
||||
import { useActiveWeb3React } from '../../hooks'
|
||||
import { useAllTokens, useTokenByAddressAndAutomaticallyAdd } from '../../hooks/Tokens'
|
||||
import { useAllDummyPairs, useRemoveUserAddedToken } from '../../state/user/hooks'
|
||||
import { useAllTokenBalancesTreatingWETHasETH, useTokenBalances } from '../../state/wallet/hooks'
|
||||
import { CloseIcon, LinkStyledButton, StyledInternalLink } from '../../theme/components'
|
||||
import { isAddress } from '../../utils'
|
||||
import Column from '../Column'
|
||||
import Modal from '../Modal'
|
||||
import QuestionHelper from '../QuestionHelper'
|
||||
import { AutoRow, RowBetween } from '../Row'
|
||||
import Tooltip from '../Tooltip'
|
||||
import CommonBases from './CommonBases'
|
||||
import { filterPairs, filterTokens } from './filtering'
|
||||
import PairList from './PairList'
|
||||
import { useTokenComparator, pairComparator } from './sorting'
|
||||
import { PaddedColumn, SearchInput } from './styleds'
|
||||
import TokenList from './TokenList'
|
||||
import SortButton from './SortButton'
|
||||
|
||||
interface SearchModalProps extends RouteComponentProps {
|
||||
isOpen?: boolean
|
||||
onDismiss?: () => void
|
||||
filterType?: 'tokens'
|
||||
hiddenToken?: string
|
||||
showSendWithSwap?: boolean
|
||||
onTokenSelect?: (address: string) => void
|
||||
otherSelectedTokenAddress?: string
|
||||
otherSelectedText?: string
|
||||
showCommonBases?: boolean
|
||||
}
|
||||
|
||||
function SearchModal({
|
||||
history,
|
||||
isOpen,
|
||||
onDismiss,
|
||||
onTokenSelect,
|
||||
filterType,
|
||||
hiddenToken,
|
||||
showSendWithSwap,
|
||||
otherSelectedTokenAddress,
|
||||
otherSelectedText,
|
||||
showCommonBases = false
|
||||
}: SearchModalProps) {
|
||||
const { t } = useTranslation()
|
||||
const { account, chainId } = useActiveWeb3React()
|
||||
const theme = useContext(ThemeContext)
|
||||
|
||||
const isTokenView = filterType === 'tokens'
|
||||
|
||||
const allTokens = useAllTokens()
|
||||
const allPairs = useAllDummyPairs()
|
||||
const allTokenBalances = useAllTokenBalancesTreatingWETHasETH() ?? {}
|
||||
const allPairBalances = useTokenBalances(
|
||||
account,
|
||||
allPairs.map(p => p.liquidityToken)
|
||||
)
|
||||
|
||||
const [searchQuery, setSearchQuery] = useState<string>('')
|
||||
const [tooltipOpen, setTooltipOpen] = useState<boolean>(false)
|
||||
const [invertSearchOrder, setInvertSearchOrder] = useState<boolean>(false)
|
||||
|
||||
const removeTokenByAddress = useRemoveUserAddedToken()
|
||||
|
||||
// if the current input is an address, and we don't have the token in context, try to fetch it and import
|
||||
useTokenByAddressAndAutomaticallyAdd(searchQuery)
|
||||
|
||||
const tokenComparator = useTokenComparator(invertSearchOrder)
|
||||
|
||||
const filteredTokens: Token[] = useMemo(() => {
|
||||
if (!isTokenView) return []
|
||||
return filterTokens(Object.values(allTokens), searchQuery)
|
||||
}, [isTokenView, allTokens, searchQuery])
|
||||
|
||||
const filteredSortedTokens: Token[] = useMemo(() => {
|
||||
if (!isTokenView) return []
|
||||
const sorted = filteredTokens.sort(tokenComparator)
|
||||
const symbolMatch = searchQuery
|
||||
.toLowerCase()
|
||||
.split(/\s+/)
|
||||
.filter(s => s.length > 0)
|
||||
if (symbolMatch.length > 1) return sorted
|
||||
|
||||
return [
|
||||
// sort any exact symbol matches first
|
||||
...sorted.filter(token => token.symbol.toLowerCase() === symbolMatch[0]),
|
||||
...sorted.filter(token => token.symbol.toLowerCase() !== symbolMatch[0])
|
||||
]
|
||||
}, [filteredTokens, isTokenView, searchQuery, tokenComparator])
|
||||
|
||||
function _onTokenSelect(address: string) {
|
||||
onTokenSelect(address)
|
||||
onDismiss()
|
||||
}
|
||||
|
||||
// clear the input on open
|
||||
useEffect(() => {
|
||||
if (isOpen) setSearchQuery('')
|
||||
}, [isOpen, setSearchQuery])
|
||||
|
||||
// manage focus on modal show
|
||||
const inputRef = useRef<HTMLInputElement>()
|
||||
function onInput(event) {
|
||||
const input = event.target.value
|
||||
const checksummedInput = isAddress(input)
|
||||
setSearchQuery(checksummedInput || input)
|
||||
}
|
||||
|
||||
const sortedPairList = useMemo(() => {
|
||||
if (isTokenView) return []
|
||||
return allPairs.sort((a, b): number => {
|
||||
const balanceA = allPairBalances[a.liquidityToken.address]
|
||||
const balanceB = allPairBalances[b.liquidityToken.address]
|
||||
return pairComparator(a, b, balanceA, balanceB)
|
||||
})
|
||||
}, [isTokenView, allPairs, allPairBalances])
|
||||
|
||||
const filteredPairs = useMemo(() => {
|
||||
if (isTokenView) return []
|
||||
return filterPairs(sortedPairList, searchQuery)
|
||||
}, [isTokenView, searchQuery, sortedPairList])
|
||||
|
||||
const selectPair = useCallback(
|
||||
(pair: Pair) => {
|
||||
history.push(`/add/${pair.token0.address}-${pair.token1.address}`)
|
||||
},
|
||||
[history]
|
||||
)
|
||||
|
||||
const focusedToken = Object.values(allTokens ?? {}).filter(token => {
|
||||
return token.symbol.toLowerCase() === searchQuery || searchQuery === token.address
|
||||
})[0]
|
||||
|
||||
const openTooltip = useCallback(() => {
|
||||
setTooltipOpen(true)
|
||||
inputRef.current?.focus()
|
||||
}, [setTooltipOpen])
|
||||
const closeTooltip = useCallback(() => setTooltipOpen(false), [setTooltipOpen])
|
||||
|
||||
return (
|
||||
<Modal isOpen={isOpen} onDismiss={onDismiss} maxHeight={70} initialFocusRef={isMobile ? undefined : inputRef}>
|
||||
<Column style={{ width: '100%' }}>
|
||||
<PaddedColumn gap="20px">
|
||||
<RowBetween>
|
||||
<Text fontWeight={500} fontSize={16}>
|
||||
{isTokenView ? 'Select a token' : 'Select a pool'}
|
||||
<QuestionHelper
|
||||
disabled={tooltipOpen}
|
||||
text={
|
||||
isTokenView
|
||||
? 'Find a token by searching for its name or symbol or by pasting its address below.'
|
||||
: 'Find a pair by searching for its name 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={onInput}
|
||||
onBlur={closeTooltip}
|
||||
/>
|
||||
</Tooltip>
|
||||
{showCommonBases && (
|
||||
<CommonBases chainId={chainId} onSelect={_onTokenSelect} selectedTokenAddress={hiddenToken} />
|
||||
)}
|
||||
<RowBetween>
|
||||
<Text fontSize={14} fontWeight={500}>
|
||||
{isTokenView ? 'Token Name' : 'Pool Name'}
|
||||
</Text>
|
||||
{isTokenView && (
|
||||
<SortButton ascending={invertSearchOrder} toggleSortOrder={() => setInvertSearchOrder(iso => !iso)} />
|
||||
)}
|
||||
</RowBetween>
|
||||
</PaddedColumn>
|
||||
<div style={{ width: '100%', height: '1px', backgroundColor: theme.bg2 }} />
|
||||
{isTokenView ? (
|
||||
<TokenList
|
||||
tokens={filteredSortedTokens}
|
||||
allTokenBalances={allTokenBalances}
|
||||
onRemoveAddedToken={removeTokenByAddress}
|
||||
onTokenSelect={_onTokenSelect}
|
||||
otherSelectedText={otherSelectedText}
|
||||
otherToken={otherSelectedTokenAddress}
|
||||
selectedToken={hiddenToken}
|
||||
showSendWithSwap={showSendWithSwap}
|
||||
hideRemove={Boolean(isAddress(searchQuery))}
|
||||
/>
|
||||
) : (
|
||||
<PairList
|
||||
pairs={filteredPairs}
|
||||
focusTokenAddress={focusedToken?.address}
|
||||
onAddLiquidity={selectPair}
|
||||
onSelectPair={selectPair}
|
||||
pairBalances={allPairBalances}
|
||||
/>
|
||||
)}
|
||||
<div style={{ width: '100%', height: '1px', backgroundColor: theme.bg2 }} />
|
||||
<Card>
|
||||
<AutoRow justify={'center'}>
|
||||
<div>
|
||||
{isTokenView ? (
|
||||
<LinkStyledButton style={{ fontWeight: 500, color: theme.text2, fontSize: 16 }} onClick={openTooltip}>
|
||||
Having trouble finding a token?
|
||||
</LinkStyledButton>
|
||||
) : (
|
||||
<Text fontWeight={500}>
|
||||
{!isMobile && "Don't see a pool? "}
|
||||
<StyledInternalLink to="/find">{!isMobile ? 'Import it.' : 'Import pool.'}</StyledInternalLink>
|
||||
</Text>
|
||||
)}
|
||||
</div>
|
||||
</AutoRow>
|
||||
</Card>
|
||||
</Column>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
export default withRouter(SearchModal)
|
||||
77
src/components/SearchModal/sorting.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
import { Token, TokenAmount, WETH, Pair } from '@uniswap/sdk'
|
||||
import { useMemo } from 'react'
|
||||
import { useActiveWeb3React } from '../../hooks'
|
||||
import { useAllTokenBalancesTreatingWETHasETH } from '../../state/wallet/hooks'
|
||||
import { DUMMY_PAIRS_TO_PIN } from '../../constants'
|
||||
|
||||
// compare two token amounts with highest one coming first
|
||||
function balanceComparator(balanceA?: TokenAmount, balanceB?: TokenAmount) {
|
||||
if (balanceA && balanceB) {
|
||||
return balanceA.greaterThan(balanceB) ? -1 : balanceA.equalTo(balanceB) ? 0 : 1
|
||||
} else if (balanceA && balanceA.greaterThan('0')) {
|
||||
return -1
|
||||
} else if (balanceB && balanceB.greaterThan('0')) {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// compare two pairs, favoring "pinned" pairs, and falling back to balances
|
||||
export function pairComparator(pairA: Pair, pairB: Pair, balanceA?: TokenAmount, balanceB?: TokenAmount) {
|
||||
const aShouldBePinned =
|
||||
DUMMY_PAIRS_TO_PIN[pairA?.token0?.chainId]?.some(
|
||||
dummyPairToPin => dummyPairToPin.liquidityToken.address === pairA?.liquidityToken?.address
|
||||
) ?? false
|
||||
const bShouldBePinned =
|
||||
DUMMY_PAIRS_TO_PIN[pairB?.token0?.chainId]?.some(
|
||||
dummyPairToPin => dummyPairToPin.liquidityToken.address === pairB?.liquidityToken?.address
|
||||
) ?? false
|
||||
|
||||
if (aShouldBePinned && !bShouldBePinned) {
|
||||
return -1
|
||||
} else if (!aShouldBePinned && bShouldBePinned) {
|
||||
return 1
|
||||
} else {
|
||||
return balanceComparator(balanceA, balanceB)
|
||||
}
|
||||
}
|
||||
|
||||
function getTokenComparator(
|
||||
weth: Token | undefined,
|
||||
balances: { [tokenAddress: string]: TokenAmount }
|
||||
): (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]
|
||||
|
||||
const balanceComp = balanceComparator(balanceA, balanceB)
|
||||
if (balanceComp !== 0) return balanceComp
|
||||
|
||||
// sort by symbol
|
||||
return tokenA.symbol.toLowerCase() < tokenB.symbol.toLowerCase() ? -1 : 1
|
||||
}
|
||||
}
|
||||
|
||||
export function useTokenComparator(inverted: boolean): (tokenA: Token, tokenB: Token) => number {
|
||||
const { chainId } = useActiveWeb3React()
|
||||
const weth = WETH[chainId]
|
||||
const balances = useAllTokenBalancesTreatingWETHasETH()
|
||||
const comparator = useMemo(() => getTokenComparator(weth, balances ?? {}), [balances, weth])
|
||||
return useMemo(() => {
|
||||
if (inverted) {
|
||||
return (tokenA: Token, tokenB: Token) => comparator(tokenA, tokenB) * -1
|
||||
} else {
|
||||
return comparator
|
||||
}
|
||||
}, [inverted, comparator])
|
||||
}
|
||||
88
src/components/SearchModal/styleds.tsx
Normal file
@@ -0,0 +1,88 @@
|
||||
import styled from 'styled-components'
|
||||
import { AutoColumn } from '../Column'
|
||||
import { AutoRow, RowBetween, RowFixed } from '../Row'
|
||||
|
||||
export const ModalInfo = styled.div`
|
||||
${({ theme }) => theme.flexRowNoWrap}
|
||||
align-items: center;
|
||||
padding: 1rem 1rem;
|
||||
margin: 0.25rem 0.5rem;
|
||||
justify-content: center;
|
||||
user-select: none;
|
||||
min-height: 200px;
|
||||
`
|
||||
|
||||
export const FadedSpan = styled(RowFixed)`
|
||||
color: ${({ theme }) => theme.primary1};
|
||||
font-size: 14px;
|
||||
`
|
||||
|
||||
export const GreySpan = styled.span`
|
||||
color: ${({ theme }) => theme.text3};
|
||||
font-weight: 400;
|
||||
`
|
||||
|
||||
export const Input = styled.input`
|
||||
position: relative;
|
||||
display: flex;
|
||||
padding: 16px;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
white-space: nowrap;
|
||||
background: none;
|
||||
border: none;
|
||||
outline: none;
|
||||
border-radius: 20px;
|
||||
color: ${({ theme }) => theme.text1};
|
||||
border-style: solid;
|
||||
border: 1px solid ${({ theme }) => theme.bg3};
|
||||
-webkit-appearance: none;
|
||||
|
||||
font-size: 18px;
|
||||
|
||||
::placeholder {
|
||||
color: ${({ theme }) => theme.text3};
|
||||
}
|
||||
`
|
||||
|
||||
export const PaddedColumn = styled(AutoColumn)`
|
||||
padding: 20px;
|
||||
padding-bottom: 12px;
|
||||
`
|
||||
|
||||
const PaddedItem = styled(RowBetween)`
|
||||
padding: 4px 20px;
|
||||
height: 56px;
|
||||
`
|
||||
|
||||
export const MenuItem = styled(PaddedItem)`
|
||||
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 BaseWrapper = styled(AutoRow)<{ disable?: boolean }>`
|
||||
border: 1px solid ${({ theme, disable }) => (disable ? 'transparent' : theme.bg3)};
|
||||
padding: 0 6px;
|
||||
border-radius: 10px;
|
||||
width: 120px;
|
||||
|
||||
:hover {
|
||||
cursor: ${({ disable }) => !disable && 'pointer'};
|
||||
background-color: ${({ theme, disable }) => !disable && theme.bg2};
|
||||
}
|
||||
|
||||
background-color: ${({ theme, disable }) => disable && theme.bg3};
|
||||
opacity: ${({ disable }) => disable && '0.4'};
|
||||
`
|
||||
|
||||
export const SearchInput = styled(Input)`
|
||||
transition: border 100ms;
|
||||
:focus {
|
||||
border: 1px solid ${({ theme }) => theme.primary1};
|
||||
outline: none;
|
||||
}
|
||||
`
|
||||
127
src/components/Slider/index.tsx
Normal file
@@ -0,0 +1,127 @@
|
||||
import React, { useCallback } from 'react'
|
||||
import styled from 'styled-components'
|
||||
|
||||
const StyledRangeInput = styled.input<{ value: 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 */
|
||||
cursor: pointer;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
&::-moz-focus-outer {
|
||||
border: 0;
|
||||
}
|
||||
|
||||
&::-webkit-slider-thumb {
|
||||
-webkit-appearance: none;
|
||||
height: 28px;
|
||||
width: 28px;
|
||||
background-color: #565a69;
|
||||
border-radius: 100%;
|
||||
border: none;
|
||||
transform: translateY(-50%);
|
||||
color: ${({ theme }) => theme.bg1};
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
box-shadow: 0px 0px 1px rgba(0, 0, 0, 0.1), 0px 4px 8px rgba(0, 0, 0, 0.08), 0px 16px 24px rgba(0, 0, 0, 0.06),
|
||||
0px 24px 32px rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
}
|
||||
|
||||
&::-moz-range-thumb {
|
||||
height: 28px;
|
||||
width: 28px;
|
||||
background-color: #565a69;
|
||||
border-radius: 100%;
|
||||
border: none;
|
||||
color: ${({ theme }) => theme.bg1};
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
box-shadow: 0px 0px 1px rgba(0, 0, 0, 0.1), 0px 4px 8px rgba(0, 0, 0, 0.08), 0px 16px 24px rgba(0, 0, 0, 0.06),
|
||||
0px 24px 32px rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
}
|
||||
|
||||
&::-ms-thumb {
|
||||
height: 28px;
|
||||
width: 28px;
|
||||
background-color: #565a69;
|
||||
border-radius: 100%;
|
||||
color: ${({ theme }) => theme.bg1};
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
box-shadow: 0px 0px 1px rgba(0, 0, 0, 0.1), 0px 4px 8px rgba(0, 0, 0, 0.08), 0px 16px 24px rgba(0, 0, 0, 0.06),
|
||||
0px 24px 32px rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
}
|
||||
|
||||
&::-webkit-slider-runnable-track {
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
${({ theme }) => theme.bg5},
|
||||
${({ theme }) => theme.bg5} ${({ value }) => value}%,
|
||||
${({ theme }) => theme.bg3} ${({ value }) => value}%,
|
||||
${({ 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}
|
||||
);
|
||||
height: 2px;
|
||||
}
|
||||
|
||||
&::-ms-track {
|
||||
width: 100%;
|
||||
border-color: transparent;
|
||||
color: transparent;
|
||||
|
||||
background: ${({ theme }) => theme.bg5};
|
||||
height: 2px;
|
||||
}
|
||||
&::-ms-fill-lower {
|
||||
background: ${({ theme }) => theme.bg5};
|
||||
}
|
||||
&::-ms-fill-upper {
|
||||
background: ${({ theme }) => theme.bg3};
|
||||
}
|
||||
`
|
||||
|
||||
interface InputSliderProps {
|
||||
value: number
|
||||
onChange: (value: number) => void
|
||||
}
|
||||
|
||||
export default function InputSlider({ value, onChange }: InputSliderProps) {
|
||||
const changeCallback = useCallback(
|
||||
e => {
|
||||
onChange(e.target.value)
|
||||
},
|
||||
[onChange]
|
||||
)
|
||||
|
||||
return (
|
||||
<StyledRangeInput
|
||||
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}
|
||||
/>
|
||||
)
|
||||
}
|
||||
251
src/components/SlippageTabs/index.tsx
Normal file
@@ -0,0 +1,251 @@
|
||||
import React, { useState, useRef, useContext } from 'react'
|
||||
import styled, { ThemeContext } from 'styled-components'
|
||||
|
||||
import QuestionHelper from '../QuestionHelper'
|
||||
import { TYPE } from '../../theme'
|
||||
import { AutoColumn } from '../Column'
|
||||
import { RowBetween, RowFixed } from '../Row'
|
||||
|
||||
import { darken } from 'polished'
|
||||
|
||||
enum SlippageError {
|
||||
InvalidInput = 'InvalidInput',
|
||||
RiskyLow = 'RiskyLow',
|
||||
RiskyHigh = 'RiskyHigh'
|
||||
}
|
||||
|
||||
enum DeadlineError {
|
||||
InvalidInput = 'InvalidInput'
|
||||
}
|
||||
|
||||
const FancyButton = styled.button`
|
||||
color: ${({ theme }) => theme.text1};
|
||||
align-items: center;
|
||||
min-width: 55px;
|
||||
height: 2rem;
|
||||
border-radius: 36px;
|
||||
font-size: 12px;
|
||||
border: 1px solid ${({ theme }) => theme.bg3};
|
||||
outline: none;
|
||||
background: ${({ theme }) => theme.bg1};
|
||||
:hover {
|
||||
cursor: inherit;
|
||||
border: 1px solid ${({ theme }) => theme.bg4};
|
||||
}
|
||||
:focus {
|
||||
border: 1px solid ${({ theme }) => theme.primary1};
|
||||
}
|
||||
`
|
||||
|
||||
const Option = styled(FancyButton)<{ active: boolean }>`
|
||||
margin-right: 8px;
|
||||
:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
background-color: ${({ active, theme }) => active && theme.primary1};
|
||||
color: ${({ active, theme }) => (active ? theme.white : theme.text1)};
|
||||
`
|
||||
|
||||
const Input = styled.input`
|
||||
background: ${({ theme }) => theme.bg1};
|
||||
flex-grow: 1;
|
||||
font-size: 12px;
|
||||
min-width: 20px;
|
||||
outline: none;
|
||||
&::-webkit-outer-spin-button,
|
||||
&::-webkit-inner-spin-button {
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
color: ${({ theme, color }) => (color === 'red' ? theme.red1 : theme.text1)};
|
||||
text-align: right;
|
||||
`
|
||||
|
||||
const OptionCustom = styled(FancyButton)<{ active?: boolean; warning?: boolean }>`
|
||||
height: 2rem;
|
||||
position: relative;
|
||||
padding: 0 0.75rem;
|
||||
border: ${({ theme, active, warning }) => active && `1px solid ${warning ? theme.red1 : theme.primary1}`};
|
||||
:hover {
|
||||
border: ${({ theme, active, warning }) =>
|
||||
active && `1px solid ${warning ? darken(0.1, theme.red1) : darken(0.1, theme.primary1)}`};
|
||||
}
|
||||
|
||||
input {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: 0px;
|
||||
border-radius: 2rem;
|
||||
}
|
||||
`
|
||||
|
||||
const SlippageSelector = styled.div`
|
||||
padding: 0 20px;
|
||||
`
|
||||
|
||||
export interface SlippageTabsProps {
|
||||
rawSlippage: number
|
||||
setRawSlippage: (rawSlippage: number) => void
|
||||
deadline: number
|
||||
setDeadline: (deadline: number) => void
|
||||
}
|
||||
|
||||
export default function SlippageTabs({ rawSlippage, setRawSlippage, deadline, setDeadline }: SlippageTabsProps) {
|
||||
const theme = useContext(ThemeContext)
|
||||
|
||||
const inputRef = useRef<HTMLInputElement>()
|
||||
|
||||
const [slippageInput, setSlippageInput] = useState('')
|
||||
const [deadlineInput, setDeadlineInput] = useState('')
|
||||
|
||||
const slippageInputIsValid =
|
||||
slippageInput === '' || (rawSlippage / 100).toFixed(2) === Number.parseFloat(slippageInput).toFixed(2)
|
||||
const deadlineInputIsValid = deadlineInput === '' || (deadline / 60).toString() === deadlineInput
|
||||
|
||||
let slippageError: SlippageError
|
||||
if (slippageInput !== '' && !slippageInputIsValid) {
|
||||
slippageError = SlippageError.InvalidInput
|
||||
} else if (slippageInputIsValid && rawSlippage < 50) {
|
||||
slippageError = SlippageError.RiskyLow
|
||||
} else if (slippageInputIsValid && rawSlippage > 500) {
|
||||
slippageError = SlippageError.RiskyHigh
|
||||
}
|
||||
|
||||
let deadlineError: DeadlineError
|
||||
if (deadlineInput !== '' && !deadlineInputIsValid) {
|
||||
deadlineError = DeadlineError.InvalidInput
|
||||
}
|
||||
|
||||
function parseCustomSlippage(event) {
|
||||
setSlippageInput(event.target.value)
|
||||
|
||||
let valueAsIntFromRoundedFloat: number
|
||||
try {
|
||||
valueAsIntFromRoundedFloat = Number.parseInt((Number.parseFloat(event.target.value) * 100).toString())
|
||||
} catch {}
|
||||
|
||||
if (
|
||||
typeof valueAsIntFromRoundedFloat === 'number' &&
|
||||
!Number.isNaN(valueAsIntFromRoundedFloat) &&
|
||||
valueAsIntFromRoundedFloat < 5000
|
||||
) {
|
||||
setRawSlippage(valueAsIntFromRoundedFloat)
|
||||
}
|
||||
}
|
||||
|
||||
function parseCustomDeadline(event) {
|
||||
setDeadlineInput(event.target.value)
|
||||
|
||||
let valueAsInt: number
|
||||
try {
|
||||
valueAsInt = Number.parseInt(event.target.value) * 60
|
||||
} catch {}
|
||||
|
||||
if (typeof valueAsInt === 'number' && !Number.isNaN(valueAsInt) && valueAsInt > 0) {
|
||||
setDeadline(valueAsInt)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<RowFixed padding={'0 20px'}>
|
||||
<TYPE.black fontWeight={400} fontSize={14} color={theme.text2}>
|
||||
Set slippage tolerance
|
||||
</TYPE.black>
|
||||
<QuestionHelper text="Your transaction will revert if the price changes unfavorably by more than this percentage." />
|
||||
</RowFixed>
|
||||
|
||||
<SlippageSelector>
|
||||
<RowBetween>
|
||||
<Option
|
||||
onClick={() => {
|
||||
setSlippageInput('')
|
||||
setRawSlippage(10)
|
||||
}}
|
||||
active={rawSlippage === 10}
|
||||
>
|
||||
0.1%
|
||||
</Option>
|
||||
<Option
|
||||
onClick={() => {
|
||||
setSlippageInput('')
|
||||
setRawSlippage(50)
|
||||
}}
|
||||
active={rawSlippage === 50}
|
||||
>
|
||||
0.5%
|
||||
</Option>
|
||||
<Option
|
||||
onClick={() => {
|
||||
setSlippageInput('')
|
||||
setRawSlippage(100)
|
||||
}}
|
||||
active={rawSlippage === 100}
|
||||
>
|
||||
1%
|
||||
</Option>
|
||||
<OptionCustom active={![10, 50, 100].includes(rawSlippage)} warning={!slippageInputIsValid} tabIndex={-1}>
|
||||
<RowBetween>
|
||||
{!!slippageInput &&
|
||||
(slippageError === SlippageError.RiskyLow || slippageError === SlippageError.RiskyHigh) ? (
|
||||
<span role="img" aria-label="warning" style={{ color: '#F3841E' }}>
|
||||
⚠️
|
||||
</span>
|
||||
) : null}
|
||||
<Input
|
||||
ref={inputRef}
|
||||
placeholder={(rawSlippage / 100).toFixed(2)}
|
||||
value={slippageInput}
|
||||
onBlur={() => {
|
||||
parseCustomSlippage({ target: { value: (rawSlippage / 100).toFixed(2) } })
|
||||
}}
|
||||
onChange={parseCustomSlippage}
|
||||
color={!slippageInputIsValid ? 'red' : ''}
|
||||
/>
|
||||
%
|
||||
</RowBetween>
|
||||
</OptionCustom>
|
||||
</RowBetween>
|
||||
{!!slippageError && (
|
||||
<RowBetween
|
||||
style={{
|
||||
fontSize: '14px',
|
||||
paddingTop: '7px',
|
||||
color: slippageError === SlippageError.InvalidInput ? 'red' : '#F3841E'
|
||||
}}
|
||||
>
|
||||
{slippageError === SlippageError.InvalidInput
|
||||
? 'Enter a valid slippage percentage'
|
||||
: slippageError === SlippageError.RiskyLow
|
||||
? 'Your transaction may fail'
|
||||
: 'Your transaction may be frontrun'}
|
||||
</RowBetween>
|
||||
)}
|
||||
</SlippageSelector>
|
||||
|
||||
<AutoColumn gap="sm">
|
||||
<RowFixed padding={'0 20px'}>
|
||||
<TYPE.black fontSize={14} fontWeight={400} color={theme.text2}>
|
||||
Deadline
|
||||
</TYPE.black>
|
||||
<QuestionHelper text="Your transaction will revert if it is pending for more than this long." />
|
||||
</RowFixed>
|
||||
<RowFixed padding={'0 20px'}>
|
||||
<OptionCustom style={{ width: '80px' }} tabIndex={-1}>
|
||||
<Input
|
||||
color={!!deadlineError ? 'red' : undefined}
|
||||
onBlur={() => {
|
||||
parseCustomDeadline({ target: { value: (deadline / 60).toString() } })
|
||||
}}
|
||||
placeholder={(deadline / 60).toString()}
|
||||
value={deadlineInput}
|
||||
onChange={parseCustomDeadline}
|
||||
/>
|
||||
</OptionCustom>
|
||||
<TYPE.body style={{ paddingLeft: '8px' }} fontSize={14}>
|
||||
minutes
|
||||
</TYPE.body>
|
||||
</RowFixed>
|
||||
</AutoColumn>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
import React, { useState } from 'react'
|
||||
import styled from 'styled-components'
|
||||
import { isAddress } from '../../utils'
|
||||
|
||||
import { ReactComponent as EthereumLogo } from '../../assets/images/ethereum-logo.svg'
|
||||
|
||||
const TOKEN_ICON_API = address =>
|
||||
`https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/${isAddress(
|
||||
address
|
||||
)}/logo.png`
|
||||
const BAD_IMAGES = {}
|
||||
|
||||
const Image = styled.img`
|
||||
width: ${({ size }) => size};
|
||||
height: ${({ size }) => size};
|
||||
background-color: white;
|
||||
border-radius: 1rem;
|
||||
`
|
||||
|
||||
const Emoji = styled.span`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: ${({ size }) => size};
|
||||
height: ${({ size }) => size};
|
||||
`
|
||||
|
||||
const StyledEthereumLogo = styled(EthereumLogo)`
|
||||
width: ${({ size }) => size};
|
||||
height: ${({ size }) => size};
|
||||
`
|
||||
|
||||
export default function TokenLogo({ address, size = '1rem', ...rest }) {
|
||||
const [error, setError] = useState(false)
|
||||
|
||||
let path = ''
|
||||
if (address === 'ETH') {
|
||||
return <StyledEthereumLogo size={size} />
|
||||
} else if (!error && !BAD_IMAGES[address]) {
|
||||
path = TOKEN_ICON_API(address.toLowerCase())
|
||||
} else {
|
||||
return (
|
||||
<Emoji {...rest} size={size}>
|
||||
<span role="img" aria-label="Thinking">
|
||||
🤔
|
||||
</span>
|
||||
</Emoji>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Image
|
||||
{...rest}
|
||||
alt={address}
|
||||
src={path}
|
||||
size={size}
|
||||
onError={() => {
|
||||
BAD_IMAGES[address] = true
|
||||
setError(true)
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
83
src/components/TokenLogo/index.tsx
Normal file
@@ -0,0 +1,83 @@
|
||||
import React, { useState } from 'react'
|
||||
import styled from 'styled-components'
|
||||
import { isAddress } from '../../utils'
|
||||
import { useActiveWeb3React } from '../../hooks'
|
||||
import { WETH } from '@uniswap/sdk'
|
||||
|
||||
import EthereumLogo from '../../assets/images/ethereum-logo.png'
|
||||
|
||||
const TOKEN_ICON_API = address =>
|
||||
`https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/${address}/logo.png`
|
||||
const BAD_IMAGES = {}
|
||||
|
||||
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};
|
||||
height: ${({ size }) => size};
|
||||
box-shadow: 0px 6px 10px rgba(0, 0, 0, 0.075);
|
||||
border-radius: 24px;
|
||||
`
|
||||
|
||||
export default function TokenLogo({
|
||||
address,
|
||||
size = '24px',
|
||||
...rest
|
||||
}: {
|
||||
address?: string
|
||||
size?: string
|
||||
style?: React.CSSProperties
|
||||
}) {
|
||||
const [error, setError] = useState(false)
|
||||
const { chainId } = useActiveWeb3React()
|
||||
|
||||
// mock rinkeby DAI
|
||||
if (chainId === 4 && address === '0xc7AD46e0b8a400Bb3C915120d284AafbA8fc4735') {
|
||||
address = '0x6B175474E89094C44Da98b954EedeAC495271d0F'
|
||||
}
|
||||
|
||||
let path = ''
|
||||
// hard code to show ETH instead of WETH in UI
|
||||
if (address === WETH[chainId].address) {
|
||||
return <StyledEthereumLogo src={EthereumLogo} size={size} {...rest} />
|
||||
} else if (!error && !BAD_IMAGES[address] && isAddress(address)) {
|
||||
path = TOKEN_ICON_API(address)
|
||||
} else {
|
||||
return (
|
||||
<Emoji {...rest} size={size}>
|
||||
<span role="img" aria-label="Thinking">
|
||||
🤔
|
||||
</span>
|
||||
</Emoji>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Image
|
||||
{...rest}
|
||||
// alt={address}
|
||||
src={path}
|
||||
size={size}
|
||||
onError={() => {
|
||||
BAD_IMAGES[address] = true
|
||||
setError(true)
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
140
src/components/TokenWarningCard/index.tsx
Normal file
@@ -0,0 +1,140 @@
|
||||
import { Token } from '@uniswap/sdk'
|
||||
import { transparentize } from 'polished'
|
||||
import React, { useMemo } from 'react'
|
||||
import styled from 'styled-components'
|
||||
import { ReactComponent as Close } from '../../assets/images/x.svg'
|
||||
import { ALL_TOKENS } from '../../constants/tokens'
|
||||
import { useActiveWeb3React } from '../../hooks'
|
||||
import { useAllTokens } from '../../hooks/Tokens'
|
||||
import { Field } from '../../state/swap/actions'
|
||||
import { useTokenWarningDismissal } from '../../state/user/hooks'
|
||||
import { ExternalLink, TYPE } from '../../theme'
|
||||
import { getEtherscanLink } from '../../utils'
|
||||
import PropsOfExcluding from '../../utils/props-of-excluding'
|
||||
import QuestionHelper from '../QuestionHelper'
|
||||
import TokenLogo from '../TokenLogo'
|
||||
|
||||
const Wrapper = styled.div<{ error: boolean }>`
|
||||
background: ${({ theme, error }) => transparentize(0.9, error ? theme.red1 : theme.yellow1)};
|
||||
position: relative;
|
||||
padding: 1rem;
|
||||
/* border: 0.5px solid ${({ theme, error }) => transparentize(0.4, error ? theme.red1 : theme.yellow1)}; */
|
||||
border-radius: 10px;
|
||||
margin-bottom: 20px;
|
||||
display: grid;
|
||||
grid-template-rows: 14px auto auto;
|
||||
grid-row-gap: 14px;
|
||||
`
|
||||
|
||||
const Row = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-items: flex-start;
|
||||
& > * {
|
||||
margin-right: 6px;
|
||||
}
|
||||
`
|
||||
|
||||
const CloseColor = styled(Close)`
|
||||
color: #aeaeae;
|
||||
`
|
||||
|
||||
const CloseIcon = styled.div`
|
||||
position: absolute;
|
||||
right: 1rem;
|
||||
top: 12px;
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
& > * {
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
}
|
||||
`
|
||||
|
||||
const HELP_TEXT = `
|
||||
The Uniswap V2 smart contracts are designed to support any ERC20 token on Ethereum. Any token can be
|
||||
loaded into the interface by entering its Ethereum address into the search field or passing it as a URL
|
||||
parameter.
|
||||
`
|
||||
|
||||
const DUPLICATE_NAME_HELP_TEXT = `${HELP_TEXT} This token has the same name or symbol as another token in your list.`
|
||||
|
||||
interface TokenWarningCardProps extends PropsOfExcluding<typeof Wrapper, 'error'> {
|
||||
token?: Token
|
||||
}
|
||||
|
||||
export default function TokenWarningCard({ token, ...rest }: TokenWarningCardProps) {
|
||||
const { chainId } = useActiveWeb3React()
|
||||
const isDefaultToken = Boolean(
|
||||
token && token.address && chainId && ALL_TOKENS[chainId] && ALL_TOKENS[chainId][token.address]
|
||||
)
|
||||
|
||||
const tokenSymbol = token?.symbol?.toLowerCase() ?? ''
|
||||
const tokenName = token?.name?.toLowerCase() ?? ''
|
||||
|
||||
const [dismissed, dismissTokenWarning] = useTokenWarningDismissal(chainId, token)
|
||||
|
||||
const allTokens = useAllTokens()
|
||||
|
||||
const duplicateNameOrSymbol = useMemo(() => {
|
||||
if (isDefaultToken || !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
|
||||
})
|
||||
}, [isDefaultToken, token, chainId, allTokens, tokenSymbol, tokenName])
|
||||
|
||||
if (isDefaultToken || !token || dismissed) return null
|
||||
|
||||
return (
|
||||
<Wrapper error={duplicateNameOrSymbol} {...rest}>
|
||||
{duplicateNameOrSymbol ? null : (
|
||||
<CloseIcon onClick={dismissTokenWarning}>
|
||||
<CloseColor />
|
||||
</CloseIcon>
|
||||
)}
|
||||
<Row>
|
||||
<TYPE.subHeader>{duplicateNameOrSymbol ? 'Duplicate token name or symbol' : 'Imported token'}</TYPE.subHeader>
|
||||
<QuestionHelper text={duplicateNameOrSymbol ? DUPLICATE_NAME_HELP_TEXT : HELP_TEXT} />
|
||||
</Row>
|
||||
<Row>
|
||||
<TokenLogo address={token.address} />
|
||||
<div style={{ fontWeight: 500 }}>
|
||||
{token && token.name && token.symbol && token.name !== token.symbol
|
||||
? `${token.name} (${token.symbol})`
|
||||
: token.name || token.symbol}
|
||||
</div>
|
||||
<ExternalLink style={{ fontWeight: 400 }} href={getEtherscanLink(chainId, token.address, 'address')}>
|
||||
(View on Etherscan)
|
||||
</ExternalLink>
|
||||
</Row>
|
||||
<Row>
|
||||
<TYPE.italic>Verify this is the correct token before making any transactions.</TYPE.italic>
|
||||
</Row>
|
||||
</Wrapper>
|
||||
)
|
||||
}
|
||||
|
||||
const WarningContainer = styled.div`
|
||||
max-width: 420px;
|
||||
width: 100%;
|
||||
padding-left: 1rem;
|
||||
padding-right: 1rem;
|
||||
`
|
||||
|
||||
export function TokenWarningCards({ tokens }: { tokens: { [field in Field]?: Token } }) {
|
||||
return (
|
||||
<WarningContainer>
|
||||
{Object.keys(tokens).map(field =>
|
||||
tokens[field] ? <TokenWarningCard style={{ marginBottom: 14 }} key={field} token={tokens[field]} /> : null
|
||||
)}
|
||||
</WarningContainer>
|
||||
)
|
||||
}
|
||||
18
src/components/Tooltip/index.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
import React from 'react'
|
||||
import styled from 'styled-components'
|
||||
import Popover, { PopoverProps } from '../Popover'
|
||||
|
||||
const TooltipContainer = styled.div`
|
||||
width: 228px;
|
||||
padding: 0.6rem 1rem;
|
||||
line-height: 150%;
|
||||
font-weight: 400;
|
||||
`
|
||||
|
||||
interface TooltipProps extends Omit<PopoverProps, 'content'> {
|
||||
text: string
|
||||
}
|
||||
|
||||
export default function Tooltip({ text, ...rest }: TooltipProps) {
|
||||
return <Popover content={<TooltipContainer>{text}</TooltipContainer>} {...rest} />
|
||||
}
|
||||
@@ -1,767 +0,0 @@
|
||||
import React, { useState, useEffect, useRef, useCallback } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled, { css, keyframes } from 'styled-components'
|
||||
import { darken, lighten } from 'polished'
|
||||
import { isAddress, amountFormatter } from '../../utils'
|
||||
import { useDebounce } from '../../hooks'
|
||||
|
||||
import question from '../../assets/images/question.svg'
|
||||
|
||||
import NewContextualInfo from '../../components/ContextualInfoNew'
|
||||
|
||||
const WARNING_TYPE = Object.freeze({
|
||||
none: 'none',
|
||||
emptyInput: 'emptyInput',
|
||||
invalidEntryBound: 'invalidEntryBound',
|
||||
riskyEntryHigh: 'riskyEntryHigh',
|
||||
riskyEntryLow: 'riskyEntryLow'
|
||||
})
|
||||
|
||||
const Flex = styled.div`
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
`
|
||||
|
||||
const FlexBetween = styled.div`
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
`
|
||||
|
||||
const WrappedSlippageRow = ({ wrap, ...rest }) => <Flex {...rest} />
|
||||
const SlippageRow = styled(WrappedSlippageRow)`
|
||||
position: relative;
|
||||
flex-wrap: ${({ wrap }) => wrap && 'wrap'};
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
padding-top: ${({ wrap }) => wrap && '0.25rem'};
|
||||
`
|
||||
|
||||
const QuestionWrapper = styled.button`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
margin-left: 0.4rem;
|
||||
padding: 0.2rem;
|
||||
border: none;
|
||||
background: none;
|
||||
outline: none;
|
||||
cursor: default;
|
||||
border-radius: 36px;
|
||||
|
||||
:hover,
|
||||
:focus {
|
||||
opacity: 0.7;
|
||||
}
|
||||
`
|
||||
|
||||
const HelpCircleStyled = styled.img`
|
||||
height: 18px;
|
||||
width: 18px;
|
||||
`
|
||||
|
||||
const fadeIn = keyframes`
|
||||
from {
|
||||
opacity : 0;
|
||||
}
|
||||
|
||||
to {
|
||||
opacity : 1;
|
||||
}
|
||||
`
|
||||
|
||||
const Popup = styled(Flex)`
|
||||
position: absolute;
|
||||
width: 228px;
|
||||
left: -78px;
|
||||
top: -94px;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 0.6rem 1rem;
|
||||
line-height: 150%;
|
||||
background: ${({ theme }) => theme.inputBackground};
|
||||
border: 1px solid ${({ theme }) => theme.mercuryGray};
|
||||
|
||||
border-radius: 8px;
|
||||
|
||||
animation: ${fadeIn} 0.15s linear;
|
||||
|
||||
color: ${({ theme }) => theme.textColor};
|
||||
font-style: italic;
|
||||
|
||||
${({ theme }) => theme.mediaWidth.upToSmall`
|
||||
left: -20px;
|
||||
`}
|
||||
`
|
||||
|
||||
const FancyButton = styled.button`
|
||||
color: ${({ theme }) => theme.textColor};
|
||||
align-items: center;
|
||||
min-width: 55px;
|
||||
height: 2rem;
|
||||
border-radius: 36px;
|
||||
font-size: 12px;
|
||||
border: 1px solid ${({ theme }) => theme.mercuryGray};
|
||||
outline: none;
|
||||
background: ${({ theme }) => theme.inputBackground};
|
||||
|
||||
:hover {
|
||||
cursor: inherit;
|
||||
border: 1px solid ${({ theme }) => theme.chaliceGray};
|
||||
}
|
||||
:focus {
|
||||
border: 1px solid ${({ theme }) => theme.royalBlue};
|
||||
}
|
||||
`
|
||||
|
||||
const Option = styled(FancyButton)`
|
||||
margin-right: 8px;
|
||||
margin-top: 6px;
|
||||
|
||||
:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
${({ active, theme }) =>
|
||||
active &&
|
||||
css`
|
||||
background-color: ${({ theme }) => theme.royalBlue};
|
||||
color: ${({ theme }) => theme.white};
|
||||
border: none;
|
||||
|
||||
:hover {
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
background-color: ${({ theme }) => darken(0.05, theme.royalBlue)};
|
||||
}
|
||||
|
||||
:focus {
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
background-color: ${({ theme }) => lighten(0.05, theme.royalBlue)};
|
||||
}
|
||||
|
||||
:active {
|
||||
background-color: ${({ theme }) => darken(0.05, theme.royalBlue)};
|
||||
}
|
||||
|
||||
:hover:focus {
|
||||
background-color: ${({ theme }) => theme.royalBlue};
|
||||
}
|
||||
:hover:focus:active {
|
||||
background-color: ${({ theme }) => darken(0.05, theme.royalBlue)};
|
||||
}
|
||||
`}
|
||||
`
|
||||
|
||||
const OptionLarge = styled(Option)`
|
||||
width: 120px;
|
||||
`
|
||||
|
||||
const Input = styled.input`
|
||||
background: ${({ theme }) => theme.inputBackground};
|
||||
flex-grow: 1;
|
||||
font-size: 12px;
|
||||
min-width: 20px;
|
||||
|
||||
outline: none;
|
||||
box-sizing: border-box;
|
||||
|
||||
&::-webkit-outer-spin-button,
|
||||
&::-webkit-inner-spin-button {
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
cursor: inherit;
|
||||
|
||||
color: ${({ theme }) => theme.doveGray};
|
||||
text-align: left;
|
||||
${({ active }) =>
|
||||
active &&
|
||||
css`
|
||||
color: initial;
|
||||
cursor: initial;
|
||||
text-align: right;
|
||||
`}
|
||||
|
||||
${({ placeholder }) =>
|
||||
placeholder !== 'Custom' &&
|
||||
css`
|
||||
text-align: right;
|
||||
color: ${({ theme }) => theme.textColor};
|
||||
`}
|
||||
|
||||
${({ color }) =>
|
||||
color === 'red' &&
|
||||
css`
|
||||
color: ${({ theme }) => theme.salmonRed};
|
||||
`}
|
||||
`
|
||||
|
||||
const BottomError = styled.div`
|
||||
${({ show }) =>
|
||||
show &&
|
||||
css`
|
||||
padding-top: 12px;
|
||||
`}
|
||||
color: ${({ theme }) => theme.doveGray};
|
||||
${({ color }) =>
|
||||
color === 'red' &&
|
||||
css`
|
||||
color: ${({ theme }) => theme.salmonRed};
|
||||
`}
|
||||
`
|
||||
|
||||
const OptionCustom = styled(FancyButton)`
|
||||
height: 2rem;
|
||||
position: relative;
|
||||
width: 120px;
|
||||
margin-top: 6px;
|
||||
padding: 0 0.75rem;
|
||||
|
||||
${({ active }) =>
|
||||
active &&
|
||||
css`
|
||||
border: 1px solid ${({ theme }) => theme.royalBlue};
|
||||
:hover {
|
||||
border: 1px solid ${({ theme }) => darken(0.1, theme.royalBlue)};
|
||||
}
|
||||
`}
|
||||
|
||||
${({ color }) =>
|
||||
color === 'red' &&
|
||||
css`
|
||||
border: 1px solid ${({ theme }) => theme.salmonRed};
|
||||
`}
|
||||
|
||||
input {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: 0px;
|
||||
border-radius: 2rem;
|
||||
}
|
||||
`
|
||||
|
||||
const Bold = styled.span`
|
||||
font-weight: 500;
|
||||
`
|
||||
|
||||
const LastSummaryText = styled.div`
|
||||
padding-top: 0.5rem;
|
||||
`
|
||||
|
||||
const SlippageSelector = styled.div`
|
||||
background-color: ${({ theme }) => darken(0.04, theme.concreteGray)};
|
||||
padding: 1rem 1.25rem 1rem 1.25rem;
|
||||
border-radius: 12px 12px 0 0;
|
||||
`
|
||||
|
||||
const Percent = styled.div`
|
||||
color: inherit;
|
||||
font-size: 0, 8rem;
|
||||
flex-grow: 0;
|
||||
|
||||
${({ color, theme }) =>
|
||||
(color === 'faded' &&
|
||||
css`
|
||||
color: ${theme.doveGray};
|
||||
`) ||
|
||||
(color === 'red' &&
|
||||
css`
|
||||
color: ${theme.salmonRed};
|
||||
`)};
|
||||
`
|
||||
|
||||
const Faded = styled.span`
|
||||
opacity: 0.7;
|
||||
`
|
||||
|
||||
const TransactionInfo = styled.div`
|
||||
padding: 1.25rem 1.25rem 1rem 1.25rem;
|
||||
`
|
||||
|
||||
const ValueWrapper = styled.span`
|
||||
padding: 0.125rem 0.3rem 0.1rem 0.3rem;
|
||||
background-color: ${({ theme }) => darken(0.04, theme.concreteGray)};
|
||||
border-radius: 12px;
|
||||
font-variant: tabular-nums;
|
||||
`
|
||||
|
||||
const DeadlineSelector = styled.div`
|
||||
background-color: ${({ theme }) => darken(0.04, theme.concreteGray)};
|
||||
padding: 1rem 1.25rem 1rem 1.25rem;
|
||||
border-radius: 0 0 12px 12px;
|
||||
`
|
||||
const DeadlineRow = SlippageRow
|
||||
const DeadlineInput = OptionCustom
|
||||
|
||||
export default function TransactionDetails(props) {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const [activeIndex, setActiveIndex] = useState(3)
|
||||
|
||||
const [warningType, setWarningType] = useState(WARNING_TYPE.none)
|
||||
|
||||
const inputRef = useRef()
|
||||
|
||||
const [showPopup, setPopup] = useState(false)
|
||||
|
||||
const [userInput, setUserInput] = useState('')
|
||||
const debouncedInput = useDebounce(userInput, 150)
|
||||
|
||||
useEffect(() => {
|
||||
if (activeIndex === 4) {
|
||||
checkBounds(debouncedInput)
|
||||
}
|
||||
})
|
||||
|
||||
const [deadlineInput, setDeadlineInput] = useState('')
|
||||
|
||||
function renderSummary() {
|
||||
let contextualInfo = ''
|
||||
let isError = false
|
||||
if (props.brokenTokenWarning) {
|
||||
contextualInfo = t('brokenToken')
|
||||
isError = true
|
||||
} else if (props.inputError || props.independentError) {
|
||||
contextualInfo = props.inputError || props.independentError
|
||||
isError = true
|
||||
} else if (!props.inputCurrency || !props.outputCurrency) {
|
||||
contextualInfo = t('selectTokenCont')
|
||||
} else if (!props.independentValue) {
|
||||
contextualInfo = t('enterValueCont')
|
||||
} else if (props.sending && !props.recipientAddress) {
|
||||
contextualInfo = t('noRecipient')
|
||||
} else if (props.sending && !isAddress(props.recipientAddress)) {
|
||||
contextualInfo = t('invalidRecipient')
|
||||
} else if (!props.account) {
|
||||
contextualInfo = t('noWallet')
|
||||
isError = true
|
||||
}
|
||||
|
||||
const slippageWarningText = props.highSlippageWarning
|
||||
? t('highSlippageWarning')
|
||||
: props.slippageWarning
|
||||
? t('slippageWarning')
|
||||
: ''
|
||||
|
||||
return (
|
||||
<NewContextualInfo
|
||||
openDetailsText={t('transactionDetails')}
|
||||
closeDetailsText={t('hideDetails')}
|
||||
contextualInfo={contextualInfo ? contextualInfo : slippageWarningText}
|
||||
allowExpand={
|
||||
!!(
|
||||
!props.brokenTokenWarning &&
|
||||
props.inputCurrency &&
|
||||
props.outputCurrency &&
|
||||
props.inputValueParsed &&
|
||||
props.outputValueParsed &&
|
||||
(props.sending ? props.recipientAddress : true)
|
||||
)
|
||||
}
|
||||
isError={isError}
|
||||
slippageWarning={props.slippageWarning && !contextualInfo}
|
||||
highSlippageWarning={props.highSlippageWarning && !contextualInfo}
|
||||
brokenTokenWarning={props.brokenTokenWarning}
|
||||
renderTransactionDetails={renderTransactionDetails}
|
||||
dropDownContent={dropDownContent}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const dropDownContent = () => {
|
||||
return (
|
||||
<>
|
||||
{renderTransactionDetails()}
|
||||
<SlippageSelector>
|
||||
<SlippageRow>
|
||||
Limit additional price slippage
|
||||
<QuestionWrapper
|
||||
onClick={() => {
|
||||
setPopup(!showPopup)
|
||||
}}
|
||||
onMouseEnter={() => {
|
||||
setPopup(true)
|
||||
}}
|
||||
onMouseLeave={() => {
|
||||
setPopup(false)
|
||||
}}
|
||||
>
|
||||
<HelpCircleStyled src={question} alt="popup" />
|
||||
</QuestionWrapper>
|
||||
{showPopup ? (
|
||||
<Popup>
|
||||
Lowering this limit decreases your risk of frontrunning. However, this makes it more likely that your
|
||||
transaction will fail due to normal price movements.
|
||||
</Popup>
|
||||
) : (
|
||||
''
|
||||
)}
|
||||
</SlippageRow>
|
||||
<SlippageRow wrap>
|
||||
<Option
|
||||
onClick={() => {
|
||||
setFromFixed(1, 0.1)
|
||||
}}
|
||||
active={activeIndex === 1}
|
||||
>
|
||||
0.1%
|
||||
</Option>
|
||||
<OptionLarge
|
||||
onClick={() => {
|
||||
setFromFixed(2, 0.5)
|
||||
}}
|
||||
active={activeIndex === 2}
|
||||
>
|
||||
0.5% <Faded>(suggested)</Faded>
|
||||
</OptionLarge>
|
||||
<Option
|
||||
onClick={() => {
|
||||
setFromFixed(3, 1)
|
||||
}}
|
||||
active={activeIndex === 3}
|
||||
>
|
||||
1%
|
||||
</Option>
|
||||
<OptionCustom
|
||||
active={activeIndex === 4}
|
||||
color={
|
||||
warningType === WARNING_TYPE.emptyInput
|
||||
? ''
|
||||
: warningType !== WARNING_TYPE.none && warningType !== WARNING_TYPE.riskyEntryLow
|
||||
? 'red'
|
||||
: ''
|
||||
}
|
||||
onClick={() => {
|
||||
setFromCustom()
|
||||
}}
|
||||
>
|
||||
<FlexBetween>
|
||||
{!(warningType === WARNING_TYPE.none || warningType === WARNING_TYPE.emptyInput) && (
|
||||
<span role="img" aria-label="warning">
|
||||
⚠️
|
||||
</span>
|
||||
)}
|
||||
<Input
|
||||
tabIndex={-1}
|
||||
ref={inputRef}
|
||||
active={activeIndex === 4}
|
||||
placeholder={
|
||||
activeIndex === 4
|
||||
? !!userInput
|
||||
? ''
|
||||
: '0'
|
||||
: activeIndex !== 4 && userInput !== ''
|
||||
? userInput
|
||||
: 'Custom'
|
||||
}
|
||||
value={activeIndex === 4 ? userInput : ''}
|
||||
onChange={parseInput}
|
||||
color={
|
||||
warningType === WARNING_TYPE.emptyInput
|
||||
? ''
|
||||
: warningType !== WARNING_TYPE.none && warningType !== WARNING_TYPE.riskyEntryLow
|
||||
? 'red'
|
||||
: ''
|
||||
}
|
||||
/>
|
||||
<Percent
|
||||
color={
|
||||
activeIndex !== 4
|
||||
? 'faded'
|
||||
: warningType === WARNING_TYPE.riskyEntryHigh || warningType === WARNING_TYPE.invalidEntryBound
|
||||
? 'red'
|
||||
: ''
|
||||
}
|
||||
>
|
||||
%
|
||||
</Percent>
|
||||
</FlexBetween>
|
||||
</OptionCustom>
|
||||
</SlippageRow>
|
||||
<SlippageRow>
|
||||
<BottomError
|
||||
show={activeIndex === 4}
|
||||
color={
|
||||
warningType === WARNING_TYPE.emptyInput
|
||||
? ''
|
||||
: warningType !== WARNING_TYPE.none && warningType !== WARNING_TYPE.riskyEntryLow
|
||||
? 'red'
|
||||
: ''
|
||||
}
|
||||
>
|
||||
{activeIndex === 4 && warningType.toString() === 'none' && 'Custom slippage value'}
|
||||
{warningType === WARNING_TYPE.emptyInput && 'Enter a slippage percentage'}
|
||||
{warningType === WARNING_TYPE.invalidEntryBound && 'Please select a value no greater than 50%'}
|
||||
{warningType === WARNING_TYPE.riskyEntryHigh && 'Your transaction may be frontrun'}
|
||||
{warningType === WARNING_TYPE.riskyEntryLow && 'Your transaction may fail'}
|
||||
</BottomError>
|
||||
</SlippageRow>
|
||||
</SlippageSelector>
|
||||
<DeadlineSelector>
|
||||
Set swap deadline (minutes from now)
|
||||
<DeadlineRow wrap>
|
||||
<DeadlineInput>
|
||||
<Input placeholder={'Deadline'} value={deadlineInput} onChange={parseDeadlineInput} />
|
||||
</DeadlineInput>
|
||||
</DeadlineRow>
|
||||
</DeadlineSelector>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const setFromCustom = () => {
|
||||
setActiveIndex(4)
|
||||
inputRef.current.focus()
|
||||
// if there's a value, evaluate the bounds
|
||||
checkBounds(debouncedInput)
|
||||
}
|
||||
|
||||
// destructure props for to limit effect callbacks
|
||||
const setRawSlippage = props.setRawSlippage
|
||||
const setRawTokenSlippage = props.setRawTokenSlippage
|
||||
const setcustomSlippageError = props.setcustomSlippageError
|
||||
const setDeadline = props.setDeadline
|
||||
|
||||
const updateSlippage = useCallback(
|
||||
newSlippage => {
|
||||
// round to 2 decimals to prevent ethers error
|
||||
let numParsed = parseInt(newSlippage * 100)
|
||||
|
||||
// set both slippage values in parents
|
||||
setRawSlippage(numParsed)
|
||||
setRawTokenSlippage(numParsed)
|
||||
},
|
||||
[setRawSlippage, setRawTokenSlippage]
|
||||
)
|
||||
|
||||
// used for slippage presets
|
||||
const setFromFixed = useCallback(
|
||||
(index, slippage) => {
|
||||
// update slippage in parent, reset errors and input state
|
||||
updateSlippage(slippage)
|
||||
setWarningType(WARNING_TYPE.none)
|
||||
setActiveIndex(index)
|
||||
setcustomSlippageError('valid`')
|
||||
},
|
||||
[setcustomSlippageError, updateSlippage]
|
||||
)
|
||||
|
||||
/**
|
||||
* @todo
|
||||
* Breaks without useState here, able to
|
||||
* break input parsing if typing is faster than
|
||||
* debounce time
|
||||
*/
|
||||
|
||||
const [initialSlippage] = useState(props.rawSlippage)
|
||||
|
||||
useEffect(() => {
|
||||
switch (Number.parseInt(initialSlippage)) {
|
||||
case 10:
|
||||
setFromFixed(1, 0.1)
|
||||
break
|
||||
case 50:
|
||||
setFromFixed(2, 0.5)
|
||||
break
|
||||
case 100:
|
||||
setFromFixed(3, 1)
|
||||
break
|
||||
default:
|
||||
// restrict to 2 decimal places
|
||||
let acceptableValues = [/^$/, /^\d{1,2}$/, /^\d{0,2}\.\d{0,2}$/]
|
||||
// if its within accepted decimal limit, update the input state
|
||||
if (acceptableValues.some(val => val.test(initialSlippage / 100))) {
|
||||
setUserInput(initialSlippage / 100)
|
||||
setActiveIndex(4)
|
||||
}
|
||||
}
|
||||
}, [initialSlippage, setFromFixed])
|
||||
|
||||
const checkBounds = useCallback(
|
||||
slippageValue => {
|
||||
setWarningType(WARNING_TYPE.none)
|
||||
setcustomSlippageError('valid')
|
||||
|
||||
if (slippageValue === '' || slippageValue === '.') {
|
||||
setcustomSlippageError('invalid')
|
||||
return setWarningType(WARNING_TYPE.emptyInput)
|
||||
}
|
||||
|
||||
// check bounds and set errors
|
||||
if (Number(slippageValue) < 0 || Number(slippageValue) > 50) {
|
||||
setcustomSlippageError('invalid')
|
||||
return setWarningType(WARNING_TYPE.invalidEntryBound)
|
||||
}
|
||||
if (Number(slippageValue) >= 0 && Number(slippageValue) < 0.1) {
|
||||
setcustomSlippageError('valid')
|
||||
setWarningType(WARNING_TYPE.riskyEntryLow)
|
||||
}
|
||||
if (Number(slippageValue) > 5) {
|
||||
setcustomSlippageError('warning')
|
||||
setWarningType(WARNING_TYPE.riskyEntryHigh)
|
||||
}
|
||||
//update the actual slippage value in parent
|
||||
updateSlippage(Number(slippageValue))
|
||||
},
|
||||
[setcustomSlippageError, updateSlippage]
|
||||
)
|
||||
|
||||
// check that the theyve entered number and correct decimal
|
||||
const parseInput = e => {
|
||||
let input = e.target.value
|
||||
|
||||
// restrict to 2 decimal places
|
||||
let acceptableValues = [/^$/, /^\d{1,2}$/, /^\d{0,2}\.\d{0,2}$/]
|
||||
// if its within accepted decimal limit, update the input state
|
||||
if (acceptableValues.some(a => a.test(input))) {
|
||||
setUserInput(input)
|
||||
}
|
||||
}
|
||||
|
||||
const [initialDeadline] = useState(props.deadline)
|
||||
|
||||
useEffect(() => {
|
||||
setDeadlineInput(initialDeadline / 60)
|
||||
}, [initialDeadline])
|
||||
|
||||
const parseDeadlineInput = e => {
|
||||
const input = e.target.value
|
||||
|
||||
const acceptableValues = [/^$/, /^\d+$/]
|
||||
if (acceptableValues.some(re => re.test(input))) {
|
||||
setDeadlineInput(input)
|
||||
setDeadline(parseInt(input) * 60)
|
||||
}
|
||||
}
|
||||
|
||||
const b = text => <Bold>{text}</Bold>
|
||||
|
||||
const renderTransactionDetails = () => {
|
||||
if (props.independentField === props.INPUT) {
|
||||
return props.sending ? (
|
||||
<TransactionInfo>
|
||||
<div>
|
||||
{t('youAreSelling')}{' '}
|
||||
<ValueWrapper>
|
||||
{b(
|
||||
`${amountFormatter(
|
||||
props.independentValueParsed,
|
||||
props.independentDecimals,
|
||||
Math.min(4, props.independentDecimals)
|
||||
)} ${props.inputSymbol}`
|
||||
)}
|
||||
</ValueWrapper>
|
||||
</div>
|
||||
<LastSummaryText>
|
||||
{b(props.recipientAddress)} {t('willReceive')}{' '}
|
||||
<ValueWrapper>
|
||||
{b(
|
||||
`${amountFormatter(
|
||||
props.dependentValueMinumum,
|
||||
props.dependentDecimals,
|
||||
Math.min(4, props.dependentDecimals)
|
||||
)} ${props.outputSymbol}`
|
||||
)}
|
||||
</ValueWrapper>{' '}
|
||||
</LastSummaryText>
|
||||
<LastSummaryText>
|
||||
{t('priceChange')} <ValueWrapper>{b(`${props.percentSlippageFormatted}%`)}</ValueWrapper>
|
||||
</LastSummaryText>
|
||||
</TransactionInfo>
|
||||
) : (
|
||||
<TransactionInfo>
|
||||
<div>
|
||||
{t('youAreSelling')}{' '}
|
||||
<ValueWrapper>
|
||||
{b(
|
||||
`${amountFormatter(
|
||||
props.independentValueParsed,
|
||||
props.independentDecimals,
|
||||
Math.min(4, props.independentDecimals)
|
||||
)} ${props.inputSymbol}`
|
||||
)}
|
||||
</ValueWrapper>{' '}
|
||||
{t('forAtLeast')}
|
||||
<ValueWrapper>
|
||||
{b(
|
||||
`${amountFormatter(
|
||||
props.dependentValueMinumum,
|
||||
props.dependentDecimals,
|
||||
Math.min(4, props.dependentDecimals)
|
||||
)} ${props.outputSymbol}`
|
||||
)}
|
||||
</ValueWrapper>
|
||||
</div>
|
||||
<LastSummaryText>
|
||||
{t('priceChange')} <ValueWrapper>{b(`${props.percentSlippageFormatted}%`)}</ValueWrapper>
|
||||
</LastSummaryText>
|
||||
</TransactionInfo>
|
||||
)
|
||||
} else {
|
||||
return props.sending ? (
|
||||
<TransactionInfo>
|
||||
<div>
|
||||
{t('youAreSending')}{' '}
|
||||
<ValueWrapper>
|
||||
{b(
|
||||
`${amountFormatter(
|
||||
props.independentValueParsed,
|
||||
props.independentDecimals,
|
||||
Math.min(4, props.independentDecimals)
|
||||
)} ${props.outputSymbol}`
|
||||
)}
|
||||
</ValueWrapper>{' '}
|
||||
{t('to')} {b(props.recipientAddress)} {t('forAtMost')}{' '}
|
||||
<ValueWrapper>
|
||||
{b(
|
||||
`${amountFormatter(
|
||||
props.dependentValueMaximum,
|
||||
props.dependentDecimals,
|
||||
Math.min(4, props.dependentDecimals)
|
||||
)} ${props.inputSymbol}`
|
||||
)}
|
||||
</ValueWrapper>{' '}
|
||||
</div>
|
||||
<LastSummaryText>
|
||||
{t('priceChange')} <ValueWrapper>{b(`${props.percentSlippageFormatted}%`)}</ValueWrapper>
|
||||
</LastSummaryText>
|
||||
</TransactionInfo>
|
||||
) : (
|
||||
<TransactionInfo>
|
||||
{t('youAreBuying')}{' '}
|
||||
<ValueWrapper>
|
||||
{b(
|
||||
`${amountFormatter(
|
||||
props.independentValueParsed,
|
||||
props.independentDecimals,
|
||||
Math.min(4, props.independentDecimals)
|
||||
)} ${props.outputSymbol}`
|
||||
)}
|
||||
</ValueWrapper>{' '}
|
||||
{t('forAtMost')}{' '}
|
||||
<ValueWrapper>
|
||||
{b(
|
||||
`${amountFormatter(
|
||||
props.dependentValueMaximum,
|
||||
props.dependentDecimals,
|
||||
Math.min(4, props.dependentDecimals)
|
||||
)} ${props.inputSymbol}`
|
||||
)}
|
||||
</ValueWrapper>{' '}
|
||||
<LastSummaryText>
|
||||
{t('priceChange')} <ValueWrapper>{b(`${props.percentSlippageFormatted}%`)}</ValueWrapper>
|
||||
</LastSummaryText>
|
||||
</TransactionInfo>
|
||||
)
|
||||
}
|
||||
}
|
||||
return <>{renderSummary()}</>
|
||||
}
|
||||
70
src/components/TxnPopup/index.tsx
Normal file
@@ -0,0 +1,70 @@
|
||||
import React, { useCallback, useState } from 'react'
|
||||
import { AlertCircle, CheckCircle } from 'react-feather'
|
||||
|
||||
import styled from 'styled-components'
|
||||
|
||||
import { useActiveWeb3React } from '../../hooks'
|
||||
import useInterval from '../../hooks/useInterval'
|
||||
import { useRemovePopup } from '../../state/application/hooks'
|
||||
import { TYPE } from '../../theme'
|
||||
|
||||
import { ExternalLink } from '../../theme/components'
|
||||
import { getEtherscanLink } from '../../utils'
|
||||
import { AutoColumn } from '../Column'
|
||||
import { AutoRow } from '../Row'
|
||||
|
||||
const Fader = styled.div<{ count: number }>`
|
||||
position: absolute;
|
||||
bottom: 0px;
|
||||
left: 0px;
|
||||
width: ${({ count }) => `calc(100% - (100% / ${150 / count}))`};
|
||||
height: 2px;
|
||||
background-color: ${({ theme }) => theme.bg3};
|
||||
transition: width 100ms linear;
|
||||
`
|
||||
|
||||
const delay = 100
|
||||
|
||||
export default function TxnPopup({
|
||||
hash,
|
||||
success,
|
||||
summary,
|
||||
popKey
|
||||
}: {
|
||||
hash: string
|
||||
success?: boolean
|
||||
summary?: string
|
||||
popKey?: string
|
||||
}) {
|
||||
const { chainId } = useActiveWeb3React()
|
||||
const [count, setCount] = useState(1)
|
||||
|
||||
const [isRunning, setIsRunning] = useState(true)
|
||||
const removePopup = useRemovePopup()
|
||||
|
||||
const removeThisPopup = useCallback(() => removePopup(popKey), [popKey, removePopup])
|
||||
|
||||
useInterval(
|
||||
() => {
|
||||
count > 150 ? removeThisPopup() : setCount(count + 1)
|
||||
},
|
||||
isRunning ? delay : null
|
||||
)
|
||||
|
||||
return (
|
||||
<AutoRow onMouseEnter={() => setIsRunning(false)} onMouseLeave={() => setIsRunning(true)}>
|
||||
{success ? (
|
||||
<CheckCircle color={'#27AE60'} size={24} style={{ paddingRight: '24px' }} />
|
||||
) : (
|
||||
<AlertCircle color={'#FF6871'} size={24} style={{ paddingRight: '24px' }} />
|
||||
)}
|
||||
<AutoColumn gap="8px">
|
||||
<TYPE.body fontWeight={500}>
|
||||
{summary ? summary : 'Hash: ' + hash.slice(0, 8) + '...' + hash.slice(58, 65)}
|
||||
</TYPE.body>
|
||||
<ExternalLink href={getEtherscanLink(chainId, hash, 'transaction')}>View on Etherscan</ExternalLink>
|
||||
</AutoColumn>
|
||||
<Fader count={count} />
|
||||
</AutoRow>
|
||||
)
|
||||
}
|
||||
@@ -1,18 +1,18 @@
|
||||
import React from 'react'
|
||||
import styled from 'styled-components'
|
||||
import { Link } from '../../theme'
|
||||
import { ExternalLink } from '../../theme'
|
||||
|
||||
const InfoCard = styled.button`
|
||||
background-color: ${({ theme, active }) => (active ? theme.activeGray : theme.backgroundColor)};
|
||||
const InfoCard = styled.button<{ active?: boolean }>`
|
||||
background-color: ${({ theme, active }) => (active ? theme.bg3 : theme.bg2)};
|
||||
padding: 1rem;
|
||||
outline: none;
|
||||
border: 1px solid;
|
||||
border-radius: 12px;
|
||||
width: 100% !important;
|
||||
&:focus {
|
||||
box-shadow: 0 0 0 1px ${({ theme }) => theme.royalBlue};
|
||||
box-shadow: 0 0 0 1px ${({ theme }) => theme.primary1};
|
||||
}
|
||||
border-color: ${({ theme, active }) => (active ? 'transparent' : theme.placeholderGray)};
|
||||
border-color: ${({ theme, active }) => (active ? 'transparent' : theme.bg3)};
|
||||
`
|
||||
|
||||
const OptionCard = styled(InfoCard)`
|
||||
@@ -30,11 +30,11 @@ const OptionCardLeft = styled.div`
|
||||
height: 100%;
|
||||
`
|
||||
|
||||
const OptionCardClickable = styled(OptionCard)`
|
||||
const OptionCardClickable = styled(OptionCard)<{ clickable?: boolean }>`
|
||||
margin-top: 0;
|
||||
&:hover {
|
||||
cursor: ${({ clickable }) => (clickable ? 'pointer' : '')};
|
||||
border: ${({ clickable, theme }) => (clickable ? `1px solid ${theme.malibuBlue}` : ``)};
|
||||
border: ${({ clickable, theme }) => (clickable ? `1px solid ${theme.primary1}` : ``)};
|
||||
}
|
||||
opacity: ${({ disabled }) => (disabled ? '0.5' : '1')};
|
||||
`
|
||||
@@ -48,13 +48,13 @@ const GreenCircle = styled.div`
|
||||
height: 8px;
|
||||
width: 8px;
|
||||
margin-right: 8px;
|
||||
background-color: ${({ theme }) => theme.connectedGreen};
|
||||
background-color: ${({ theme }) => theme.green1};
|
||||
border-radius: 50%;
|
||||
}
|
||||
`
|
||||
|
||||
const CircleWrapper = styled.div`
|
||||
color: ${({ theme }) => theme.connectedGreen};
|
||||
color: ${({ theme }) => theme.green1};
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
@@ -62,18 +62,18 @@ const CircleWrapper = styled.div`
|
||||
|
||||
const HeaderText = styled.div`
|
||||
${({ theme }) => theme.flexRowNoWrap};
|
||||
color: ${props => (props.color === 'blue' ? ({ theme }) => theme.royalBlue : ({ theme }) => theme.textColor)};
|
||||
color: ${props => (props.color === 'blue' ? ({ theme }) => theme.primary1 : ({ theme }) => theme.text1)};
|
||||
font-size: 1rem;
|
||||
font-weight: 500;
|
||||
`
|
||||
|
||||
const SubHeader = styled.div`
|
||||
color: ${({ theme }) => theme.textColor};
|
||||
color: ${({ theme }) => theme.text1};
|
||||
margin-top: 10px;
|
||||
font-size: 12px;
|
||||
`
|
||||
|
||||
const IconWrapper = styled.div`
|
||||
const IconWrapper = styled.div<{ size?: number }>`
|
||||
${({ theme }) => theme.flexColumnNoWrap};
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@@ -96,10 +96,22 @@ export default function Option({
|
||||
header,
|
||||
subheader = null,
|
||||
icon,
|
||||
active = false
|
||||
active = false,
|
||||
id
|
||||
}: {
|
||||
link?: string | null
|
||||
clickable?: boolean
|
||||
size?: number | null
|
||||
onClick?: null | (() => void)
|
||||
color: string
|
||||
header: React.ReactNode
|
||||
subheader: React.ReactNode | null
|
||||
icon: string
|
||||
active?: boolean
|
||||
id: string
|
||||
}) {
|
||||
const content = (
|
||||
<OptionCardClickable onClick={onClick} clickable={clickable && !active} active={active}>
|
||||
<OptionCardClickable id={id} onClick={onClick} clickable={clickable && !active} active={active}>
|
||||
<OptionCardLeft>
|
||||
<HeaderText color={color}>
|
||||
{' '}
|
||||
@@ -116,13 +128,13 @@ export default function Option({
|
||||
</HeaderText>
|
||||
{subheader && <SubHeader>{subheader}</SubHeader>}
|
||||
</OptionCardLeft>
|
||||
<IconWrapper size={size} active={active}>
|
||||
<IconWrapper size={size}>
|
||||
<img src={icon} alt={'Icon'} />
|
||||
</IconWrapper>
|
||||
</OptionCardClickable>
|
||||
)
|
||||
if (link) {
|
||||
return <Link href={link}>{content}</Link>
|
||||
return <ExternalLink href={link}>{content}</ExternalLink>
|
||||
}
|
||||
|
||||
return content
|
||||
@@ -1,12 +1,12 @@
|
||||
import { AbstractConnector } from '@web3-react/abstract-connector'
|
||||
import React from 'react'
|
||||
import styled from 'styled-components'
|
||||
import Option from './Option'
|
||||
import { SUPPORTED_WALLETS } from '../../constants'
|
||||
import WalletConnectData from './WalletConnectData'
|
||||
import { walletconnect, injected } from '../../connectors'
|
||||
import { Spinner } from '../../theme'
|
||||
import Circle from '../../assets/images/circle.svg'
|
||||
import { darken } from 'polished'
|
||||
import Loader from '../Loader'
|
||||
|
||||
const PendingSection = styled.div`
|
||||
${({ theme }) => theme.flexColumnNoWrap};
|
||||
@@ -18,24 +18,18 @@ const PendingSection = styled.div`
|
||||
}
|
||||
`
|
||||
|
||||
const SpinnerWrapper = styled(Spinner)`
|
||||
font-size: 4rem;
|
||||
const StyledLoader = styled(Loader)`
|
||||
margin-right: 1rem;
|
||||
svg {
|
||||
path {
|
||||
color: ${({ theme }) => theme.placeholderGray};
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const LoadingMessage = styled.div`
|
||||
const LoadingMessage = styled.div<{ error?: boolean }>`
|
||||
${({ theme }) => theme.flexRowNoWrap};
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
border-radius: 12px;
|
||||
margin-bottom: 20px;
|
||||
color: ${({ theme, error }) => (error ? theme.salmonRed : 'inherit')};
|
||||
border: 1px solid ${({ theme, error }) => (error ? theme.salmonRed : theme.placeholderGray)};
|
||||
color: ${({ theme, error }) => (error ? theme.red1 : 'inherit')};
|
||||
border: 1px solid ${({ theme, error }) => (error ? theme.red1 : theme.text4)};
|
||||
|
||||
& > * {
|
||||
padding: 1rem;
|
||||
@@ -51,8 +45,8 @@ const ErrorGroup = styled.div`
|
||||
const ErrorButton = styled.div`
|
||||
border-radius: 8px;
|
||||
font-size: 12px;
|
||||
color: ${({ theme }) => theme.textColor};
|
||||
background-color: ${({ theme }) => theme.placeholderGray};
|
||||
color: ${({ theme }) => theme.text1};
|
||||
background-color: ${({ theme }) => theme.bg4};
|
||||
margin-left: 1rem;
|
||||
padding: 0.5rem;
|
||||
font-weight: 600;
|
||||
@@ -60,7 +54,7 @@ const ErrorButton = styled.div`
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
background-color: ${({ theme }) => darken(0.1, theme.placeholderGray)};
|
||||
background-color: ${({ theme }) => darken(0.1, theme.text4)};
|
||||
}
|
||||
`
|
||||
|
||||
@@ -70,7 +64,21 @@ const LoadingWrapper = styled.div`
|
||||
justify-content: center;
|
||||
`
|
||||
|
||||
export default function PendingView({ uri = '', size, connector, error = false, setPendingError, tryActivation }) {
|
||||
export default function PendingView({
|
||||
uri = '',
|
||||
size,
|
||||
connector,
|
||||
error = false,
|
||||
setPendingError,
|
||||
tryActivation
|
||||
}: {
|
||||
uri?: string
|
||||
size?: number
|
||||
connector?: AbstractConnector
|
||||
error?: boolean
|
||||
setPendingError: (error: boolean) => void
|
||||
tryActivation: (connector: AbstractConnector) => void
|
||||
}) {
|
||||
const isMetamask = window.ethereum && window.ethereum.isMetaMask
|
||||
|
||||
return (
|
||||
@@ -78,7 +86,7 @@ export default function PendingView({ uri = '', size, connector, error = false,
|
||||
{!error && connector === walletconnect && <WalletConnectData size={size} uri={uri} />}
|
||||
<LoadingMessage error={error}>
|
||||
<LoadingWrapper>
|
||||
{!error && <SpinnerWrapper src={Circle} />}
|
||||
{!error && <StyledLoader />}
|
||||
{error ? (
|
||||
<ErrorGroup>
|
||||
<div>Error connecting.</div>
|
||||
@@ -111,6 +119,7 @@ export default function PendingView({ uri = '', size, connector, error = false,
|
||||
}
|
||||
return (
|
||||
<Option
|
||||
id={`connect-${key}`}
|
||||
key={key}
|
||||
clickable={false}
|
||||
color={option.color}
|
||||
@@ -1,23 +0,0 @@
|
||||
import React from 'react'
|
||||
import styled from 'styled-components'
|
||||
import QRCode from 'qrcode.react'
|
||||
import { useDarkModeManager } from '../../contexts/LocalStorage'
|
||||
|
||||
const QRCodeWrapper = styled.div`
|
||||
${({ theme }) => theme.flexColumnNoWrap};
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 12px;
|
||||
margin-bottom: 20px;
|
||||
`
|
||||
|
||||
export default function WalletConnectData({ uri = '', size }) {
|
||||
const [isDark] = useDarkModeManager()
|
||||
return (
|
||||
<QRCodeWrapper>
|
||||
{uri && (
|
||||
<QRCode size={size} value={uri} bgColor={isDark ? '#333639' : 'white'} fgColor={isDark ? 'white' : 'black'} />
|
||||
)}
|
||||
</QRCodeWrapper>
|
||||
)
|
||||
}
|
||||
20
src/components/WalletModal/WalletConnectData.tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
import React from 'react'
|
||||
import styled from 'styled-components'
|
||||
import QRCode from 'qrcode.react'
|
||||
|
||||
const QRCodeWrapper = styled.div`
|
||||
${({ theme }) => theme.flexColumnNoWrap};
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 12px;
|
||||
margin-bottom: 20px;
|
||||
`
|
||||
|
||||
interface WalletConnectDataProps {
|
||||
uri?: string
|
||||
size: number
|
||||
}
|
||||
|
||||
export default function WalletConnectData({ uri = '', size }: WalletConnectDataProps) {
|
||||
return <QRCodeWrapper>{uri && <QRCode size={size} value={uri} />}</QRCodeWrapper>
|
||||
}
|
||||