Compare commits
59 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f4f0f29409 | ||
|
|
fa25e3c3e5 | ||
|
|
51d2b3792f | ||
|
|
04ded04e74 | ||
|
|
cf188a86de | ||
|
|
f7e2435868 | ||
|
|
5817d3bbdb | ||
|
|
bdeb62ad64 | ||
|
|
e42effdbfd | ||
|
|
c0a5adf43d | ||
|
|
fd0489e654 | ||
|
|
caa0a2967d | ||
|
|
11ec2333dd | ||
|
|
251339a9ef | ||
|
|
148e415fe8 | ||
|
|
fd964c5b74 | ||
|
|
83b6eec271 | ||
|
|
cd76fffbbe | ||
|
|
2c0ac56296 | ||
|
|
f836e3ca32 | ||
|
|
1733fbb378 | ||
|
|
78142270a8 | ||
|
|
db4e2a9bee | ||
|
|
9cee94a473 | ||
|
|
db5a14387f | ||
|
|
5dc7d36669 | ||
|
|
a3cbe672c7 | ||
|
|
dc368ed7ac | ||
|
|
b109248b4c | ||
|
|
64cc6fb88c | ||
|
|
74f6a4ef3f | ||
|
|
f468001404 | ||
|
|
f26ec2ff1b | ||
|
|
61d1036d28 | ||
|
|
e11d2080a4 | ||
|
|
da33423719 | ||
|
|
bd4545538d | ||
|
|
4274db67d5 | ||
|
|
28498706cb | ||
|
|
68c71a67dd | ||
|
|
fe195b63f7 | ||
|
|
86b85e25a5 | ||
|
|
0ea635ce15 | ||
|
|
99ab581a87 | ||
|
|
fc571d0f63 | ||
|
|
2de43a8cdb | ||
|
|
5383436c88 | ||
|
|
521f3aae04 | ||
|
|
9318c1204b | ||
|
|
5055695b9b | ||
|
|
ae8c0377de | ||
|
|
8eaf1f4964 | ||
|
|
f717bf4a49 | ||
|
|
dcbd4e475d | ||
|
|
b704bdac94 | ||
|
|
00d3df95c0 | ||
|
|
251b8b703a | ||
|
|
ef8432437d | ||
|
|
71aebf33db |
24
.github/pull_request_template.md
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
Your PR title must follow [conventional commits](https://www.conventionalcommits.org/en/v1.0.0/#summary), and should start with one of the following [types](https://github.com/angular/angular/blob/22b96b9/CONTRIBUTING.md#type):
|
||||
|
||||
- build: Changes that affect the build system or external dependencies (example scopes: yarn, eslint, typescript)
|
||||
- ci: Changes to our CI configuration files and scripts (example scopes: vercel, github, cypress)
|
||||
- docs: Documentation only changes
|
||||
- feat: A new feature
|
||||
- fix: A bug fix
|
||||
- perf: A code change that improves performance
|
||||
- refactor: A code change that neither fixes a bug nor adds a feature
|
||||
- style: Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc)
|
||||
- test: Adding missing tests or correcting existing tests
|
||||
|
||||
Example commit messages:
|
||||
|
||||
- feat: adds support for gnosis safe wallet
|
||||
- fix: removes a polling memory leak
|
||||
- chore: bumps redux version
|
||||
|
||||
Other things to note:
|
||||
|
||||
- Please describe the change using verb statements (ex: Removes X from Y)
|
||||
- PRs with multiple changes should use a list of verb statements
|
||||
- Add any relevant unit / integration tests
|
||||
- Changes will be previewable via vercel. Non-obvious changes should include instructions for how to reproduce them
|
||||
40
.github/workflows/bundle.yaml
vendored
@@ -1,40 +0,0 @@
|
||||
name: Widgets
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Set up node
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: 14
|
||||
registry-url: https://registry.npmjs.org
|
||||
|
||||
- name: Get yarn cache directory path
|
||||
id: yarn-cache-dir-path
|
||||
run: echo "::set-output name=dir::$(yarn cache dir)"
|
||||
|
||||
- uses: actions/cache@v2
|
||||
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-
|
||||
|
||||
- name: Install dependencies
|
||||
run: yarn install --frozen-lockfile
|
||||
|
||||
- name: Build
|
||||
run: yarn widgets:build
|
||||
2
.github/workflows/crowdin-sync.yaml
vendored
@@ -42,7 +42,7 @@ jobs:
|
||||
run: "yarn i18n:extract"
|
||||
|
||||
- name: Synchronize
|
||||
uses: crowdin/github-action@1.1.0
|
||||
uses: crowdin/github-action@1.4.9
|
||||
with:
|
||||
upload_sources: false
|
||||
download_translations: true
|
||||
|
||||
2
.gitignore
vendored
@@ -19,8 +19,6 @@
|
||||
|
||||
# builds
|
||||
/build
|
||||
/cosmos-export
|
||||
/dist
|
||||
/dts
|
||||
|
||||
# misc
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
# Uniswap Interface
|
||||
|
||||
An open source interface for Uniswap -- a protocol for decentralized exchange of Ethereum tokens.
|
||||
|
||||
- Website: [uniswap.org](https://uniswap.org/)
|
||||
- Interface: [app.uniswap.org](https://app.uniswap.org)
|
||||
- Docs: [uniswap.org/docs/](https://docs.uniswap.org/)
|
||||
- Twitter: [@Uniswap](https://twitter.com/Uniswap)
|
||||
- Reddit: [/r/Uniswap](https://www.reddit.com/r/Uniswap/)
|
||||
- Email: [contact@uniswap.org](mailto:contact@uniswap.org)
|
||||
- Discord: [Uniswap](https://discord.gg/FCfyBSbCU5)
|
||||
- Whitepapers:
|
||||
- [V1](https://hackmd.io/C-DvwDSfSxuh-Gd4WKE_ig)
|
||||
- [V2](https://uniswap.org/whitepaper.pdf)
|
||||
- [V3](https://uniswap.org/whitepaper-v3.pdf)
|
||||
|
||||
## Accessing the Uniswap Interface
|
||||
|
||||
To access the Uniswap Interface, use an IPFS gateway link from the
|
||||
[latest release](https://github.com/Uniswap/uniswap-interface/releases/latest),
|
||||
or visit [app.uniswap.org](https://app.uniswap.org).
|
||||
|
||||
## Unsupported tokens
|
||||
|
||||
Check out `useUnsupportedTokenList()` in [src/state/lists/hooks.ts](./src/state/lists/hooks.ts) for blocking tokens in your instance of the interface.
|
||||
|
||||
You can block an entire list of tokens by passing in a tokenlist like [here](./src/constants/lists.ts) or you can block specific tokens by adding them to [unsupported.tokenlist.json](./src/constants/tokenLists/unsupported.tokenlist.json).
|
||||
|
||||
## Contributions
|
||||
|
||||
For steps on local deployment, development, and code contribution, please see [CONTRIBUTING](./CONTRIBUTING.md).
|
||||
|
||||
## Accessing Uniswap V2
|
||||
|
||||
The Uniswap Interface supports swapping, adding liquidity, removing liquidity and migrating liquidity for Uniswap protocol V2.
|
||||
|
||||
- Swap on Uniswap V2: https://app.uniswap.org/#/swap?use=v2
|
||||
- View V2 liquidity: https://app.uniswap.org/#/pool/v2
|
||||
- Add V2 liquidity: https://app.uniswap.org/#/add/v2
|
||||
- Migrate V2 liquidity to V3: https://app.uniswap.org/#/migrate/v2
|
||||
|
||||
## Accessing Uniswap V1
|
||||
|
||||
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-interface/releases/tag/v1.0.0).
|
||||
46
README.md
@@ -1,5 +1,3 @@
|
||||
This repo is home to the Uniswap Widgets package and the web app interface [app.uniswap.org](https://app.uniswap.org).
|
||||
|
||||
# Uniswap Labs Interface
|
||||
|
||||
[](https://github.com/Uniswap/interface/actions/workflows/unit-tests.yaml)
|
||||
@@ -8,14 +6,46 @@ This repo is home to the Uniswap Widgets package and the web app interface [app.
|
||||
[](https://github.com/Uniswap/interface/actions/workflows/release.yaml)
|
||||
[](https://crowdin.com/project/uniswap-interface)
|
||||
|
||||
The web application hosted at https://app.uniswap.org is a convenient way to access the core functionality of the Uniswap Protocol.
|
||||
An open source interface for Uniswap -- a protocol for decentralized exchange of Ethereum tokens.
|
||||
|
||||
For documentation of the interface including how to contribute or access prior builds, please view the README here: [INTERFACE_README.md](./INTERFACE_README.md)
|
||||
- Website: [uniswap.org](https://uniswap.org/)
|
||||
- Interface: [app.uniswap.org](https://app.uniswap.org)
|
||||
- Docs: [uniswap.org/docs/](https://docs.uniswap.org/)
|
||||
- Twitter: [@Uniswap](https://twitter.com/Uniswap)
|
||||
- Reddit: [/r/Uniswap](https://www.reddit.com/r/Uniswap/)
|
||||
- Email: [contact@uniswap.org](mailto:contact@uniswap.org)
|
||||
- Discord: [Uniswap](https://discord.gg/FCfyBSbCU5)
|
||||
- Whitepapers:
|
||||
- [V1](https://hackmd.io/C-DvwDSfSxuh-Gd4WKE_ig)
|
||||
- [V2](https://uniswap.org/whitepaper.pdf)
|
||||
- [V3](https://uniswap.org/whitepaper-v3.pdf)
|
||||
|
||||
# Uniswap Labs Widgets
|
||||
## Accessing the Uniswap Interface
|
||||
|
||||
The `@uniswap/widgets` package is an npm package of React components used to provide subsets of the Uniswap Protocol functionality in a small and configurable user interface element.
|
||||
To access the Uniswap Interface, use an IPFS gateway link from the
|
||||
[latest release](https://github.com/Uniswap/uniswap-interface/releases/latest),
|
||||
or visit [app.uniswap.org](https://app.uniswap.org).
|
||||
|
||||
The npm package can be found here. [@uniswap/widgets](https://www.npmjs.com/package/@uniswap/widgets)
|
||||
## Unsupported tokens
|
||||
|
||||
For documentation of the widgets package, please view the README here: [WIDGETS_README.md](./WIDGETS_README.md).
|
||||
Check out `useUnsupportedTokenList()` in [src/state/lists/hooks.ts](./src/state/lists/hooks.ts) for blocking tokens in your instance of the interface.
|
||||
|
||||
You can block an entire list of tokens by passing in a tokenlist like [here](./src/constants/lists.ts) or you can block specific tokens by adding them to [unsupported.tokenlist.json](./src/constants/tokenLists/unsupported.tokenlist.json).
|
||||
|
||||
## Contributions
|
||||
|
||||
For steps on local deployment, development, and code contribution, please see [CONTRIBUTING](./CONTRIBUTING.md).
|
||||
|
||||
## Accessing Uniswap V2
|
||||
|
||||
The Uniswap Interface supports swapping, adding liquidity, removing liquidity and migrating liquidity for Uniswap protocol V2.
|
||||
|
||||
- Swap on Uniswap V2: https://app.uniswap.org/#/swap?use=v2
|
||||
- View V2 liquidity: https://app.uniswap.org/#/pool/v2
|
||||
- Add V2 liquidity: https://app.uniswap.org/#/add/v2
|
||||
- Migrate V2 liquidity to V3: https://app.uniswap.org/#/migrate/v2
|
||||
|
||||
## Accessing Uniswap V1
|
||||
|
||||
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-interface/releases/tag/v1.0.0).
|
||||
@@ -1,40 +0,0 @@
|
||||
# Uniswap Labs Swap Widget
|
||||
|
||||
The Swap Widget bundles the whole swapping experience into a single React component that developers can easily embed in their app with one line of code.
|
||||
|
||||

|
||||
|
||||
You can customize the theme (colors, fonts, border radius, and more) to match the style of your application. You can also configure your own default token list and optionally set a convenience fee on swaps executed through the widget on your site.
|
||||
|
||||
## Installation
|
||||
|
||||
Install the widgets library via `npm` or `yarn`. If you do not already use the widget's peerDependencies `redux` and `react-redux`, then you'll need to add them as well.
|
||||
|
||||
```js
|
||||
yarn add @uniswap/widgets redux react-redux
|
||||
```
|
||||
```js
|
||||
npm i --save @uniswap/widgets redux react-redux
|
||||
```
|
||||
|
||||
## Documentation
|
||||
|
||||
- [overview](https://docs.uniswap.org/sdk/widgets/swap-widget)
|
||||
- [api reference](https://docs.uniswap.org/sdk/widgets/swap-widget/api)
|
||||
|
||||
## Example Apps
|
||||
|
||||
Uniswap Labs maintains two demo apps in branches of the [widgets-demo](https://github.com/Uniswap/widgets-demo) repo:
|
||||
|
||||
- [NextJS](https://github.com/Uniswap/widgets-demo/tree/nextjs)
|
||||
- [Create React App](https://github.com/Uniswap/widgets-demo/tree/cra)
|
||||
|
||||
Others have also also released the widget in production to their userbase:
|
||||
|
||||
- [OpenSea](https://opensea.io/)
|
||||
- [Friends With Benefits](https://www.fwb.help/)
|
||||
- [Oasis](https://oasis.app/)
|
||||
|
||||
## Legal notice
|
||||
|
||||
Uniswap Labs encourages integrators to evaluate their own regulatory obligations when integrating this widget into their products, including, but not limited to, those related to economic or trade sanctions compliance.
|
||||
@@ -1,10 +0,0 @@
|
||||
{
|
||||
"watchDirs": [
|
||||
"src"
|
||||
],
|
||||
"webpack": {
|
||||
"configPath": "react-scripts/config/webpack.config",
|
||||
"overridePath": "cosmos.override.cjs"
|
||||
},
|
||||
"port": 5001
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
/* eslint-disable @typescript-eslint/no-var-requires */
|
||||
const HtmlWebpackPlugin = require('html-webpack-plugin')
|
||||
const { DefinePlugin } = require('webpack')
|
||||
|
||||
// Renders the cosmos fixtures in isolation, instead of using public/index.html.
|
||||
module.exports = (webpackConfig) => ({
|
||||
...webpackConfig,
|
||||
plugins: webpackConfig.plugins.map((plugin) => {
|
||||
if (plugin instanceof HtmlWebpackPlugin) {
|
||||
return new HtmlWebpackPlugin({
|
||||
templateContent: '<body></body>',
|
||||
})
|
||||
}
|
||||
if (plugin instanceof DefinePlugin) {
|
||||
return new DefinePlugin({
|
||||
...plugin.definitions,
|
||||
'process.env': {
|
||||
...plugin.definitions['process.env'],
|
||||
REACT_APP_IS_WIDGET: true,
|
||||
REACT_APP_LOCALES: '"../locales"',
|
||||
},
|
||||
})
|
||||
}
|
||||
return plugin
|
||||
}),
|
||||
})
|
||||
28
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@uniswap/widgets",
|
||||
"version": "1.0.6",
|
||||
"version": "1.0.7",
|
||||
"description": "Uniswap Interface",
|
||||
"homepage": ".",
|
||||
"files": [
|
||||
@@ -44,16 +44,6 @@
|
||||
"@reach/dialog": "^0.10.3",
|
||||
"@reach/portal": "^0.10.3",
|
||||
"@react-hook/window-scroll": "^1.3.0",
|
||||
"@rollup/plugin-alias": "^3.1.9",
|
||||
"@rollup/plugin-babel": "^5.3.0",
|
||||
"@rollup/plugin-commonjs": "^21.0.1",
|
||||
"@rollup/plugin-eslint": "^8.0.1",
|
||||
"@rollup/plugin-json": "^4.1.0",
|
||||
"@rollup/plugin-node-resolve": "^13.1.3",
|
||||
"@rollup/plugin-replace": "^3.0.1",
|
||||
"@rollup/plugin-typescript": "^8.3.0",
|
||||
"@rollup/plugin-url": "^6.1.0",
|
||||
"@svgr/rollup": "^6.2.0",
|
||||
"@testing-library/jest-dom": "^5.14.1",
|
||||
"@testing-library/react": "^12.0.0",
|
||||
"@testing-library/react-hooks": "^7.0.2",
|
||||
@@ -117,7 +107,6 @@
|
||||
"qs": "^6.9.4",
|
||||
"react": "^17.0.1",
|
||||
"react-confetti": "^6.0.0",
|
||||
"react-cosmos": "^5.6.6",
|
||||
"react-dom": "^17.0.1",
|
||||
"react-ga4": "^1.4.1",
|
||||
"react-is": "^17.0.2",
|
||||
@@ -129,14 +118,6 @@
|
||||
"react-use-gesture": "^6.0.14",
|
||||
"redux": "^4.1.2",
|
||||
"redux-localstorage-simple": "^2.3.1",
|
||||
"rollup": "^2.63.0",
|
||||
"rollup-plugin-copy": "^3.4.0",
|
||||
"rollup-plugin-delete": "^2.0.0",
|
||||
"rollup-plugin-dts": "^4.1.0",
|
||||
"rollup-plugin-multi-input": "^1.3.1",
|
||||
"rollup-plugin-node-externals": "^3.1.2",
|
||||
"rollup-plugin-scss": "^3.0.0",
|
||||
"rollup-plugin-typescript2": "^0.31.1",
|
||||
"sass": "^1.45.1",
|
||||
"serve": "^11.3.2",
|
||||
"start-server-and-test": "^1.11.0",
|
||||
@@ -162,7 +143,7 @@
|
||||
},
|
||||
"scripts": {
|
||||
"contracts:compile:abi": "typechain --target ethers-v5 --out-dir src/abis/types \"./src/abis/**/*.json\"",
|
||||
"contracts:compile:v3": "typechain --target ethers-v5 --out-dir src/types/v3 \"./node_modules/@uniswap/**/artifacts/contracts/**/*.json\"",
|
||||
"contracts:compile:v3": "typechain --target ethers-v5 --out-dir src/types/v3 \"./node_modules/@uniswap/**/artifacts/contracts/**/*[!dbg].json\"",
|
||||
"contracts:compile": "yarn contracts:compile:abi && yarn contracts:compile:v3",
|
||||
"graphql:generate": "graphql-codegen --config codegen.yml",
|
||||
"prei18n:extract": "touch src/locales/en-US.po",
|
||||
@@ -170,13 +151,10 @@
|
||||
"i18n:compile": "yarn i18n:extract && lingui compile",
|
||||
"i18n:pseudo": "lingui extract --locale pseudo && lingui compile",
|
||||
"prepare": "yarn contracts:compile && yarn graphql:generate && yarn i18n:compile",
|
||||
"prepublishOnly": "yarn widgets:build",
|
||||
"start": "react-scripts start",
|
||||
"build": "react-scripts build",
|
||||
"test": "react-scripts test --env=./custom-test-env.cjs",
|
||||
"test:e2e": "start-server-and-test 'serve build -l 3000' http://localhost:3000 'cypress run --record'",
|
||||
"widgets:start": "cosmos",
|
||||
"widgets:build": "rollup --config --failAfterWarnings --configPlugin typescript2"
|
||||
"test:e2e": "start-server-and-test 'serve build -l 3000' http://localhost:3000 'cypress run --record'"
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
|
||||
182
rollup.config.ts
@@ -1,182 +0,0 @@
|
||||
/**
|
||||
* Bundles the widgets library, which is released independently of the interface application.
|
||||
* This library lives in src/lib, but shares code with the interface application.
|
||||
*/
|
||||
|
||||
import alias from '@rollup/plugin-alias'
|
||||
import babel from '@rollup/plugin-babel'
|
||||
import commonjs from '@rollup/plugin-commonjs'
|
||||
import json from '@rollup/plugin-json'
|
||||
import resolve from '@rollup/plugin-node-resolve'
|
||||
import replace from '@rollup/plugin-replace'
|
||||
import typescript from '@rollup/plugin-typescript'
|
||||
import url from '@rollup/plugin-url'
|
||||
import svgr from '@svgr/rollup'
|
||||
import path from 'path'
|
||||
import { RollupWarning } from 'rollup'
|
||||
import copy from 'rollup-plugin-copy'
|
||||
import del from 'rollup-plugin-delete'
|
||||
import dts from 'rollup-plugin-dts'
|
||||
// @ts-ignore // missing types
|
||||
import multi from 'rollup-plugin-multi-input'
|
||||
import externals from 'rollup-plugin-node-externals'
|
||||
import sass from 'rollup-plugin-scss'
|
||||
import { CompilerOptions } from 'typescript'
|
||||
|
||||
const REPLACEMENTS = {
|
||||
'process.env.REACT_APP_IS_WIDGET': true,
|
||||
'process.env.REACT_APP_LOCALES': '"./locales"',
|
||||
// esm requires fully-specified paths:
|
||||
'react/jsx-runtime': 'react/jsx-runtime.js',
|
||||
}
|
||||
|
||||
const EXTENSIONS = ['.js', '.jsx', '.ts', '.tsx']
|
||||
const ASSET_EXTENSIONS = ['.png', '.svg']
|
||||
function isAsset(source: string) {
|
||||
const extname = path.extname(source)
|
||||
return extname && [...ASSET_EXTENSIONS, '.css', '.scss'].includes(extname)
|
||||
}
|
||||
|
||||
function isEthers(source: string) {
|
||||
// @ethersproject/* modules are provided by ethers, with the exception of experimental.
|
||||
return source.startsWith('@ethersproject/') && !source.endsWith('experimental')
|
||||
}
|
||||
|
||||
const TS_CONFIG = './tsconfig.lib.json'
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const { baseUrl, paths }: CompilerOptions = require(TS_CONFIG).compilerOptions
|
||||
const aliases = Object.entries({ ...paths }).flatMap(([find, replacements]) => {
|
||||
return replacements.map((replacement) => ({
|
||||
find: path.dirname(find),
|
||||
replacement: path.join(__dirname, baseUrl || '.', path.dirname(replacement)),
|
||||
}))
|
||||
})
|
||||
|
||||
const plugins = [
|
||||
// Dependency resolution
|
||||
resolve({ extensions: EXTENSIONS }), // resolves third-party modules within node_modules/
|
||||
alias({ entries: aliases }), // resolves paths aliased through the tsconfig (babel does not use tsconfig path resolution)
|
||||
|
||||
// Source code transformation
|
||||
replace({ ...REPLACEMENTS, preventAssignment: true }),
|
||||
json(), // imports json as ES6; doing so enables type-checking and module resolution
|
||||
]
|
||||
|
||||
const check = {
|
||||
input: 'src/lib/index.tsx',
|
||||
output: { file: 'dist/widgets.tsc', inlineDynamicImports: true },
|
||||
external: (source: string) => isAsset(source) || isEthers(source),
|
||||
plugins: [
|
||||
externals({ exclude: ['constants'], deps: true, peerDeps: true }), // marks builtins, dependencies, and peerDependencies external
|
||||
...plugins,
|
||||
typescript({ tsconfig: TS_CONFIG }),
|
||||
],
|
||||
onwarn: squelchTranspilationWarnings, // this pipeline is only for typechecking and generating definitions
|
||||
}
|
||||
|
||||
const type = {
|
||||
input: 'dist/dts/lib/index.d.ts',
|
||||
output: { file: 'dist/index.d.ts' },
|
||||
external: (source: string) => isAsset(source) || isEthers(source),
|
||||
plugins: [
|
||||
externals({ exclude: ['constants'], deps: true, peerDeps: true }),
|
||||
dts({ compilerOptions: { baseUrl: 'dist/dts' } }),
|
||||
process.env.ROLLUP_WATCH ? undefined : del({ hook: 'buildEnd', targets: ['dist/widgets.tsc', 'dist/dts'] }),
|
||||
],
|
||||
}
|
||||
|
||||
/**
|
||||
* This exports scheme works for nextjs and for CRA5.
|
||||
*
|
||||
* It will also work for CRA4 if you use direct imports:
|
||||
* instead of `import { SwapWidget } from '@uniswap/widgets'`,
|
||||
* `import { SwapWidget } from '@uniswap/widgets/dist/index.js'`.
|
||||
* I do not know why CRA4 does not seem to use exports for resolution.
|
||||
*
|
||||
* Note that chunks are enabled. This is so the tokenlist spec can be loaded async,
|
||||
* to improve first load time (due to ajv). Locales are also in separate chunks.
|
||||
*
|
||||
* Lastly, note that JSON and lingui are bundled into the library, as neither are fully
|
||||
* supported/compatible with ES Modules. Both _could_ be bundled only with esm, but this
|
||||
* yields a less complex pipeline.
|
||||
*/
|
||||
|
||||
const transpile = {
|
||||
input: 'src/lib/index.tsx',
|
||||
output: [
|
||||
{
|
||||
dir: 'dist',
|
||||
format: 'esm',
|
||||
sourcemap: false,
|
||||
},
|
||||
{
|
||||
dir: 'dist/cjs',
|
||||
entryFileNames: '[name].cjs',
|
||||
chunkFileNames: '[name]-[hash].cjs',
|
||||
format: 'cjs',
|
||||
sourcemap: false,
|
||||
},
|
||||
],
|
||||
external: isEthers,
|
||||
plugins: [
|
||||
externals({
|
||||
exclude: [
|
||||
'constants',
|
||||
/@lingui\/(core|react)/, // @lingui incorrectly exports esm, so it must be bundled in
|
||||
/\.json$/, // esm does not support JSON loading, so it must be bundled in
|
||||
],
|
||||
deps: true,
|
||||
peerDeps: true,
|
||||
}),
|
||||
...plugins,
|
||||
|
||||
// Source code transformation
|
||||
url({ include: ASSET_EXTENSIONS.map((extname) => '**/*' + extname), limit: Infinity }), // imports assets as data URIs
|
||||
svgr({ exportType: 'named', svgo: false }), // imports svgs as React components
|
||||
sass({ output: 'dist/fonts.css' }), // generates fonts.css
|
||||
commonjs(), // transforms cjs dependencies into tree-shakeable ES modules
|
||||
|
||||
babel({
|
||||
babelHelpers: 'runtime',
|
||||
presets: ['@babel/preset-env', ['@babel/preset-react', { runtime: 'automatic' }], '@babel/preset-typescript'],
|
||||
extensions: EXTENSIONS,
|
||||
plugins: [
|
||||
'macros', // enables @lingui and styled-components macros
|
||||
'@babel/plugin-transform-runtime', // embeds the babel runtime for library distribution
|
||||
],
|
||||
}),
|
||||
],
|
||||
onwarn: squelchTypeWarnings, // this pipeline is only for transpilation
|
||||
}
|
||||
|
||||
const locales = {
|
||||
input: 'src/locales/*.js',
|
||||
output: [
|
||||
{
|
||||
dir: 'dist',
|
||||
format: 'esm',
|
||||
sourcemap: false,
|
||||
},
|
||||
],
|
||||
plugins: [
|
||||
copy({
|
||||
copyOnce: true,
|
||||
targets: [{ src: 'src/locales/*.js', dest: 'dist/cjs/locales', rename: (name) => `${name}.cjs` }],
|
||||
}),
|
||||
commonjs(),
|
||||
multi(),
|
||||
],
|
||||
}
|
||||
|
||||
const config = [check, type, transpile, locales]
|
||||
export default config
|
||||
|
||||
function squelchTranspilationWarnings(warning: RollupWarning, warn: (warning: RollupWarning) => void) {
|
||||
if (warning.pluginCode === 'TS5055') return
|
||||
warn(warning)
|
||||
}
|
||||
|
||||
function squelchTypeWarnings(warning: RollupWarning, warn: (warning: RollupWarning) => void) {
|
||||
if (warning.code === 'UNUSED_EXTERNAL_IMPORT') return
|
||||
warn(warning)
|
||||
}
|
||||
BIN
src/assets/images/tally.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
@@ -1,49 +1,64 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import React from 'react'
|
||||
import useCopyClipboard from 'hooks/useCopyClipboard'
|
||||
import React, { useCallback } from 'react'
|
||||
import { CheckCircle, Copy } from 'react-feather'
|
||||
import styled from 'styled-components/macro'
|
||||
|
||||
import useCopyClipboard from '../../hooks/useCopyClipboard'
|
||||
import { LinkStyledButton } from '../../theme'
|
||||
import { LinkStyledButton } from 'theme'
|
||||
|
||||
const CopyIcon = styled(LinkStyledButton)`
|
||||
color: ${({ theme }) => theme.text3};
|
||||
color: ${({ color, theme }) => color || theme.text3};
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
text-decoration: none;
|
||||
font-size: 0.825rem;
|
||||
:hover,
|
||||
:active,
|
||||
:focus {
|
||||
text-decoration: none;
|
||||
color: ${({ theme }) => theme.text2};
|
||||
color: ${({ color, theme }) => color || theme.text2};
|
||||
}
|
||||
`
|
||||
const TransactionStatusText = styled.span`
|
||||
const StyledText = styled.span`
|
||||
margin-left: 0.25rem;
|
||||
font-size: 0.825rem;
|
||||
${({ theme }) => theme.flexRowNoWrap};
|
||||
align-items: center;
|
||||
`
|
||||
|
||||
export default function CopyHelper(props: { toCopy: string; children?: React.ReactNode }) {
|
||||
const Copied = ({ iconSize }: { iconSize?: number }) => (
|
||||
<StyledText>
|
||||
<CheckCircle size={iconSize ?? '16'} />
|
||||
<StyledText>
|
||||
<Trans>Copied</Trans>
|
||||
</StyledText>
|
||||
</StyledText>
|
||||
)
|
||||
|
||||
const Icon = ({ iconSize }: { iconSize?: number }) => (
|
||||
<StyledText>
|
||||
<Copy size={iconSize ?? '16'} />
|
||||
</StyledText>
|
||||
)
|
||||
|
||||
interface BaseProps {
|
||||
toCopy: string
|
||||
color?: string
|
||||
iconSize?: number
|
||||
iconPosition?: 'left' | 'right'
|
||||
}
|
||||
export type CopyHelperProps = BaseProps & Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, keyof BaseProps>
|
||||
|
||||
export default function CopyHelper({ color, toCopy, children, iconSize, iconPosition }: CopyHelperProps) {
|
||||
const [isCopied, setCopied] = useCopyClipboard()
|
||||
const copy = useCallback(() => {
|
||||
setCopied(toCopy)
|
||||
}, [toCopy, setCopied])
|
||||
|
||||
return (
|
||||
<CopyIcon onClick={() => setCopied(props.toCopy)}>
|
||||
{isCopied ? (
|
||||
<TransactionStatusText>
|
||||
<CheckCircle size={'16'} />
|
||||
<TransactionStatusText>
|
||||
<Trans>Copied</Trans>
|
||||
</TransactionStatusText>
|
||||
</TransactionStatusText>
|
||||
) : (
|
||||
<TransactionStatusText>
|
||||
<Copy size={'16'} />
|
||||
</TransactionStatusText>
|
||||
)}
|
||||
{isCopied ? '' : props.children}
|
||||
<CopyIcon onClick={copy} color={color}>
|
||||
{iconPosition === 'left' ? isCopied ? <Copied iconSize={iconSize} /> : <Icon iconSize={iconSize} /> : null}
|
||||
{iconPosition === 'left' && <> </>}
|
||||
{isCopied ? '' : children}
|
||||
{iconPosition === 'right' && <> </>}
|
||||
{iconPosition === 'right' ? isCopied ? <Copied iconSize={iconSize} /> : <Icon iconSize={iconSize} /> : null}
|
||||
</CopyIcon>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ import {
|
||||
VoteTransactionInfo,
|
||||
WithdrawLiquidityStakingTransactionInfo,
|
||||
WrapTransactionInfo,
|
||||
} from '../../state/transactions/actions'
|
||||
} from '../../state/transactions/types'
|
||||
|
||||
function formatAmount(amountRaw: string, decimals: number, sigFigs: number): string {
|
||||
return new Fraction(amountRaw, JSBI.exponentiate(JSBI.BigInt(10), JSBI.BigInt(decimals))).toSignificant(sigFigs)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import { Connector } from '@web3-react/types'
|
||||
import CopyHelper from 'components/AccountDetails/Copy'
|
||||
import useActiveWeb3React from 'hooks/useActiveWeb3React'
|
||||
import { useCallback, useContext } from 'react'
|
||||
import { ExternalLink as LinkIcon } from 'react-feather'
|
||||
@@ -10,14 +11,13 @@ import { AbstractConnector } from 'web3-react-abstract-connector'
|
||||
import { ReactComponent as Close } from '../../assets/images/x.svg'
|
||||
import { injected, walletlink } from '../../connectors'
|
||||
import { SUPPORTED_WALLETS } from '../../constants/wallet'
|
||||
import { clearAllTransactions } from '../../state/transactions/actions'
|
||||
import { clearAllTransactions } from '../../state/transactions/reducer'
|
||||
import { ExternalLink, LinkStyledButton, ThemedText } from '../../theme'
|
||||
import { shortenAddress } from '../../utils'
|
||||
import { ExplorerDataType, getExplorerLink } from '../../utils/getExplorerLink'
|
||||
import { ButtonSecondary } from '../Button'
|
||||
import StatusIcon from '../Identicon/StatusIcon'
|
||||
import { AutoRow } from '../Row'
|
||||
import Copy from './Copy'
|
||||
import Transaction from './Transaction'
|
||||
|
||||
const HeaderRow = styled.div`
|
||||
@@ -201,10 +201,6 @@ const WalletAction = styled(ButtonSecondary)`
|
||||
}
|
||||
`
|
||||
|
||||
const MainWalletAction = styled(WalletAction)`
|
||||
color: ${({ theme }) => theme.primary1};
|
||||
`
|
||||
|
||||
function renderTransactions(transactions: string[]) {
|
||||
return (
|
||||
<TransactionListWrapper>
|
||||
@@ -314,11 +310,11 @@ export default function AccountDetails({
|
||||
<AccountControl>
|
||||
<div>
|
||||
{account && (
|
||||
<Copy toCopy={account}>
|
||||
<CopyHelper toCopy={account} iconPosition="left">
|
||||
<span style={{ marginLeft: '4px' }}>
|
||||
<Trans>Copy Address</Trans>
|
||||
</span>
|
||||
</Copy>
|
||||
</CopyHelper>
|
||||
)}
|
||||
{chainId && account && (
|
||||
<AddressLink
|
||||
@@ -340,11 +336,11 @@ export default function AccountDetails({
|
||||
<AccountControl>
|
||||
<div>
|
||||
{account && (
|
||||
<Copy toCopy={account}>
|
||||
<CopyHelper toCopy={account} iconPosition="left">
|
||||
<span style={{ marginLeft: '4px' }}>
|
||||
<Trans>Copy Address</Trans>
|
||||
</span>
|
||||
</Copy>
|
||||
</CopyHelper>
|
||||
)}
|
||||
{chainId && account && (
|
||||
<AddressLink
|
||||
|
||||
@@ -2,7 +2,6 @@ import { Trans } from '@lingui/macro'
|
||||
import useActiveWeb3React from 'hooks/useActiveWeb3React'
|
||||
import { ReactNode, useMemo } from 'react'
|
||||
|
||||
// SDN OFAC addresses
|
||||
const BLOCKED_ADDRESSES: string[] = [
|
||||
'0x7Db418b5D567A4e0E8c59Ad71BE1FcE48f3E6107',
|
||||
'0x72a5843cc08275C8171E582972Aa4fDa8C397B2A',
|
||||
@@ -24,16 +23,18 @@ const BLOCKED_ADDRESSES: string[] = [
|
||||
'0x6acdfba02d390b97ac2b2d42a63e85293bcc1',
|
||||
'0x48549a34ae37b12f6a30566245176994e17c6',
|
||||
'0x5512d943ed1f7c8a43f3435c85f7ab68b30121',
|
||||
'0xc455f7fd3e0e12afd51fba5c106909934d8a0e',
|
||||
'0x3cbded43efdaf0fc77b9c55f6fc9988fcc9b757d',
|
||||
'0xC455f7fd3e0e12afd51fba5c106909934D8A0e4a',
|
||||
'0x3CBdeD43EFdAf0FC77b9C55F6fC9988fCC9b757d',
|
||||
'0x67d40EE1A85bf4a4Bb7Ffae16De985e8427B6b45',
|
||||
'0x6f1ca141a28907f78ebaa64fb83a9088b02a8352',
|
||||
'0x6acdfba02d390b97ac2b2d42a63e85293bcc160e',
|
||||
'0x6F1cA141A28907F78Ebaa64fb83A9088b02A8352',
|
||||
'0x6aCDFBA02D390b97Ac2b2d42A63E85293BCc160e',
|
||||
'0x48549a34ae37b12f6a30566245176994e17c6b4a',
|
||||
'0x5512d943ed1f7c8a43f3435c85f7ab68b30121b0',
|
||||
'0xc455f7fd3e0e12afd51fba5c106909934d8a0e4a',
|
||||
'0xC455f7fd3e0e12afd51fba5c106909934D8A0e4a',
|
||||
'0x629e7Da20197a5429d30da36E77d06CdF796b71A',
|
||||
'0x7FF9cFad3877F21d41Da833E2F775dB0569eE3D9',
|
||||
'0x098B716B8Aaf21512996dC57EB0615e2383E2f96',
|
||||
'0xfEC8A60023265364D066a1212fDE3930F6Ae8da7',
|
||||
]
|
||||
|
||||
export default function Blocklist({ children }: { children: ReactNode }) {
|
||||
|
||||
58
src/components/ConnectedAccountBlocked/index.tsx
Normal file
@@ -0,0 +1,58 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import CopyHelper from 'components/AccountDetails/Copy'
|
||||
import Column from 'components/Column'
|
||||
import useTheme from 'hooks/useTheme'
|
||||
import { AlertOctagon } from 'react-feather'
|
||||
import styled from 'styled-components/macro'
|
||||
import { ExternalLink, ThemedText } from 'theme'
|
||||
|
||||
import Modal from '../Modal'
|
||||
|
||||
const ContentWrapper = styled(Column)`
|
||||
align-items: center;
|
||||
margin: 32px;
|
||||
text-align: center;
|
||||
`
|
||||
const WarningIcon = styled(AlertOctagon)`
|
||||
min-height: 22px;
|
||||
min-width: 22px;
|
||||
color: ${({ theme }) => theme.warning};
|
||||
`
|
||||
const Copy = styled(CopyHelper)`
|
||||
font-size: 12px;
|
||||
`
|
||||
|
||||
interface ConnectedAccountBlockedProps {
|
||||
account: string | null | undefined
|
||||
isOpen: boolean
|
||||
}
|
||||
|
||||
export default function ConnectedAccountBlocked(props: ConnectedAccountBlockedProps) {
|
||||
const theme = useTheme()
|
||||
return (
|
||||
<Modal isOpen={props.isOpen} onDismiss={Function.prototype()}>
|
||||
<ContentWrapper>
|
||||
<WarningIcon />
|
||||
<ThemedText.LargeHeader lineHeight={2} marginBottom={1} marginTop={1}>
|
||||
<Trans>Blocked Address</Trans>
|
||||
</ThemedText.LargeHeader>
|
||||
<ThemedText.DarkGray fontSize={12} marginBottom={12}>
|
||||
{props.account}
|
||||
</ThemedText.DarkGray>
|
||||
<ThemedText.Main fontSize={14} marginBottom={12}>
|
||||
<Trans>This address is blocked on the Uniswap Labs interface because it is associated with one or more</Trans>{' '}
|
||||
<ExternalLink href="https://help.uniswap.org/en/articles/6149816">
|
||||
<Trans>blocked activities</Trans>
|
||||
</ExternalLink>
|
||||
.
|
||||
</ThemedText.Main>
|
||||
<ThemedText.Main fontSize={12}>
|
||||
<Trans>If you believe this is an error, please send an email including your address to </Trans>{' '}
|
||||
</ThemedText.Main>
|
||||
<Copy iconSize={12} toCopy="compliance@uniswap.org" color={theme.primary1} iconPosition="right">
|
||||
compliance@uniswap.org
|
||||
</Copy>
|
||||
</ContentWrapper>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
@@ -27,13 +27,16 @@ export function FiatValue({
|
||||
return theme.red1
|
||||
}, [priceImpact, theme.green1, theme.red1, theme.text3, theme.yellow1])
|
||||
|
||||
const p = Number(fiatValue?.toFixed())
|
||||
const visibleDecimalPlaces = p < 1.05 ? 4 : 2
|
||||
|
||||
return (
|
||||
<ThemedText.Body fontSize={14} color={fiatValue ? theme.text3 : theme.text4}>
|
||||
{fiatValue ? (
|
||||
<Trans>
|
||||
$
|
||||
<HoverInlineText
|
||||
text={fiatValue?.toSignificant(6, { groupSeparator: ',' })}
|
||||
text={fiatValue?.toFixed(visibleDecimalPlaces, { groupSeparator: ',' })}
|
||||
textColor={fiatValue ? theme.text3 : theme.text4}
|
||||
/>
|
||||
</Trans>
|
||||
|
||||
@@ -93,6 +93,13 @@ const FlyoutRowActiveIndicator = styled.div`
|
||||
height: 9px;
|
||||
width: 9px;
|
||||
`
|
||||
|
||||
const CircleContainer = styled.div`
|
||||
width: 20px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
`
|
||||
|
||||
const LinkOutCircle = styled(ArrowDownCircle)`
|
||||
transform: rotate(230deg);
|
||||
width: 16px;
|
||||
@@ -146,7 +153,7 @@ const BridgeLabel = ({ chainId }: { chainId: SupportedChainId }) => {
|
||||
return <Trans>Arbitrum Bridge</Trans>
|
||||
case SupportedChainId.OPTIMISM:
|
||||
case SupportedChainId.OPTIMISTIC_KOVAN:
|
||||
return <Trans>Optimism Gateway</Trans>
|
||||
return <Trans>Optimism Bridge</Trans>
|
||||
case SupportedChainId.POLYGON:
|
||||
case SupportedChainId.POLYGON_MUMBAI:
|
||||
return <Trans>Polygon Bridge</Trans>
|
||||
@@ -188,7 +195,11 @@ function Row({
|
||||
<FlyoutRow onClick={() => onSelectChain(targetChain)} active={active}>
|
||||
<Logo src={logoUrl} />
|
||||
<NetworkLabel>{label}</NetworkLabel>
|
||||
{chainId === targetChain && <FlyoutRowActiveIndicator />}
|
||||
{chainId === targetChain && (
|
||||
<CircleContainer>
|
||||
<FlyoutRowActiveIndicator />
|
||||
</CircleContainer>
|
||||
)}
|
||||
</FlyoutRow>
|
||||
)
|
||||
|
||||
@@ -197,21 +208,30 @@ function Row({
|
||||
<ActiveRowWrapper>
|
||||
{rowContent}
|
||||
<ActiveRowLinkList>
|
||||
{bridge ? (
|
||||
{bridge && (
|
||||
<ExternalLink href={bridge}>
|
||||
<BridgeLabel chainId={chainId} /> <LinkOutCircle />
|
||||
<BridgeLabel chainId={chainId} />
|
||||
<CircleContainer>
|
||||
<LinkOutCircle />
|
||||
</CircleContainer>
|
||||
</ExternalLink>
|
||||
) : null}
|
||||
{explorer ? (
|
||||
)}
|
||||
{explorer && (
|
||||
<ExternalLink href={explorer}>
|
||||
<ExplorerLabel chainId={chainId} /> <LinkOutCircle />
|
||||
<ExplorerLabel chainId={chainId} />
|
||||
<CircleContainer>
|
||||
<LinkOutCircle />
|
||||
</CircleContainer>
|
||||
</ExternalLink>
|
||||
) : null}
|
||||
{helpCenterUrl ? (
|
||||
)}
|
||||
{helpCenterUrl && (
|
||||
<ExternalLink href={helpCenterUrl}>
|
||||
<Trans>Help Center</Trans> <LinkOutCircle />
|
||||
<Trans>Help Center</Trans>
|
||||
<CircleContainer>
|
||||
<LinkOutCircle />
|
||||
</CircleContainer>
|
||||
</ExternalLink>
|
||||
) : null}
|
||||
)}
|
||||
</ActiveRowLinkList>
|
||||
</ActiveRowWrapper>
|
||||
)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import { ButtonText } from 'components/Button'
|
||||
import PositionListItem from 'components/PositionListItem'
|
||||
import Toggle from 'components/Toggle'
|
||||
import React from 'react'
|
||||
import styled from 'styled-components/macro'
|
||||
import { MEDIA_WIDTHS } from 'theme'
|
||||
@@ -28,9 +28,37 @@ const MobileHeader = styled.div`
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
padding: 8px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
@media screen and (min-width: ${MEDIA_WIDTHS.upToSmall}px) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media screen and (max-width: ${MEDIA_WIDTHS.upToExtraSmall}px) {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: start;
|
||||
}
|
||||
`
|
||||
|
||||
const ToggleWrap = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
`
|
||||
|
||||
const ToggleLabel = styled.div`
|
||||
opacity: 0.6;
|
||||
margin-right: 10px;
|
||||
`
|
||||
|
||||
const MobileTogglePosition = styled.div`
|
||||
@media screen and (max-width: ${MEDIA_WIDTHS.upToExtraSmall}px) {
|
||||
position: absolute;
|
||||
right: 20px;
|
||||
}
|
||||
`
|
||||
|
||||
type PositionListProps = React.PropsWithChildren<{
|
||||
@@ -51,12 +79,35 @@ export default function PositionList({
|
||||
<Trans>Your positions</Trans>
|
||||
{positions && ' (' + positions.length + ')'}
|
||||
</div>
|
||||
<ButtonText style={{ opacity: 0.6 }} onClick={() => setUserHideClosedPositions(!userHideClosedPositions)}>
|
||||
<Trans>Hide closed positions</Trans>
|
||||
</ButtonText>
|
||||
<ToggleWrap>
|
||||
<ToggleLabel>
|
||||
<Trans>Show closed positions</Trans>
|
||||
</ToggleLabel>
|
||||
<Toggle
|
||||
id="desktop-hide-closed-positions"
|
||||
isActive={!userHideClosedPositions}
|
||||
toggle={() => {
|
||||
setUserHideClosedPositions(!userHideClosedPositions)
|
||||
}}
|
||||
/>
|
||||
</ToggleWrap>
|
||||
</DesktopHeader>
|
||||
<MobileHeader>
|
||||
<Trans>Your positions</Trans>
|
||||
<ToggleWrap>
|
||||
<ToggleLabel>
|
||||
<Trans>Show closed positions</Trans>
|
||||
</ToggleLabel>
|
||||
<MobileTogglePosition>
|
||||
<Toggle
|
||||
id="mobile-hide-closed-positions"
|
||||
isActive={!userHideClosedPositions}
|
||||
toggle={() => {
|
||||
setUserHideClosedPositions(!userHideClosedPositions)
|
||||
}}
|
||||
/>
|
||||
</MobileTogglePosition>
|
||||
</ToggleWrap>
|
||||
</MobileHeader>
|
||||
{positions.map((p) => {
|
||||
return <PositionListItem key={p.tokenId.toString()} positionDetails={p} />
|
||||
|
||||
@@ -167,11 +167,13 @@ export function PrivacyPolicy() {
|
||||
</AutoColumn>
|
||||
</DarkGreyCard>
|
||||
))}
|
||||
<Row justify="center" marginBottom="1rem">
|
||||
<ExternalLink href="https://help.uniswap.org/en/articles/5675203-terms-of-service-faq">
|
||||
<Trans>Learn more</Trans>
|
||||
</ExternalLink>
|
||||
</Row>
|
||||
<ThemedText.Body fontSize={12}>
|
||||
<Row justify="center" marginBottom="1rem">
|
||||
<ExternalLink href="https://help.uniswap.org/en/articles/5675203-terms-of-service-faq">
|
||||
<Trans>Learn more</Trans>
|
||||
</ExternalLink>
|
||||
</Row>
|
||||
</ThemedText.Body>
|
||||
</AutoColumn>
|
||||
</AutoColumn>
|
||||
</Wrapper>
|
||||
|
||||
@@ -5,8 +5,8 @@ import Badge from 'components/Badge'
|
||||
import CurrencyLogo from 'components/CurrencyLogo'
|
||||
import DoubleCurrencyLogo from 'components/DoubleLogo'
|
||||
import Row, { AutoRow } from 'components/Row'
|
||||
import { RoutingDiagramEntry } from 'components/swap/SwapRoute'
|
||||
import { useTokenInfoFromActiveList } from 'hooks/useTokenInfoFromActiveList'
|
||||
import { RoutingDiagramEntry } from 'lib/components/Swap/RoutingDiagram/utils'
|
||||
import { Box } from 'rebass'
|
||||
import styled from 'styled-components/macro'
|
||||
import { ThemedText, Z_INDEX } from 'theme'
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`renders currency rows correctly when currencies list is non-empty 1`] = `
|
||||
<DocumentFragment>
|
||||
<div
|
||||
style="position: relative; height: 10px; width: 100%; overflow: auto; will-change: transform; direction: ltr;"
|
||||
>
|
||||
<div
|
||||
style="height: 168px; width: 100%;"
|
||||
>
|
||||
<div
|
||||
class="sc-bdnxRM Row-sc-u7azg8-0 Row__RowBetween-sc-u7azg8-1 styleds__MenuItem-sc-muzgnq-3 lmTMKd hLLNig hzJkYd firMKT token-item-0x6B175474E89094C44Da98b954EedeAC495271d0F"
|
||||
style="position: absolute; left: 0px; top: 0px; height: 56px; width: 100%;"
|
||||
>
|
||||
CurrencyLogo currency=DAI
|
||||
<div
|
||||
class="Column-sc-1r2yyln-0 cYEAJI"
|
||||
>
|
||||
<div
|
||||
class="css-8mokm4"
|
||||
title="Dai Stablecoin"
|
||||
>
|
||||
DAI
|
||||
</div>
|
||||
<div
|
||||
class="theme__TextWrapper-sc-5lu8um-0 gVIOIC css-165qfk5"
|
||||
>
|
||||
Dai Stablecoin
|
||||
</div>
|
||||
</div>
|
||||
<span />
|
||||
</div>
|
||||
<div
|
||||
class="sc-bdnxRM Row-sc-u7azg8-0 Row__RowBetween-sc-u7azg8-1 styleds__MenuItem-sc-muzgnq-3 lmTMKd hLLNig hzJkYd firMKT token-item-0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"
|
||||
style="position: absolute; left: 0px; top: 56px; height: 56px; width: 100%;"
|
||||
>
|
||||
CurrencyLogo currency=USDC
|
||||
<div
|
||||
class="Column-sc-1r2yyln-0 cYEAJI"
|
||||
>
|
||||
<div
|
||||
class="css-8mokm4"
|
||||
title="USD//C"
|
||||
>
|
||||
USDC
|
||||
</div>
|
||||
<div
|
||||
class="theme__TextWrapper-sc-5lu8um-0 gVIOIC css-165qfk5"
|
||||
>
|
||||
USD//C
|
||||
</div>
|
||||
</div>
|
||||
<span />
|
||||
</div>
|
||||
<div
|
||||
class="sc-bdnxRM Row-sc-u7azg8-0 Row__RowBetween-sc-u7azg8-1 styleds__MenuItem-sc-muzgnq-3 lmTMKd hLLNig hzJkYd firMKT token-item-0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599"
|
||||
style="position: absolute; left: 0px; top: 112px; height: 56px; width: 100%;"
|
||||
>
|
||||
CurrencyLogo currency=WBTC
|
||||
<div
|
||||
class="Column-sc-1r2yyln-0 cYEAJI"
|
||||
>
|
||||
<div
|
||||
class="css-8mokm4"
|
||||
title="Wrapped BTC"
|
||||
>
|
||||
WBTC
|
||||
</div>
|
||||
<div
|
||||
class="theme__TextWrapper-sc-5lu8um-0 gVIOIC css-165qfk5"
|
||||
>
|
||||
Wrapped BTC
|
||||
</div>
|
||||
</div>
|
||||
<span />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
||||
exports[`renders loading rows when isLoading is true 1`] = `
|
||||
<DocumentFragment>
|
||||
<div
|
||||
style="position: relative; height: 10px; width: 100%; overflow: auto; will-change: transform; direction: ltr;"
|
||||
>
|
||||
<div
|
||||
style="height: 0px; width: 100%;"
|
||||
/>
|
||||
</div>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
73
src/components/SearchModal/CurrencyList/index.test.tsx
Normal file
@@ -0,0 +1,73 @@
|
||||
import { Currency, CurrencyAmount as mockCurrencyAmount, Token as mockToken } from '@uniswap/sdk-core'
|
||||
import { DAI, USDC_MAINNET, WBTC } from 'constants/tokens'
|
||||
import * as mockJSBI from 'jsbi'
|
||||
import { render } from 'test-utils'
|
||||
|
||||
import CurrencyList from '.'
|
||||
|
||||
const noOp = function () {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
const mockCurrencyAmt = {
|
||||
[DAI.address]: mockCurrencyAmount.fromRawAmount(DAI, mockJSBI.default.BigInt(100)),
|
||||
[USDC_MAINNET.address]: mockCurrencyAmount.fromRawAmount(USDC_MAINNET, mockJSBI.default.BigInt(10)),
|
||||
[WBTC.address]: mockCurrencyAmount.fromRawAmount(WBTC, mockJSBI.default.BigInt(1)),
|
||||
}
|
||||
|
||||
jest.mock(
|
||||
'components/CurrencyLogo',
|
||||
() =>
|
||||
({ currency }: { currency: Currency }) =>
|
||||
`CurrencyLogo currency=${currency.symbol}`
|
||||
)
|
||||
|
||||
jest.mock('hooks/useActiveWeb3React', () => {
|
||||
return {
|
||||
__esModule: true,
|
||||
default: () => ({
|
||||
account: '123',
|
||||
active: true,
|
||||
}),
|
||||
}
|
||||
})
|
||||
|
||||
jest.mock('../../../state/wallet/hooks', () => {
|
||||
return {
|
||||
useCurrencyBalance: (currency: Currency) => {
|
||||
return mockCurrencyAmt[(currency as mockToken).address]
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
it('renders loading rows when isLoading is true', () => {
|
||||
const { asFragment } = render(
|
||||
<CurrencyList
|
||||
height={10}
|
||||
currencies={[]}
|
||||
otherListTokens={[]}
|
||||
selectedCurrency={null}
|
||||
onCurrencySelect={noOp}
|
||||
showImportView={noOp}
|
||||
setImportToken={noOp}
|
||||
isLoading={true}
|
||||
/>
|
||||
)
|
||||
expect(asFragment()).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it('renders currency rows correctly when currencies list is non-empty', () => {
|
||||
const { asFragment } = render(
|
||||
<CurrencyList
|
||||
height={10}
|
||||
currencies={[DAI, USDC_MAINNET, WBTC]}
|
||||
otherListTokens={[]}
|
||||
selectedCurrency={null}
|
||||
onCurrencySelect={noOp}
|
||||
showImportView={noOp}
|
||||
setImportToken={noOp}
|
||||
isLoading={false}
|
||||
/>
|
||||
)
|
||||
expect(asFragment()).toMatchSnapshot()
|
||||
})
|
||||
@@ -9,20 +9,20 @@ import { FixedSizeList } from 'react-window'
|
||||
import { Text } from 'rebass'
|
||||
import styled from 'styled-components/macro'
|
||||
|
||||
import TokenListLogo from '../../assets/svg/tokenlist.svg'
|
||||
import { useIsUserAddedToken } from '../../hooks/Tokens'
|
||||
import { useCombinedActiveList } from '../../state/lists/hooks'
|
||||
import { WrappedTokenInfo } from '../../state/lists/wrappedTokenInfo'
|
||||
import { useCurrencyBalance } from '../../state/wallet/hooks'
|
||||
import { ThemedText } from '../../theme'
|
||||
import { isTokenOnList } from '../../utils'
|
||||
import Column from '../Column'
|
||||
import CurrencyLogo from '../CurrencyLogo'
|
||||
import Loader from '../Loader'
|
||||
import { RowBetween, RowFixed } from '../Row'
|
||||
import { MouseoverTooltip } from '../Tooltip'
|
||||
import ImportRow from './ImportRow'
|
||||
import { MenuItem } from './styleds'
|
||||
import TokenListLogo from '../../../assets/svg/tokenlist.svg'
|
||||
import { useIsUserAddedToken } from '../../../hooks/Tokens'
|
||||
import { useCombinedActiveList } from '../../../state/lists/hooks'
|
||||
import { WrappedTokenInfo } from '../../../state/lists/wrappedTokenInfo'
|
||||
import { useCurrencyBalance } from '../../../state/wallet/hooks'
|
||||
import { ThemedText } from '../../../theme'
|
||||
import { isTokenOnList } from '../../../utils'
|
||||
import Column from '../../Column'
|
||||
import CurrencyLogo from '../../CurrencyLogo'
|
||||
import Loader from '../../Loader'
|
||||
import { RowBetween, RowFixed } from '../../Row'
|
||||
import { MouseoverTooltip } from '../../Tooltip'
|
||||
import ImportRow from '../ImportRow'
|
||||
import { LoadingRows, MenuItem } from '../styleds'
|
||||
|
||||
function currencyKey(currency: Currency): string {
|
||||
return currency.isToken ? currency.address : 'ETHER'
|
||||
@@ -195,6 +195,7 @@ export default function CurrencyList({
|
||||
showImportView,
|
||||
setImportToken,
|
||||
showCurrencyAmount,
|
||||
isLoading,
|
||||
}: {
|
||||
height: number
|
||||
currencies: Currency[]
|
||||
@@ -206,6 +207,7 @@ export default function CurrencyList({
|
||||
showImportView: () => void
|
||||
setImportToken: (token: Token) => void
|
||||
showCurrencyAmount?: boolean
|
||||
isLoading: boolean
|
||||
}) {
|
||||
const itemData: (Currency | BreakLine)[] = useMemo(() => {
|
||||
if (otherListTokens && otherListTokens?.length > 0) {
|
||||
@@ -232,7 +234,15 @@ export default function CurrencyList({
|
||||
|
||||
const showImport = index > currencies.length
|
||||
|
||||
if (showImport && token) {
|
||||
if (isLoading) {
|
||||
return (
|
||||
<LoadingRows>
|
||||
<div />
|
||||
<div />
|
||||
<div />
|
||||
</LoadingRows>
|
||||
)
|
||||
} else if (showImport && token) {
|
||||
return (
|
||||
<ImportRow style={style} token={token} showImportView={showImportView} setImportToken={setImportToken} dim />
|
||||
)
|
||||
@@ -259,6 +269,7 @@ export default function CurrencyList({
|
||||
setImportToken,
|
||||
showImportView,
|
||||
showCurrencyAmount,
|
||||
isLoading,
|
||||
]
|
||||
)
|
||||
|
||||
@@ -74,6 +74,8 @@ export function CurrencySearch({
|
||||
const { chainId } = useActiveWeb3React()
|
||||
const theme = useTheme()
|
||||
|
||||
const [tokenLoaderTimerElapsed, setTokenLoaderTimerElapsed] = useState(false)
|
||||
|
||||
// refs for fixed size lists
|
||||
const fixedList = useRef<FixedSizeList>()
|
||||
|
||||
@@ -103,10 +105,11 @@ export function CurrencySearch({
|
||||
return Object.values(allTokens).filter(getTokenFilter(debouncedQuery))
|
||||
}, [allTokens, debouncedQuery])
|
||||
|
||||
const balances = useAllTokenBalances()
|
||||
const [balances, balancesIsLoading] = useAllTokenBalances()
|
||||
const sortedTokens: Token[] = useMemo(() => {
|
||||
return filteredTokens.sort(tokenComparator.bind(null, balances))
|
||||
}, [balances, filteredTokens])
|
||||
void balancesIsLoading // creates a new array once balances load to update hooks
|
||||
return [...filteredTokens].sort(tokenComparator.bind(null, balances))
|
||||
}, [balances, filteredTokens, balancesIsLoading])
|
||||
|
||||
const filteredSortedTokens = useSortTokensByQuery(debouncedQuery, sortedTokens)
|
||||
|
||||
@@ -173,6 +176,14 @@ export function CurrencySearch({
|
||||
filteredTokens.length === 0 || (debouncedQuery.length > 2 && !isAddressSearch) ? debouncedQuery : undefined
|
||||
)
|
||||
|
||||
// Timeout token loader after 3 seconds to avoid hanging in a loading state.
|
||||
useEffect(() => {
|
||||
const tokenLoaderTimer = setTimeout(() => {
|
||||
setTokenLoaderTimerElapsed(true)
|
||||
}, 3000)
|
||||
return () => clearTimeout(tokenLoaderTimer)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<ContentWrapper>
|
||||
<PaddedColumn gap="16px">
|
||||
@@ -218,6 +229,7 @@ export function CurrencySearch({
|
||||
showImportView={showImportView}
|
||||
setImportToken={setImportToken}
|
||||
showCurrencyAmount={showCurrencyAmount}
|
||||
isLoading={balancesIsLoading && !tokenLoaderTimerElapsed}
|
||||
/>
|
||||
)}
|
||||
</AutoSizer>
|
||||
|
||||
@@ -26,7 +26,7 @@ import { ButtonEmpty, ButtonPrimary } from '../Button'
|
||||
import Column, { AutoColumn } from '../Column'
|
||||
import ListLogo from '../ListLogo'
|
||||
import Row, { RowBetween, RowFixed } from '../Row'
|
||||
import ListToggle from '../Toggle/ListToggle'
|
||||
import Toggle from '../Toggle'
|
||||
import { CurrencyModalView } from './CurrencySearchModal'
|
||||
import { PaddedColumn, SearchInput, Separator, SeparatorDark } from './styleds'
|
||||
|
||||
@@ -215,7 +215,7 @@ const ListRow = memo(function ListRow({ listUrl }: { listUrl: string }) {
|
||||
</StyledMenu>
|
||||
</RowFixed>
|
||||
</Column>
|
||||
<ListToggle
|
||||
<Toggle
|
||||
isActive={isActive}
|
||||
bgColor={listColor}
|
||||
toggle={() => {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { LoadingRows as BaseLoadingRows } from 'components/Loader/styled'
|
||||
import styled from 'styled-components/macro'
|
||||
|
||||
import { AutoColumn } from '../Column'
|
||||
@@ -72,3 +73,25 @@ export const SeparatorDark = styled.div`
|
||||
height: 1px;
|
||||
background-color: ${({ theme }) => theme.bg3};
|
||||
`
|
||||
|
||||
export const LoadingRows = styled(BaseLoadingRows)`
|
||||
grid-column-gap: 0.5em;
|
||||
grid-template-columns: repeat(12, 1fr);
|
||||
max-width: 960px;
|
||||
padding: 12px 20px;
|
||||
|
||||
& > div:nth-child(4n + 1) {
|
||||
grid-column: 1 / 8;
|
||||
height: 1em;
|
||||
margin-bottom: 0.25em;
|
||||
}
|
||||
& > div:nth-child(4n + 2) {
|
||||
grid-column: 12;
|
||||
height: 1em;
|
||||
margin-top: 0.25em;
|
||||
}
|
||||
& > div:nth-child(4n + 3) {
|
||||
grid-column: 1 / 4;
|
||||
height: 0.75em;
|
||||
}
|
||||
`
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import styled from 'styled-components/macro'
|
||||
|
||||
import { ThemedText } from '../../theme'
|
||||
|
||||
const Wrapper = styled.button<{ isActive?: boolean; activeElement?: boolean }>`
|
||||
border-radius: 20px;
|
||||
border: none;
|
||||
background: ${({ theme }) => theme.bg1};
|
||||
display: flex;
|
||||
width: fit-content;
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
padding: 0.4rem 0.4rem;
|
||||
align-items: center;
|
||||
`
|
||||
|
||||
const ToggleElement = styled.span<{ isActive?: boolean; bgColor?: string }>`
|
||||
border-radius: 50%;
|
||||
height: 24px;
|
||||
width: 24px;
|
||||
background-color: ${({ isActive, bgColor, theme }) => (isActive ? bgColor : theme.bg4)};
|
||||
:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
`
|
||||
|
||||
const StatusText = styled(ThemedText.Main)<{ isActive?: boolean }>`
|
||||
margin: 0 10px;
|
||||
width: 24px;
|
||||
color: ${({ theme, isActive }) => (isActive ? theme.text1 : theme.text3)};
|
||||
`
|
||||
|
||||
interface ToggleProps {
|
||||
id?: string
|
||||
isActive: boolean
|
||||
bgColor: string
|
||||
toggle: () => void
|
||||
}
|
||||
|
||||
export default function ListToggle({ id, isActive, bgColor, toggle }: ToggleProps) {
|
||||
return (
|
||||
<Wrapper id={id} isActive={isActive} onClick={toggle}>
|
||||
{isActive && (
|
||||
<StatusText fontWeight="600" margin="0 6px" isActive={true}>
|
||||
<Trans>ON</Trans>
|
||||
</StatusText>
|
||||
)}
|
||||
<ToggleElement isActive={isActive} bgColor={bgColor} />
|
||||
{!isActive && (
|
||||
<StatusText fontWeight="600" margin="0 6px" isActive={false}>
|
||||
<Trans>OFF</Trans>
|
||||
</StatusText>
|
||||
)}
|
||||
</Wrapper>
|
||||
)
|
||||
}
|
||||
@@ -1,57 +1,75 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import { darken } from 'polished'
|
||||
import { ReactNode } from 'react'
|
||||
import styled from 'styled-components/macro'
|
||||
import styled, { keyframes } from 'styled-components/macro'
|
||||
|
||||
const ToggleElement = styled.span<{ isActive?: boolean; isOnSwitch?: boolean }>`
|
||||
padding: 0.25rem 0.6rem;
|
||||
border-radius: 9px;
|
||||
background: ${({ theme, isActive, isOnSwitch }) => (isActive ? (isOnSwitch ? theme.primary1 : theme.bg4) : 'none')};
|
||||
color: ${({ theme, isActive }) => (isActive ? theme.white : theme.text2)};
|
||||
font-size: 14px;
|
||||
font-weight: ${({ isOnSwitch }) => (isOnSwitch ? '500' : '400')};
|
||||
:hover {
|
||||
user-select: ${({ isOnSwitch }) => (isOnSwitch ? 'none' : 'initial')};
|
||||
background: ${({ theme, isActive, isOnSwitch }) =>
|
||||
isActive ? (isOnSwitch ? darken(0.05, theme.primary1) : darken(0.05, theme.bg4)) : 'none'};
|
||||
color: ${({ theme, isActive, isOnSwitch }) => (isActive ? (isOnSwitch ? theme.white : theme.white) : theme.text3)};
|
||||
const Wrapper = styled.button<{ isActive?: boolean; activeElement?: boolean }>`
|
||||
align-items: center;
|
||||
background: ${({ theme }) => theme.bg1};
|
||||
border: none;
|
||||
border-radius: 20px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
outline: none;
|
||||
padding: 0.4rem 0.4rem;
|
||||
width: fit-content;
|
||||
`
|
||||
|
||||
const turnOnToggle = keyframes`
|
||||
from {
|
||||
margin-left: 0em;
|
||||
margin-right: 2.2em;
|
||||
}
|
||||
to {
|
||||
margin-left: 2.2em;
|
||||
margin-right: 0em;
|
||||
}
|
||||
`
|
||||
|
||||
const StyledToggle = styled.button<{ isActive?: boolean; activeElement?: boolean }>`
|
||||
border-radius: 12px;
|
||||
border: none;
|
||||
background: ${({ theme }) => theme.bg0};
|
||||
display: flex;
|
||||
width: fit-content;
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
padding: 2px;
|
||||
const turnOffToggle = keyframes`
|
||||
from {
|
||||
margin-left: 2.2em;
|
||||
margin-right: 0em;
|
||||
}
|
||||
to {
|
||||
margin-left: 0em;
|
||||
margin-right: 2.2em;
|
||||
}
|
||||
`
|
||||
|
||||
const ToggleElementHoverStyle = (hasBgColor: boolean, theme: any, isActive?: boolean) =>
|
||||
hasBgColor
|
||||
? {
|
||||
opacity: '0.8',
|
||||
}
|
||||
: {
|
||||
background: isActive ? darken(0.05, theme.primary1) : darken(0.05, theme.bg4),
|
||||
color: isActive ? theme.white : theme.text3,
|
||||
}
|
||||
|
||||
const ToggleElement = styled.span<{ isActive?: boolean; bgColor?: string }>`
|
||||
animation: 0.1s ${({ isActive }) => (isActive ? turnOnToggle : turnOffToggle)} ease-in;
|
||||
background: ${({ theme, bgColor, isActive }) =>
|
||||
isActive ? bgColor ?? theme.primary1 : !!bgColor ? theme.bg4 : theme.text3};
|
||||
border-radius: 50%;
|
||||
height: 24px;
|
||||
:hover {
|
||||
${({ bgColor, theme, isActive }) => ToggleElementHoverStyle(!!bgColor, theme, isActive)}
|
||||
}
|
||||
margin-left: ${({ isActive }) => (isActive ? '2.2em' : '0em')};
|
||||
margin-right: ${({ isActive }) => (!isActive ? '2.2em' : '0em')};
|
||||
width: 24px;
|
||||
`
|
||||
|
||||
interface ToggleProps {
|
||||
id?: string
|
||||
bgColor?: string
|
||||
isActive: boolean
|
||||
toggle: () => void
|
||||
checked?: ReactNode
|
||||
unchecked?: ReactNode
|
||||
}
|
||||
|
||||
export default function Toggle({
|
||||
id,
|
||||
isActive,
|
||||
toggle,
|
||||
checked = <Trans>On</Trans>,
|
||||
unchecked = <Trans>Off</Trans>,
|
||||
}: ToggleProps) {
|
||||
export default function Toggle({ id, bgColor, isActive, toggle }: ToggleProps) {
|
||||
return (
|
||||
<StyledToggle id={id} isActive={isActive} onClick={toggle}>
|
||||
<ToggleElement isActive={isActive} isOnSwitch={true}>
|
||||
{checked}
|
||||
</ToggleElement>
|
||||
<ToggleElement isActive={!isActive} isOnSwitch={false}>
|
||||
{unchecked}
|
||||
</ToggleElement>
|
||||
</StyledToggle>
|
||||
<Wrapper id={id} isActive={isActive} onClick={toggle}>
|
||||
<ToggleElement isActive={isActive} bgColor={bgColor} />
|
||||
</Wrapper>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ export const TooltipContainer = styled.div`
|
||||
|
||||
interface TooltipProps extends Omit<PopoverProps, 'content'> {
|
||||
text: ReactNode
|
||||
disableHover?: boolean // disable the hover and content display
|
||||
}
|
||||
|
||||
interface TooltipContentProps extends Omit<PopoverProps, 'content'> {
|
||||
@@ -29,19 +30,20 @@ interface TooltipContentProps extends Omit<PopoverProps, 'content'> {
|
||||
}
|
||||
|
||||
export default function Tooltip({ text, ...rest }: TooltipProps) {
|
||||
return <Popover content={<TooltipContainer>{text}</TooltipContainer>} {...rest} />
|
||||
return <Popover content={text && <TooltipContainer>{text}</TooltipContainer>} {...rest} />
|
||||
}
|
||||
|
||||
function TooltipContent({ content, wrap = false, ...rest }: TooltipContentProps) {
|
||||
return <Popover content={wrap ? <TooltipContainer>{content}</TooltipContainer> : content} {...rest} />
|
||||
}
|
||||
|
||||
export function MouseoverTooltip({ children, ...rest }: Omit<TooltipProps, 'show'>) {
|
||||
/** Standard text tooltip. */
|
||||
export function MouseoverTooltip({ text, disableHover, children, ...rest }: Omit<TooltipProps, 'show'>) {
|
||||
const [show, setShow] = useState(false)
|
||||
const open = useCallback(() => setShow(true), [setShow])
|
||||
const close = useCallback(() => setShow(false), [setShow])
|
||||
return (
|
||||
<Tooltip {...rest} show={show}>
|
||||
<Tooltip {...rest} show={show} text={disableHover ? null : text}>
|
||||
<div onMouseEnter={open} onMouseLeave={close}>
|
||||
{children}
|
||||
</div>
|
||||
@@ -49,6 +51,7 @@ export function MouseoverTooltip({ children, ...rest }: Omit<TooltipProps, 'show
|
||||
)
|
||||
}
|
||||
|
||||
/** Tooltip that displays custom content. */
|
||||
export function MouseoverTooltipContent({
|
||||
content,
|
||||
children,
|
||||
|
||||
23
src/components/TopLevelModals/index.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
import AddressClaimModal from 'components/claim/AddressClaimModal'
|
||||
import ConnectedAccountBlocked from 'components/ConnectedAccountBlocked'
|
||||
import useAccountRiskCheck from 'hooks/useAccountRiskCheck'
|
||||
import useActiveWeb3React from 'hooks/useActiveWeb3React'
|
||||
import { useModalOpen, useToggleModal } from 'state/application/hooks'
|
||||
import { ApplicationModal } from 'state/application/reducer'
|
||||
|
||||
export default function TopLevelModals() {
|
||||
const addressClaimOpen = useModalOpen(ApplicationModal.ADDRESS_CLAIM)
|
||||
const addressClaimToggle = useToggleModal(ApplicationModal.ADDRESS_CLAIM)
|
||||
|
||||
const blockedAccountModalOpen = useModalOpen(ApplicationModal.BLOCKED_ACCOUNT)
|
||||
const { account } = useActiveWeb3React()
|
||||
|
||||
useAccountRiskCheck(account)
|
||||
const open = Boolean(blockedAccountModalOpen && account)
|
||||
return (
|
||||
<>
|
||||
<AddressClaimModal isOpen={addressClaimOpen} onDismiss={addressClaimToggle} />
|
||||
<ConnectedAccountBlocked account={account} isOpen={open} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -1,12 +1,10 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import { darken } from 'polished'
|
||||
import { ButtonEmpty, ButtonPrimary } from 'components/Button'
|
||||
import styled from 'styled-components/macro'
|
||||
import { ThemedText } from 'theme'
|
||||
import { AbstractConnector } from 'web3-react-abstract-connector'
|
||||
|
||||
import { injected } from '../../connectors'
|
||||
import { SUPPORTED_WALLETS } from '../../constants/wallet'
|
||||
import Loader from '../Loader'
|
||||
import Option from './Option'
|
||||
|
||||
const PendingSection = styled.div`
|
||||
${({ theme }) => theme.flexColumnNoWrap};
|
||||
@@ -18,18 +16,18 @@ const PendingSection = styled.div`
|
||||
}
|
||||
`
|
||||
|
||||
const StyledLoader = styled(Loader)`
|
||||
margin-right: 1rem;
|
||||
`
|
||||
|
||||
const LoadingMessage = styled.div<{ error?: boolean }>`
|
||||
const LoaderContainer = styled.div`
|
||||
margin: 16px 0;
|
||||
${({ theme }) => theme.flexRowNoWrap};
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
justify-content: center;
|
||||
`
|
||||
|
||||
const LoadingMessage = styled.div`
|
||||
${({ theme }) => theme.flexRowNoWrap};
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 12px;
|
||||
margin-bottom: 20px;
|
||||
color: ${({ theme, error }) => (error ? theme.red1 : 'inherit')};
|
||||
border: 1px solid ${({ theme, error }) => (error ? theme.red1 : theme.text4)};
|
||||
|
||||
& > * {
|
||||
padding: 1rem;
|
||||
@@ -37,29 +35,13 @@ const LoadingMessage = styled.div<{ error?: boolean }>`
|
||||
`
|
||||
|
||||
const ErrorGroup = styled.div`
|
||||
${({ theme }) => theme.flexRowNoWrap};
|
||||
${({ theme }) => theme.flexColumnNoWrap};
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
`
|
||||
|
||||
const ErrorButton = styled.div`
|
||||
border-radius: 8px;
|
||||
font-size: 12px;
|
||||
color: ${({ theme }) => theme.text1};
|
||||
background-color: ${({ theme }) => theme.bg4};
|
||||
margin-left: 1rem;
|
||||
padding: 0.5rem;
|
||||
font-weight: 600;
|
||||
user-select: none;
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
background-color: ${({ theme }) => darken(0.1, theme.text4)};
|
||||
}
|
||||
`
|
||||
|
||||
const LoadingWrapper = styled.div`
|
||||
${({ theme }) => theme.flexRowNoWrap};
|
||||
${({ theme }) => theme.flexColumnNoWrap};
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
`
|
||||
@@ -69,65 +51,56 @@ export default function PendingView({
|
||||
error = false,
|
||||
setPendingError,
|
||||
tryActivation,
|
||||
resetAccountView,
|
||||
}: {
|
||||
connector?: AbstractConnector
|
||||
error?: boolean
|
||||
setPendingError: (error: boolean) => void
|
||||
tryActivation: (connector: AbstractConnector) => void
|
||||
resetAccountView: () => void
|
||||
}) {
|
||||
const isMetamask = window?.ethereum?.isMetaMask
|
||||
|
||||
return (
|
||||
<PendingSection>
|
||||
<LoadingMessage error={error}>
|
||||
<LoadingMessage>
|
||||
<LoadingWrapper>
|
||||
{error ? (
|
||||
<ErrorGroup>
|
||||
<div>
|
||||
<ThemedText.MediumHeader marginBottom={12}>
|
||||
<Trans>Error connecting</Trans>
|
||||
</div>
|
||||
<ErrorButton
|
||||
</ThemedText.MediumHeader>
|
||||
<ThemedText.Body fontSize={14} marginBottom={36} textAlign="center">
|
||||
<Trans>
|
||||
The connection attempt failed. Please click try again and follow the steps to connect in your wallet.
|
||||
</Trans>
|
||||
</ThemedText.Body>
|
||||
<ButtonPrimary
|
||||
$borderRadius="12px"
|
||||
padding="12px"
|
||||
onClick={() => {
|
||||
setPendingError(false)
|
||||
connector && tryActivation(connector)
|
||||
}}
|
||||
>
|
||||
<Trans>Try Again</Trans>
|
||||
</ErrorButton>
|
||||
</ButtonPrimary>
|
||||
<ButtonEmpty width="fit-content" padding="0" marginTop={20}>
|
||||
<ThemedText.Link fontSize={12} onClick={resetAccountView}>
|
||||
<Trans>Back to wallet selection</Trans>
|
||||
</ThemedText.Link>
|
||||
</ButtonEmpty>
|
||||
</ErrorGroup>
|
||||
) : (
|
||||
<>
|
||||
<StyledLoader />
|
||||
<Trans>Initializing...</Trans>
|
||||
<ThemedText.Black fontSize={20} marginY={16}>
|
||||
<LoaderContainer>
|
||||
<Loader stroke="currentColor" size="32px" />
|
||||
</LoaderContainer>
|
||||
<Trans>Connecting...</Trans>
|
||||
</ThemedText.Black>
|
||||
</>
|
||||
)}
|
||||
</LoadingWrapper>
|
||||
</LoadingMessage>
|
||||
{Object.keys(SUPPORTED_WALLETS).map((key) => {
|
||||
const option = SUPPORTED_WALLETS[key]
|
||||
if (option.connector === connector) {
|
||||
if (option.connector === injected) {
|
||||
if (isMetamask && option.name !== 'MetaMask') {
|
||||
return null
|
||||
}
|
||||
if (!isMetamask && option.name === 'MetaMask') {
|
||||
return null
|
||||
}
|
||||
}
|
||||
return (
|
||||
<Option
|
||||
id={`connect-${key}`}
|
||||
key={key}
|
||||
clickable={false}
|
||||
color={option.color}
|
||||
header={option.name}
|
||||
subheader={option.description}
|
||||
icon={option.iconURL}
|
||||
/>
|
||||
)
|
||||
}
|
||||
return null
|
||||
})}
|
||||
</PendingSection>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import { AutoColumn } from 'components/Column'
|
||||
import { PrivacyPolicy } from 'components/PrivacyPolicy'
|
||||
import Row, { AutoRow, RowBetween } from 'components/Row'
|
||||
import { useWalletConnectMonitoringEventCallback } from 'hooks/useMonitoringEventCallback'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { ArrowLeft, ArrowRight, Info } from 'react-feather'
|
||||
import Row, { AutoRow } from 'components/Row'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { ArrowLeft } from 'react-feather'
|
||||
import ReactGA from 'react-ga4'
|
||||
import styled from 'styled-components/macro'
|
||||
import { AbstractConnector } from 'web3-react-abstract-connector'
|
||||
@@ -12,6 +11,7 @@ import { UnsupportedChainIdError, useWeb3React } from 'web3-react-core'
|
||||
import { WalletConnectConnector } from 'web3-react-walletconnect-connector'
|
||||
|
||||
import MetamaskIcon from '../../assets/images/metamask.png'
|
||||
import TallyIcon from '../../assets/images/tally.png'
|
||||
import { ReactComponent as Close } from '../../assets/images/x.svg'
|
||||
import { fortmatic, injected } from '../../connectors'
|
||||
import { OVERLAY_READY } from '../../connectors/Fortmatic'
|
||||
@@ -22,7 +22,7 @@ import { ApplicationModal } from '../../state/application/reducer'
|
||||
import { ExternalLink, ThemedText } from '../../theme'
|
||||
import { isMobile } from '../../utils/userAgent'
|
||||
import AccountDetails from '../AccountDetails'
|
||||
import Card, { LightCard } from '../Card'
|
||||
import { LightCard } from '../Card'
|
||||
import Modal from '../Modal'
|
||||
import Option from './Option'
|
||||
import PendingView from './PendingView'
|
||||
@@ -109,16 +109,6 @@ const HoverText = styled.div`
|
||||
}
|
||||
`
|
||||
|
||||
const LinkCard = styled(Card)`
|
||||
background-color: ${({ theme }) => theme.bg1};
|
||||
color: ${({ theme }) => theme.text3};
|
||||
|
||||
:hover {
|
||||
cursor: pointer;
|
||||
filter: brightness(0.9);
|
||||
}
|
||||
`
|
||||
|
||||
const WALLET_VIEWS = {
|
||||
OPTIONS: 'options',
|
||||
OPTIONS_SECONDARY: 'options_secondary',
|
||||
@@ -137,7 +127,7 @@ export default function WalletModal({
|
||||
ENSName?: string
|
||||
}) {
|
||||
// important that these are destructed from the account-specific web3-react context
|
||||
const { active, account, connector, activate, error } = useWeb3React()
|
||||
const { account, connector, activate, error } = useWeb3React()
|
||||
|
||||
const [walletView, setWalletView] = useState(WALLET_VIEWS.ACCOUNT)
|
||||
const previousWalletView = usePrevious(walletView)
|
||||
@@ -151,7 +141,10 @@ export default function WalletModal({
|
||||
|
||||
const previousAccount = usePrevious(account)
|
||||
|
||||
const logMonitoringEvent = useWalletConnectMonitoringEventCallback()
|
||||
const resetAccountView = useCallback(() => {
|
||||
setPendingError(false)
|
||||
setWalletView(WALLET_VIEWS.ACCOUNT)
|
||||
}, [setPendingError, setWalletView])
|
||||
|
||||
// close on connection, when logged out before
|
||||
useEffect(() => {
|
||||
@@ -163,19 +156,9 @@ export default function WalletModal({
|
||||
// always reset to account view
|
||||
useEffect(() => {
|
||||
if (walletModalOpen) {
|
||||
setPendingError(false)
|
||||
setWalletView(WALLET_VIEWS.ACCOUNT)
|
||||
resetAccountView()
|
||||
}
|
||||
}, [walletModalOpen])
|
||||
|
||||
// close modal when a connection is successful
|
||||
const activePrevious = usePrevious(active)
|
||||
const connectorPrevious = usePrevious(connector)
|
||||
useEffect(() => {
|
||||
if (walletModalOpen && ((active && !activePrevious) || (connector && connector !== connectorPrevious && !error))) {
|
||||
setWalletView(WALLET_VIEWS.ACCOUNT)
|
||||
}
|
||||
}, [setWalletView, active, error, connector, walletModalOpen, activePrevious, connectorPrevious])
|
||||
}, [walletModalOpen, resetAccountView])
|
||||
|
||||
const tryActivation = async (connector: AbstractConnector | undefined) => {
|
||||
let name = ''
|
||||
@@ -200,18 +183,13 @@ export default function WalletModal({
|
||||
}
|
||||
|
||||
connector &&
|
||||
activate(connector, undefined, true)
|
||||
.then(async () => {
|
||||
const walletAddress = await connector.getAccount()
|
||||
logMonitoringEvent({ walletAddress })
|
||||
})
|
||||
.catch((error) => {
|
||||
if (error instanceof UnsupportedChainIdError) {
|
||||
activate(connector) // a little janky...can't use setError because the connector isn't set
|
||||
} else {
|
||||
setPendingError(true)
|
||||
}
|
||||
})
|
||||
activate(connector, undefined, true).catch((error) => {
|
||||
if (error instanceof UnsupportedChainIdError) {
|
||||
activate(connector) // a little janky...can't use setError because the connector isn't set
|
||||
} else {
|
||||
setPendingError(true)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// close wallet modal if fortmatic modal is active
|
||||
@@ -223,7 +201,8 @@ export default function WalletModal({
|
||||
|
||||
// get wallets user can switch too, depending on device/browser
|
||||
function getOptions() {
|
||||
const isMetamask = window.ethereum && window.ethereum.isMetaMask
|
||||
const isMetamask = !!window.ethereum?.isMetaMask
|
||||
const isTally = !!window.ethereum?.isTally
|
||||
return Object.keys(SUPPORTED_WALLETS).map((key) => {
|
||||
const option = SUPPORTED_WALLETS[key]
|
||||
// check for mobile options
|
||||
@@ -275,6 +254,24 @@ export default function WalletModal({
|
||||
// likewise for generic
|
||||
else if (option.name === 'Injected' && isMetamask) {
|
||||
return null
|
||||
} else if (option.name === 'Injected' && isTally) {
|
||||
return (
|
||||
<Option
|
||||
id={`connect-${key}`}
|
||||
key={key}
|
||||
onClick={() => {
|
||||
option.connector === connector
|
||||
? setWalletView(WALLET_VIEWS.ACCOUNT)
|
||||
: !option.href && tryActivation(option.connector)
|
||||
}}
|
||||
color={'#E8831D'}
|
||||
header={<Trans>Tally</Trans>}
|
||||
active={option.connector === connector}
|
||||
subheader={null}
|
||||
link={null}
|
||||
icon={TallyIcon}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -366,12 +363,7 @@ export default function WalletModal({
|
||||
</CloseIcon>
|
||||
{walletView !== WALLET_VIEWS.ACCOUNT ? (
|
||||
<HeaderRow color="blue">
|
||||
<HoverText
|
||||
onClick={() => {
|
||||
setPendingError(false)
|
||||
setWalletView(WALLET_VIEWS.ACCOUNT)
|
||||
}}
|
||||
>
|
||||
<HoverText onClick={resetAccountView}>
|
||||
<ArrowLeft />
|
||||
</HoverText>
|
||||
</HeaderRow>
|
||||
@@ -385,39 +377,38 @@ export default function WalletModal({
|
||||
|
||||
<ContentWrapper>
|
||||
<AutoColumn gap="16px">
|
||||
<LightCard>
|
||||
<AutoRow style={{ flexWrap: 'nowrap' }}>
|
||||
<ThemedText.Black fontSize={14}>
|
||||
<Trans>
|
||||
By connecting a wallet, you agree to Uniswap Labs’{' '}
|
||||
<ExternalLink href="https://uniswap.org/terms-of-service/">Terms of Service</ExternalLink> and
|
||||
acknowledge that you have read and understand the Uniswap{' '}
|
||||
<ExternalLink href="https://uniswap.org/disclaimer/">Protocol Disclaimer</ExternalLink>.
|
||||
</Trans>
|
||||
</ThemedText.Black>
|
||||
</AutoRow>
|
||||
</LightCard>
|
||||
{walletView === WALLET_VIEWS.PENDING ? (
|
||||
{walletView === WALLET_VIEWS.PENDING && (
|
||||
<PendingView
|
||||
connector={pendingWallet}
|
||||
error={pendingError}
|
||||
setPendingError={setPendingError}
|
||||
tryActivation={tryActivation}
|
||||
resetAccountView={resetAccountView}
|
||||
/>
|
||||
) : (
|
||||
<OptionGrid>{getOptions()}</OptionGrid>
|
||||
)}
|
||||
<LinkCard padding=".5rem" $borderRadius=".75rem" onClick={() => setWalletView(WALLET_VIEWS.LEGAL)}>
|
||||
<RowBetween>
|
||||
<AutoRow gap="4px">
|
||||
<Info size={20} />
|
||||
<ThemedText.Label fontSize={14}>
|
||||
<Trans>How this app uses APIs</Trans>
|
||||
</ThemedText.Label>
|
||||
{walletView !== WALLET_VIEWS.PENDING && <OptionGrid>{getOptions()}</OptionGrid>}
|
||||
{!pendingError && (
|
||||
<LightCard>
|
||||
<AutoRow style={{ flexWrap: 'nowrap' }}>
|
||||
<ThemedText.Body fontSize={12}>
|
||||
<Trans>
|
||||
By connecting a wallet, you agree to Uniswap Labs’{' '}
|
||||
<ExternalLink
|
||||
style={{ textDecoration: 'underline' }}
|
||||
href="https://uniswap.org/terms-of-service/"
|
||||
>
|
||||
Terms of Service
|
||||
</ExternalLink>{' '}
|
||||
and acknowledge that you have read and understand the Uniswap{' '}
|
||||
<ExternalLink style={{ textDecoration: 'underline' }} href="https://uniswap.org/disclaimer/">
|
||||
Protocol Disclaimer
|
||||
</ExternalLink>
|
||||
.
|
||||
</Trans>
|
||||
</ThemedText.Body>
|
||||
</AutoRow>
|
||||
<ArrowRight size={16} />
|
||||
</RowBetween>
|
||||
</LinkCard>
|
||||
</LightCard>
|
||||
)}
|
||||
</AutoColumn>
|
||||
</ContentWrapper>
|
||||
</UpperSection>
|
||||
|
||||
@@ -13,7 +13,7 @@ import useENSName from '../../hooks/useENSName'
|
||||
import { useHasSocks } from '../../hooks/useSocksBalance'
|
||||
import { useWalletModalToggle } from '../../state/application/hooks'
|
||||
import { isTransactionRecent, useAllTransactions } from '../../state/transactions/hooks'
|
||||
import { TransactionDetails } from '../../state/transactions/reducer'
|
||||
import { TransactionDetails } from '../../state/transactions/types'
|
||||
import { shortenAddress } from '../../utils'
|
||||
import { ButtonSecondary } from '../Button'
|
||||
import StatusIcon from '../Identicon/StatusIcon'
|
||||
|
||||
@@ -7,8 +7,8 @@ import styled from 'styled-components/macro'
|
||||
|
||||
import { useContract } from '../../hooks/useContract'
|
||||
import { StakingInfo } from '../../state/stake/hooks'
|
||||
import { TransactionType } from '../../state/transactions/actions'
|
||||
import { useTransactionAdder } from '../../state/transactions/hooks'
|
||||
import { TransactionType } from '../../state/transactions/types'
|
||||
import { CloseIcon, ThemedText } from '../../theme'
|
||||
import { ButtonError } from '../Button'
|
||||
import { AutoColumn } from '../Column'
|
||||
|
||||
@@ -12,8 +12,8 @@ import { ApprovalState, useApproveCallback } from '../../hooks/useApproveCallbac
|
||||
import { useContract, usePairContract, useV2RouterContract } from '../../hooks/useContract'
|
||||
import useTransactionDeadline from '../../hooks/useTransactionDeadline'
|
||||
import { StakingInfo, useDerivedStakeInfo } from '../../state/stake/hooks'
|
||||
import { TransactionType } from '../../state/transactions/actions'
|
||||
import { useTransactionAdder } from '../../state/transactions/hooks'
|
||||
import { TransactionType } from '../../state/transactions/types'
|
||||
import { CloseIcon, ThemedText } from '../../theme'
|
||||
import { formatCurrencyAmount } from '../../utils/formatCurrencyAmount'
|
||||
import { maxAmountSpend } from '../../utils/maxAmountSpend'
|
||||
|
||||
@@ -7,8 +7,8 @@ import styled from 'styled-components/macro'
|
||||
|
||||
import { useContract } from '../../hooks/useContract'
|
||||
import { StakingInfo } from '../../state/stake/hooks'
|
||||
import { TransactionType } from '../../state/transactions/actions'
|
||||
import { useTransactionAdder } from '../../state/transactions/hooks'
|
||||
import { TransactionType } from '../../state/transactions/types'
|
||||
import { CloseIcon, ThemedText } from '../../theme'
|
||||
import { ButtonError } from '../Button'
|
||||
import { AutoColumn } from '../Column'
|
||||
|
||||
@@ -4,6 +4,7 @@ import Card from 'components/Card'
|
||||
import { LoadingRows } from 'components/Loader/styled'
|
||||
import { SUPPORTED_GAS_ESTIMATE_CHAIN_IDS } from 'constants/chains'
|
||||
import useActiveWeb3React from 'hooks/useActiveWeb3React'
|
||||
import useNativeCurrency from 'lib/hooks/useNativeCurrency'
|
||||
import { useContext, useMemo } from 'react'
|
||||
import { InterfaceTrade } from 'state/routing/types'
|
||||
import styled, { ThemeContext } from 'styled-components/macro'
|
||||
@@ -12,6 +13,7 @@ import { Separator, ThemedText } from '../../theme'
|
||||
import { computeRealizedLPFeePercent } from '../../utils/prices'
|
||||
import { AutoColumn } from '../Column'
|
||||
import { RowBetween, RowFixed } from '../Row'
|
||||
import { MouseoverTooltip } from '../Tooltip'
|
||||
import FormattedPriceImpact from './FormattedPriceImpact'
|
||||
|
||||
const StyledCard = styled(Card)`
|
||||
@@ -23,6 +25,7 @@ interface AdvancedSwapDetailsProps {
|
||||
allowedSlippage: Percent
|
||||
syncing?: boolean
|
||||
hideRouteDiagram?: boolean
|
||||
hideInfoTooltips?: boolean
|
||||
}
|
||||
|
||||
function TextWithLoadingPlaceholder({
|
||||
@@ -43,9 +46,15 @@ function TextWithLoadingPlaceholder({
|
||||
)
|
||||
}
|
||||
|
||||
export function AdvancedSwapDetails({ trade, allowedSlippage, syncing = false }: AdvancedSwapDetailsProps) {
|
||||
export function AdvancedSwapDetails({
|
||||
trade,
|
||||
allowedSlippage,
|
||||
syncing = false,
|
||||
hideInfoTooltips = false,
|
||||
}: AdvancedSwapDetailsProps) {
|
||||
const theme = useContext(ThemeContext)
|
||||
const { chainId } = useActiveWeb3React()
|
||||
const nativeCurrency = useNativeCurrency()
|
||||
|
||||
const { expectedOutputAmount, priceImpact } = useMemo(() => {
|
||||
if (!trade) return { expectedOutputAmount: undefined, priceImpact: undefined }
|
||||
@@ -60,9 +69,19 @@ export function AdvancedSwapDetails({ trade, allowedSlippage, syncing = false }:
|
||||
<AutoColumn gap="8px">
|
||||
<RowBetween>
|
||||
<RowFixed>
|
||||
<ThemedText.SubHeader color={theme.text1}>
|
||||
<Trans>Expected Output</Trans>
|
||||
</ThemedText.SubHeader>
|
||||
<MouseoverTooltip
|
||||
text={
|
||||
<Trans>
|
||||
The amount you expect to receive at the current market price. You may receive less or more if the
|
||||
market price changes while your transaction is pending.
|
||||
</Trans>
|
||||
}
|
||||
disableHover={hideInfoTooltips}
|
||||
>
|
||||
<ThemedText.SubHeader color={theme.text1}>
|
||||
<Trans>Expected Output</Trans>
|
||||
</ThemedText.SubHeader>
|
||||
</MouseoverTooltip>
|
||||
</RowFixed>
|
||||
<TextWithLoadingPlaceholder syncing={syncing} width={65}>
|
||||
<ThemedText.Black textAlign="right" fontSize={14}>
|
||||
@@ -74,9 +93,14 @@ export function AdvancedSwapDetails({ trade, allowedSlippage, syncing = false }:
|
||||
</RowBetween>
|
||||
<RowBetween>
|
||||
<RowFixed>
|
||||
<ThemedText.SubHeader color={theme.text1}>
|
||||
<Trans>Price Impact</Trans>
|
||||
</ThemedText.SubHeader>
|
||||
<MouseoverTooltip
|
||||
text={<Trans>The impact your trade has on the market price of this pool.</Trans>}
|
||||
disableHover={hideInfoTooltips}
|
||||
>
|
||||
<ThemedText.SubHeader color={theme.text1}>
|
||||
<Trans>Price Impact</Trans>
|
||||
</ThemedText.SubHeader>
|
||||
</MouseoverTooltip>
|
||||
</RowFixed>
|
||||
<TextWithLoadingPlaceholder syncing={syncing} width={50}>
|
||||
<ThemedText.Black textAlign="right" fontSize={14}>
|
||||
@@ -87,14 +111,24 @@ export function AdvancedSwapDetails({ trade, allowedSlippage, syncing = false }:
|
||||
<Separator />
|
||||
<RowBetween>
|
||||
<RowFixed style={{ marginRight: '20px' }}>
|
||||
<ThemedText.SubHeader color={theme.text3}>
|
||||
{trade.tradeType === TradeType.EXACT_INPUT ? (
|
||||
<Trans>Minimum received</Trans>
|
||||
) : (
|
||||
<Trans>Maximum sent</Trans>
|
||||
)}{' '}
|
||||
<Trans>after slippage</Trans> ({allowedSlippage.toFixed(2)}%)
|
||||
</ThemedText.SubHeader>
|
||||
<MouseoverTooltip
|
||||
text={
|
||||
<Trans>
|
||||
The minimum amount you are guaranteed to receive. If the price slips any further, your transaction
|
||||
will revert.
|
||||
</Trans>
|
||||
}
|
||||
disableHover={hideInfoTooltips}
|
||||
>
|
||||
<ThemedText.SubHeader color={theme.text3}>
|
||||
{trade.tradeType === TradeType.EXACT_INPUT ? (
|
||||
<Trans>Minimum received</Trans>
|
||||
) : (
|
||||
<Trans>Maximum sent</Trans>
|
||||
)}{' '}
|
||||
<Trans>after slippage</Trans> ({allowedSlippage.toFixed(2)}%)
|
||||
</ThemedText.SubHeader>
|
||||
</MouseoverTooltip>
|
||||
</RowFixed>
|
||||
<TextWithLoadingPlaceholder syncing={syncing} width={70}>
|
||||
<ThemedText.Black textAlign="right" fontSize={14} color={theme.text3}>
|
||||
@@ -106,9 +140,18 @@ export function AdvancedSwapDetails({ trade, allowedSlippage, syncing = false }:
|
||||
</RowBetween>
|
||||
{!trade?.gasUseEstimateUSD || !chainId || !SUPPORTED_GAS_ESTIMATE_CHAIN_IDS.includes(chainId) ? null : (
|
||||
<RowBetween>
|
||||
<ThemedText.SubHeader color={theme.text3}>
|
||||
<Trans>Network Fee</Trans>
|
||||
</ThemedText.SubHeader>
|
||||
<MouseoverTooltip
|
||||
text={
|
||||
<Trans>
|
||||
The fee paid to miners who process your transaction. This must be paid in {nativeCurrency.symbol}.
|
||||
</Trans>
|
||||
}
|
||||
disableHover={hideInfoTooltips}
|
||||
>
|
||||
<ThemedText.SubHeader color={theme.text3}>
|
||||
<Trans>Network Fee</Trans>
|
||||
</ThemedText.SubHeader>
|
||||
</MouseoverTooltip>
|
||||
<TextWithLoadingPlaceholder syncing={syncing} width={50}>
|
||||
<ThemedText.Black textAlign="right" fontSize={14} color={theme.text3}>
|
||||
~${trade.gasUseEstimateUSD.toFixed(2)}
|
||||
|
||||
@@ -147,7 +147,12 @@ export default function SwapDetailsDropdown({
|
||||
content={
|
||||
<ResponsiveTooltipContainer origin="top right" style={{ padding: '0' }}>
|
||||
<Card padding="12px">
|
||||
<AdvancedSwapDetails trade={trade} allowedSlippage={allowedSlippage} syncing={syncing} />
|
||||
<AdvancedSwapDetails
|
||||
trade={trade}
|
||||
allowedSlippage={allowedSlippage}
|
||||
syncing={syncing}
|
||||
hideInfoTooltips={true}
|
||||
/>
|
||||
</Card>
|
||||
</ResponsiveTooltipContainer>
|
||||
}
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import { Currency, TradeType } from '@uniswap/sdk-core'
|
||||
import { Protocol } from '@uniswap/router-sdk'
|
||||
import { Currency, Percent, TradeType } from '@uniswap/sdk-core'
|
||||
import { Pair } from '@uniswap/v2-sdk'
|
||||
import { FeeAmount } from '@uniswap/v3-sdk'
|
||||
import AnimatedDropdown from 'components/AnimatedDropdown'
|
||||
import { AutoColumn } from 'components/Column'
|
||||
import { LoadingRows } from 'components/Loader/styled'
|
||||
@@ -8,7 +11,6 @@ import { AutoRow, RowBetween } from 'components/Row'
|
||||
import { SUPPORTED_GAS_ESTIMATE_CHAIN_IDS } from 'constants/chains'
|
||||
import useActiveWeb3React from 'hooks/useActiveWeb3React'
|
||||
import useAutoRouterSupported from 'hooks/useAutoRouterSupported'
|
||||
import { getTokenPath } from 'lib/components/Swap/RoutingDiagram/utils'
|
||||
import { memo, useState } from 'react'
|
||||
import { Plus } from 'react-feather'
|
||||
import { InterfaceTrade } from 'state/routing/types'
|
||||
@@ -106,3 +108,41 @@ export default memo(function SwapRoute({ trade, syncing, fixedOpen = false, ...r
|
||||
</Wrapper>
|
||||
)
|
||||
})
|
||||
|
||||
export interface RoutingDiagramEntry {
|
||||
percent: Percent
|
||||
path: [Currency, Currency, FeeAmount][]
|
||||
protocol: Protocol
|
||||
}
|
||||
|
||||
const V2_DEFAULT_FEE_TIER = 3000
|
||||
|
||||
/**
|
||||
* Loops through all routes on a trade and returns an array of diagram entries.
|
||||
*/
|
||||
export function getTokenPath(trade: InterfaceTrade<Currency, Currency, TradeType>): RoutingDiagramEntry[] {
|
||||
return trade.swaps.map(({ route: { path: tokenPath, pools, protocol }, inputAmount, outputAmount }) => {
|
||||
const portion =
|
||||
trade.tradeType === TradeType.EXACT_INPUT
|
||||
? inputAmount.divide(trade.inputAmount)
|
||||
: outputAmount.divide(trade.outputAmount)
|
||||
const percent = new Percent(portion.numerator, portion.denominator)
|
||||
const path: RoutingDiagramEntry['path'] = []
|
||||
for (let i = 0; i < pools.length; i++) {
|
||||
const nextPool = pools[i]
|
||||
const tokenIn = tokenPath[i]
|
||||
const tokenOut = tokenPath[i + 1]
|
||||
const entry: RoutingDiagramEntry['path'][0] = [
|
||||
tokenIn,
|
||||
tokenOut,
|
||||
nextPool instanceof Pair ? V2_DEFAULT_FEE_TIER : nextPool.fee,
|
||||
]
|
||||
path.push(entry)
|
||||
}
|
||||
return {
|
||||
percent,
|
||||
path,
|
||||
protocol,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -33,6 +33,11 @@ export default function TradePrice({ price, showInverted, setShowInverted }: Tra
|
||||
const theme = useContext(ThemeContext)
|
||||
|
||||
const usdcPrice = useUSDCPrice(showInverted ? price.baseCurrency : price.quoteCurrency)
|
||||
/*
|
||||
* calculate needed amount of decimal prices, for prices between 0.95-1.05 use 4 decimal places
|
||||
*/
|
||||
const p = Number(usdcPrice?.toFixed())
|
||||
const visibleDecimalPlaces = p < 1.05 ? 4 : 2
|
||||
|
||||
let formattedPrice: string
|
||||
try {
|
||||
@@ -60,7 +65,7 @@ export default function TradePrice({ price, showInverted, setShowInverted }: Tra
|
||||
</Text>{' '}
|
||||
{usdcPrice && (
|
||||
<ThemedText.DarkGray>
|
||||
<Trans>(${usdcPrice.toSignificant(6, { groupSeparator: ',' })})</Trans>
|
||||
<Trans>(${usdcPrice.toFixed(visibleDecimalPlaces, { groupSeparator: ',' })})</Trans>
|
||||
</ThemedText.DarkGray>
|
||||
)}
|
||||
</StyledPriceContainer>
|
||||
|
||||
@@ -94,7 +94,7 @@ export const CHAIN_INFO: ChainInfoMap = {
|
||||
[SupportedChainId.OPTIMISM]: {
|
||||
networkType: NetworkType.L2,
|
||||
blockWaitMsBeforeWarning: ms`25m`,
|
||||
bridge: 'https://gateway.optimism.io/?chainId=1',
|
||||
bridge: 'https://app.optimism.io/bridge',
|
||||
defaultListUrl: OPTIMISM_LIST,
|
||||
docs: 'https://optimism.io/',
|
||||
explorer: 'https://optimistic.etherscan.io/',
|
||||
@@ -108,7 +108,7 @@ export const CHAIN_INFO: ChainInfoMap = {
|
||||
[SupportedChainId.OPTIMISTIC_KOVAN]: {
|
||||
networkType: NetworkType.L2,
|
||||
blockWaitMsBeforeWarning: ms`25m`,
|
||||
bridge: 'https://gateway.optimism.io/',
|
||||
bridge: 'https://app.optimism.io/bridge',
|
||||
defaultListUrl: OPTIMISM_LIST,
|
||||
docs: 'https://optimism.io/',
|
||||
explorer: 'https://optimistic.etherscan.io/',
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import {
|
||||
GOVERNANCE_ALPHA_V0_ADDRESSES,
|
||||
GOVERNANCE_ALPHA_V1_ADDRESSES,
|
||||
GOVERNANCE_BRAVO_ADDRESSES,
|
||||
TIMELOCK_ADDRESS,
|
||||
UNI_ADDRESS,
|
||||
} from './addresses'
|
||||
@@ -11,7 +12,8 @@ export const COMMON_CONTRACT_NAMES: Record<number, { [address: string]: string }
|
||||
[UNI_ADDRESS[SupportedChainId.MAINNET]]: 'UNI',
|
||||
[TIMELOCK_ADDRESS[SupportedChainId.MAINNET]]: 'Timelock',
|
||||
[GOVERNANCE_ALPHA_V0_ADDRESSES[SupportedChainId.MAINNET]]: 'Governance (V0)',
|
||||
[GOVERNANCE_ALPHA_V1_ADDRESSES[SupportedChainId.MAINNET]]: 'Governance',
|
||||
[GOVERNANCE_ALPHA_V1_ADDRESSES[SupportedChainId.MAINNET]]: 'Governance (V1)',
|
||||
[GOVERNANCE_BRAVO_ADDRESSES[SupportedChainId.MAINNET]]: 'Governance',
|
||||
'0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e': 'ENS Registry',
|
||||
'0x4976fb03C32e5B8cfe2b6cCB31c09Ba78EBaBa41': 'ENS Public Resolver',
|
||||
},
|
||||
|
||||
@@ -2,3 +2,4 @@ export const UNISWAP_GRANTS_START_BLOCK = 11473815
|
||||
export const BRAVO_START_BLOCK = 13059344
|
||||
export const ONE_BIP_START_BLOCK = 13551293
|
||||
export const POLYGON_START_BLOCK = 13786993
|
||||
export const MOONBEAN_START_BLOCK = 14732457
|
||||
|
||||
33
src/hooks/useAccountRiskCheck.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { useEffect } from 'react'
|
||||
import ReactGA from 'react-ga4'
|
||||
import { ApplicationModal, setOpenModal } from 'state/application/reducer'
|
||||
import { useAppDispatch } from 'state/hooks'
|
||||
|
||||
export default function useAccountRiskCheck(account: string | null | undefined) {
|
||||
const dispatch = useAppDispatch()
|
||||
|
||||
useEffect(() => {
|
||||
if (account) {
|
||||
const headers = new Headers({ 'Content-Type': 'application/json' })
|
||||
fetch('https://screening-worker.uniswap.workers.dev', {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify({ address: account }),
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then((data) => {
|
||||
if (data.block) {
|
||||
dispatch(setOpenModal(ApplicationModal.BLOCKED_ACCOUNT))
|
||||
ReactGA.event({
|
||||
category: 'Address Screening',
|
||||
action: 'blocked',
|
||||
label: account,
|
||||
})
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
dispatch(setOpenModal(null))
|
||||
})
|
||||
}
|
||||
}, [account, dispatch])
|
||||
}
|
||||
@@ -1,15 +1,10 @@
|
||||
/* eslint-disable react-hooks/rules-of-hooks */
|
||||
import { Web3Provider } from '@ethersproject/providers'
|
||||
import { default as useWidgetsWeb3React } from 'lib/hooks/useActiveWeb3React'
|
||||
import { useWeb3React } from 'web3-react-core'
|
||||
|
||||
import { NetworkContextName } from '../constants/misc'
|
||||
|
||||
export default function useActiveWeb3React() {
|
||||
if (process.env.REACT_APP_IS_WIDGET) {
|
||||
return useWidgetsWeb3React()
|
||||
}
|
||||
|
||||
const interfaceContext = useWeb3React<Web3Provider>()
|
||||
const interfaceNetworkContext = useWeb3React<Web3Provider>(
|
||||
process.env.REACT_APP_IS_WIDGET ? undefined : NetworkContextName
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useEffect } from 'react'
|
||||
import { useAppDispatch } from 'state/hooks'
|
||||
|
||||
import { updateUserExpertMode } from '../state/user/actions'
|
||||
import { updateUserExpertMode } from '../state/user/reducer'
|
||||
import useParsedQueryString from './useParsedQueryString'
|
||||
|
||||
export default function ApeModeQueryParamReader(): null {
|
||||
|
||||
@@ -6,8 +6,8 @@ import useSwapApproval, { useSwapApprovalOptimizedTrade } from 'lib/hooks/swap/u
|
||||
import { ApprovalState, useApproval } from 'lib/hooks/useApproval'
|
||||
import { useCallback } from 'react'
|
||||
|
||||
import { TransactionType } from '../state/transactions/actions'
|
||||
import { useHasPendingApproval, useTransactionAdder } from '../state/transactions/hooks'
|
||||
import { TransactionType } from '../state/transactions/types'
|
||||
export { ApprovalState } from 'lib/hooks/useApproval'
|
||||
|
||||
function useGetAndTrackApproval(getApproval: ReturnType<typeof useApproval>[1]) {
|
||||
|
||||
@@ -1,98 +0,0 @@
|
||||
import { TransactionResponse } from '@ethersproject/providers'
|
||||
import { initializeApp } from 'firebase/app'
|
||||
import { getDatabase, push, ref } from 'firebase/database'
|
||||
import useActiveWeb3React from 'hooks/useActiveWeb3React'
|
||||
import { useCallback } from 'react'
|
||||
import { TransactionInfo, TransactionType } from 'state/transactions/actions'
|
||||
|
||||
type PartialTransactionResponse = Pick<TransactionResponse, 'hash' | 'v' | 'r' | 's'>
|
||||
|
||||
const SUPPORTED_TRANSACTION_TYPES = [
|
||||
TransactionType.ADD_LIQUIDITY_V2_POOL,
|
||||
TransactionType.ADD_LIQUIDITY_V3_POOL,
|
||||
TransactionType.CREATE_V3_POOL,
|
||||
TransactionType.REMOVE_LIQUIDITY_V3,
|
||||
TransactionType.SWAP,
|
||||
]
|
||||
|
||||
const FIREBASE_API_KEY = process.env.REACT_APP_FIREBASE_KEY
|
||||
const firebaseEnabled = typeof FIREBASE_API_KEY !== 'undefined'
|
||||
if (firebaseEnabled) initializeFirebase()
|
||||
|
||||
function useMonitoringEventCallback() {
|
||||
const { chainId } = useActiveWeb3React()
|
||||
|
||||
return useCallback(
|
||||
async function log(
|
||||
type: string,
|
||||
{
|
||||
transactionResponse,
|
||||
walletAddress,
|
||||
}: { transactionResponse: PartialTransactionResponse; walletAddress: string | undefined }
|
||||
) {
|
||||
if (!firebaseEnabled) return
|
||||
|
||||
const db = getDatabase()
|
||||
|
||||
if (!walletAddress) {
|
||||
console.debug('Wallet address required to log monitoring events.')
|
||||
return
|
||||
}
|
||||
try {
|
||||
push(ref(db, 'trm'), {
|
||||
chainId,
|
||||
origin: window.location.origin,
|
||||
timestamp: Date.now(),
|
||||
tx: transactionResponse,
|
||||
type,
|
||||
walletAddress,
|
||||
})
|
||||
} catch (e) {
|
||||
console.debug('Error adding document: ', e)
|
||||
}
|
||||
},
|
||||
[chainId]
|
||||
)
|
||||
}
|
||||
|
||||
export function useTransactionMonitoringEventCallback() {
|
||||
const { account } = useActiveWeb3React()
|
||||
const log = useMonitoringEventCallback()
|
||||
|
||||
return useCallback(
|
||||
(info: TransactionInfo, transactionResponse: TransactionResponse) => {
|
||||
if (SUPPORTED_TRANSACTION_TYPES.includes(info.type)) {
|
||||
log(TransactionType[info.type], {
|
||||
transactionResponse: (({ hash, v, r, s }: PartialTransactionResponse) => ({ hash, v, r, s }))(
|
||||
transactionResponse
|
||||
),
|
||||
walletAddress: account ?? undefined,
|
||||
})
|
||||
}
|
||||
},
|
||||
[account, log]
|
||||
)
|
||||
}
|
||||
|
||||
export function useWalletConnectMonitoringEventCallback() {
|
||||
const log = useMonitoringEventCallback()
|
||||
|
||||
return useCallback(
|
||||
(walletAddress) => {
|
||||
log('WALLET_CONNECTED', { transactionResponse: { hash: '', r: '', s: '', v: -1 }, walletAddress })
|
||||
},
|
||||
[log]
|
||||
)
|
||||
}
|
||||
|
||||
function initializeFirebase() {
|
||||
initializeApp({
|
||||
apiKey: process.env.REACT_APP_FIREBASE_KEY,
|
||||
authDomain: 'interface-monitoring.firebaseapp.com',
|
||||
databaseURL: 'https://interface-monitoring-default-rtdb.firebaseio.com',
|
||||
projectId: 'interface-monitoring',
|
||||
storageBucket: 'interface-monitoring.appspot.com',
|
||||
messagingSenderId: '968187720053',
|
||||
appId: '1:968187720053:web:acedf72dce629d470be33c',
|
||||
})
|
||||
}
|
||||
@@ -4,8 +4,8 @@ import useActiveWeb3React from 'hooks/useActiveWeb3React'
|
||||
import { SwapCallbackState, useSwapCallback as useLibSwapCallBack } from 'lib/hooks/swap/useSwapCallback'
|
||||
import { ReactNode, useMemo } from 'react'
|
||||
|
||||
import { TransactionType } from '../state/transactions/actions'
|
||||
import { useTransactionAdder } from '../state/transactions/hooks'
|
||||
import { TransactionType } from '../state/transactions/types'
|
||||
import { currencyId } from '../utils/currencyId'
|
||||
import useENS from './useENS'
|
||||
import { SignatureData } from './useERC20Permit'
|
||||
|
||||
@@ -6,8 +6,8 @@ import tryParseCurrencyAmount from 'lib/utils/tryParseCurrencyAmount'
|
||||
import { useMemo } from 'react'
|
||||
|
||||
import { WRAPPED_NATIVE_CURRENCY } from '../constants/tokens'
|
||||
import { TransactionType } from '../state/transactions/actions'
|
||||
import { useTransactionAdder } from '../state/transactions/hooks'
|
||||
import { TransactionType } from '../state/transactions/types'
|
||||
import { useCurrencyBalance } from '../state/wallet/hooks'
|
||||
import { useWETHContract } from './useContract'
|
||||
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
{
|
||||
"extends": ["../../.eslintrc.json"],
|
||||
"plugins": ["better-styled-components"],
|
||||
"rules": {
|
||||
"better-styled-components/sort-declarations-alphabetically": "error",
|
||||
"no-restricted-imports": [
|
||||
"error",
|
||||
{
|
||||
"paths": [
|
||||
{
|
||||
"name": "react-feather",
|
||||
"message": "Please import from lib/icons to ensure performant usage."
|
||||
},
|
||||
{
|
||||
"name": "@uniswap/smart-order-router",
|
||||
"message": "Forbidden import; smart-order-router is lazy-loaded."
|
||||
}
|
||||
],
|
||||
"patterns": [
|
||||
{
|
||||
"group": ["styled-components"],
|
||||
"message": "Please import styled from lib/theme to get the correct typings."
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
// Use Inter mixin to set font-display: block.
|
||||
@use "@fontsource/inter/scss/mixins" as Inter;
|
||||
@include Inter.fontFace(
|
||||
$fontName: 'Inter',
|
||||
$weight: 400,
|
||||
$display: block,
|
||||
);
|
||||
@include Inter.fontFace(
|
||||
$fontName: 'Inter',
|
||||
$weight: 500,
|
||||
$display: block,
|
||||
);
|
||||
@include Inter.fontFace(
|
||||
$fontName: 'Inter',
|
||||
$weight: 600,
|
||||
$display: block,
|
||||
);
|
||||
@include Inter.fontFaceVariable(
|
||||
$display: block,
|
||||
);
|
||||
|
||||
@import "~@fontsource/ibm-plex-mono/400.css";
|
||||
|
Before Width: | Height: | Size: 2.5 KiB |
@@ -1,10 +0,0 @@
|
||||
<svg width="23" height="20" viewBox="0 0 23 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<linearGradient id="gradient" x1="0" y1="0" x2="1" y2="0" gradientTransform="rotate(95)">
|
||||
<stop id="stop1" offset="0" stop-color="#2274E2"/>
|
||||
<stop id="stop1" offset="0.5" stop-color="#2274E2"/>
|
||||
<stop id="stop2" offset="1" stop-color="#3FB672" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<path d="M16 16C10 16 9 10 5 10M16 16C16 17.6569 17.3431 19 19 19C20.6569 19 22 17.6569 22 16C22 14.3431 20.6569 13 19 13C17.3431 13 16 14.3431 16 16ZM5 10C9 10 10 4 16 4M5 10H1.5M16 4C16 5.65685 17.3431 7 19 7C20.6569 7 22 5.65685 22 4C22 2.34315 20.6569 1 19 1C17.3431 1 16 2.34315 16 4Z" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" stroke="url(#gradient)" />
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 780 B |
@@ -1,4 +0,0 @@
|
||||
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="10" cy="10" r="10" />
|
||||
<path d="M14 7L8.5 12.5L6 10" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 233 B |
@@ -1,4 +0,0 @@
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke-width="2" stroke-linecap="round" xmlns="http://www.w3.org/2000/svg">
|
||||
<polyline class="left" points="18 15 12 9"></polyline>
|
||||
<polyline class="right" points="12 9 6 15"></polyline>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 238 B |
@@ -1,35 +0,0 @@
|
||||
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<mask id="mask">
|
||||
<circle cx="12" cy="12" r="10" fill="black" stroke="black" stroke-width="2" />
|
||||
<rect width="12" height="12" fill="white" stroke-width="0" />
|
||||
<circle cx="2" cy="12" r="1" fill="white" stroke-width="0" />
|
||||
<circle cx="12" cy="2" r="1" fill="white" stroke-width="0" />
|
||||
</mask>
|
||||
<circle
|
||||
cx="12"
|
||||
cy="12"
|
||||
r="6"
|
||||
stroke="none"
|
||||
/>
|
||||
<circle
|
||||
id="track"
|
||||
cx="12"
|
||||
cy="12"
|
||||
r="10"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
fill="none"
|
||||
/>
|
||||
<circle
|
||||
cx="12"
|
||||
cy="12"
|
||||
r="10"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
fill="none"
|
||||
mask="url(#mask)"
|
||||
/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 931 B |
@@ -1,13 +0,0 @@
|
||||
<svg viewBox="0 0 14 15" fill="black" xmlns="http://www.w3.org/2000/svg">
|
||||
<g>
|
||||
<path d="M4.15217 1.55141C3.96412 1.52242 3.95619 1.51902 4.04468 1.5055C4.21427 1.47958 4.61472 1.51491 4.89067 1.58012C5.53489 1.73232 6.12109 2.12221 6.74683 2.81466L6.91307 2.99862L7.15088 2.96062C8.15274 2.8006 9.17194 2.92778 10.0244 3.31918C10.2589 3.42686 10.6287 3.64121 10.6749 3.69629C10.6896 3.71384 10.7166 3.82684 10.7349 3.94742C10.7982 4.36458 10.7665 4.68434 10.6382 4.92317C10.5683 5.05313 10.5644 5.09432 10.6114 5.20554C10.6489 5.2943 10.7534 5.35999 10.8569 5.35985C11.0687 5.35956 11.2968 5.0192 11.4024 4.54561L11.4444 4.3575L11.5275 4.45109C11.9835 4.96459 12.3417 5.66488 12.4032 6.16335L12.4192 6.29332L12.3426 6.17517C12.2107 5.97186 12.0781 5.83346 11.9084 5.72183C11.6024 5.52062 11.2789 5.45215 10.4222 5.40727C9.64839 5.36675 9.21045 5.30106 8.77621 5.16032C8.03738 4.9209 7.66493 4.60204 6.78729 3.4576C6.39748 2.94928 6.15654 2.66804 5.91687 2.44155C5.37228 1.92691 4.83716 1.65701 4.15217 1.55141Z"/>
|
||||
<path d="M10.8494 2.68637C10.8689 2.34575 10.9153 2.12108 11.0088 1.9159C11.0458 1.83469 11.0804 1.76822 11.0858 1.76822C11.0911 1.76822 11.075 1.82816 11.05 1.90142C10.9821 2.10054 10.9709 2.3729 11.0177 2.68978C11.0771 3.09184 11.1109 3.14985 11.5385 3.58416C11.739 3.78788 11.9723 4.0448 12.0568 4.15511L12.2106 4.35568L12.0568 4.21234C11.8688 4.03705 11.4364 3.6952 11.3409 3.64633C11.2768 3.61356 11.2673 3.61413 11.2278 3.65321C11.1914 3.68922 11.1837 3.74333 11.1787 3.99915C11.1708 4.39786 11.1161 4.65377 10.9842 4.90965C10.9128 5.04805 10.9015 5.01851 10.9661 4.8623C11.0143 4.74566 11.0192 4.69439 11.0189 4.30842C11.0181 3.53291 10.9255 3.34647 10.3823 3.02709C10.2447 2.94618 10.0179 2.8295 9.87839 2.76778C9.73887 2.70606 9.62805 2.6523 9.63208 2.64828C9.64746 2.63307 10.1772 2.78675 10.3905 2.86828C10.7077 2.98954 10.76 3.00526 10.7985 2.99063C10.8244 2.98082 10.8369 2.90608 10.8494 2.68637Z"/>
|
||||
<path d="M4.51745 4.01304C4.13569 3.49066 3.89948 2.68973 3.95062 2.091L3.96643 1.90572L4.05333 1.92148C4.21652 1.95106 4.49789 2.05515 4.62964 2.13469C4.9912 2.35293 5.14773 2.64027 5.30697 3.37811C5.35362 3.59423 5.41482 3.8388 5.44298 3.9216C5.48831 4.05487 5.65962 4.36617 5.7989 4.56834C5.89922 4.71395 5.83258 4.78295 5.61082 4.76305C5.27215 4.73267 4.8134 4.41799 4.51745 4.01304Z"/>
|
||||
<path d="M10.3863 7.90088C8.60224 7.18693 7.97389 6.56721 7.97389 5.52157C7.97389 5.36769 7.97922 5.24179 7.98571 5.24179C7.99221 5.24179 8.06124 5.29257 8.1391 5.35465C8.50088 5.64305 8.906 5.76623 10.0275 5.92885C10.6875 6.02455 11.0589 6.10185 11.4015 6.21477C12.4904 6.57371 13.1641 7.30212 13.3248 8.29426C13.3715 8.58255 13.3441 9.12317 13.2684 9.4081C13.2087 9.63315 13.0263 10.0388 12.9779 10.0544C12.9645 10.0587 12.9514 10.0076 12.9479 9.93809C12.9296 9.56554 12.7402 9.20285 12.4221 8.93116C12.0604 8.62227 11.5745 8.37633 10.3863 7.90088Z"/>
|
||||
<path d="M9.13385 8.19748C9.11149 8.06527 9.07272 7.89643 9.04769 7.82228L9.00217 7.68748L9.08672 7.7818C9.20374 7.91233 9.2962 8.07937 9.37457 8.30185C9.43438 8.47165 9.44111 8.52215 9.44066 8.79807C9.4402 9.06896 9.43273 9.12575 9.3775 9.27858C9.29042 9.51959 9.18233 9.69048 9.00097 9.87391C8.67507 10.2036 8.25607 10.3861 7.65143 10.4618C7.54633 10.4749 7.24 10.4971 6.97069 10.511C6.292 10.5461 5.84531 10.6186 5.44393 10.7587C5.38623 10.7788 5.3347 10.7911 5.32947 10.7859C5.31323 10.7698 5.58651 10.6079 5.81223 10.4998C6.1305 10.3474 6.44733 10.2643 7.15719 10.1468C7.50785 10.0887 7.86998 10.0183 7.96194 9.99029C8.83033 9.72566 9.27671 9.04276 9.13385 8.19748Z"/>
|
||||
<path d="M9.95169 9.64109C9.71465 9.13463 9.66022 8.64564 9.79009 8.18961C9.80399 8.14088 9.82632 8.101 9.83976 8.101C9.85319 8.101 9.90913 8.13105 9.96404 8.16777C10.0733 8.24086 10.2924 8.36395 10.876 8.68023C11.6043 9.0749 12.0196 9.3805 12.302 9.72965C12.5493 10.0354 12.7023 10.3837 12.776 10.8084C12.8177 11.0489 12.7932 11.6277 12.7311 11.8699C12.5353 12.6337 12.0802 13.2336 11.4311 13.5837C11.336 13.635 11.2506 13.6771 11.2414 13.6773C11.2321 13.6775 11.2668 13.5899 11.3184 13.4827C11.5367 13.029 11.5616 12.5877 11.3965 12.0965C11.2954 11.7957 11.0893 11.4287 10.6732 10.8084C10.1893 10.0873 10.0707 9.89539 9.95169 9.64109Z"/>
|
||||
<path d="M3.25046 12.3737C3.91252 11.8181 4.73629 11.4234 5.48666 11.3022C5.81005 11.25 6.34877 11.2707 6.64823 11.3469C7.12824 11.469 7.55763 11.7425 7.78094 12.0683C7.99918 12.3867 8.09281 12.6642 8.19029 13.2816C8.22875 13.5252 8.27057 13.7697 8.28323 13.8251C8.35644 14.1451 8.4989 14.4008 8.67544 14.5293C8.95583 14.7333 9.43865 14.7459 9.91362 14.5618C9.99423 14.5305 10.0642 14.5089 10.0691 14.5138C10.0864 14.5308 9.84719 14.6899 9.67847 14.7737C9.45143 14.8864 9.2709 14.93 9.03102 14.93C8.59601 14.93 8.23486 14.7101 7.9335 14.2616C7.87419 14.1733 7.7409 13.909 7.63729 13.6741C7.3191 12.9528 7.16199 12.7331 6.79255 12.4926C6.47104 12.2834 6.05641 12.2459 5.74449 12.3979C5.33475 12.5976 5.22043 13.118 5.51389 13.4478C5.63053 13.5789 5.84803 13.6919 6.02588 13.7139C6.35861 13.7551 6.64455 13.5035 6.64455 13.1696C6.64455 12.9528 6.56071 12.8291 6.34966 12.7344C6.0614 12.6051 5.75156 12.7562 5.75304 13.0254C5.75368 13.1402 5.80396 13.2122 5.91971 13.2643C5.99397 13.2977 5.99569 13.3003 5.93514 13.2878C5.67066 13.2333 5.6087 12.9164 5.82135 12.706C6.07667 12.4535 6.60461 12.5649 6.78591 12.9097C6.86208 13.0545 6.87092 13.3429 6.80451 13.517C6.6559 13.9068 6.22256 14.1117 5.78297 14.0002C5.48368 13.9242 5.36181 13.842 5.00097 13.4726C4.37395 12.8306 4.13053 12.7062 3.22657 12.566L3.05335 12.5391L3.25046 12.3737Z"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M0.308383 0.883984C2.40235 3.40996 3.84457 4.45213 4.00484 4.67231C4.13717 4.85412 4.08737 5.01757 3.86067 5.14567C3.7346 5.21689 3.47541 5.28905 3.34564 5.28905C3.19887 5.28905 3.14847 5.23278 3.14847 5.23278C3.06337 5.15255 3.01544 5.16658 2.5784 4.39555C1.97166 3.45981 1.46389 2.68357 1.45004 2.67057C1.41801 2.64052 1.41856 2.64153 2.51654 4.59413C2.69394 5.0011 2.55182 5.15049 2.55182 5.20845C2.55182 5.32636 2.51946 5.38834 2.37311 5.55059C2.12914 5.8211 2.02008 6.12505 1.94135 6.7541C1.8531 7.45926 1.60492 7.95737 0.917156 8.80989C0.514562 9.30893 0.448686 9.4004 0.3471 9.60153C0.219144 9.85482 0.183961 9.99669 0.169701 10.3165C0.154629 10.6547 0.183983 10.8732 0.287934 11.1965C0.378939 11.4796 0.473932 11.6665 0.716778 12.0403C0.926351 12.3629 1.04702 12.6027 1.04702 12.6965C1.04702 12.7711 1.06136 12.7712 1.38611 12.6983C2.16328 12.5239 2.79434 12.2171 3.14925 11.8411C3.36891 11.6084 3.42048 11.4799 3.42215 11.1611C3.42325 10.9525 3.41587 10.9088 3.35914 10.7888C3.2668 10.5935 3.09869 10.4311 2.72817 10.1794C2.2427 9.84953 2.03534 9.58398 1.97807 9.21878C1.93108 8.91913 1.98559 8.70771 2.25416 8.14825C2.53214 7.56916 2.60103 7.32239 2.64763 6.73869C2.67773 6.36158 2.71941 6.21286 2.82842 6.09348C2.94212 5.969 3.04447 5.92684 3.32584 5.88863C3.78457 5.82635 4.07667 5.70839 4.31677 5.48849C4.52505 5.29772 4.61221 5.11391 4.62558 4.8372L4.63574 4.62747L4.51934 4.49259C4.09783 4.00411 0.0261003 0.5 0.000160437 0.5C-0.00538105 0.5 0.133325 0.672804 0.308383 0.883984ZM1.28364 10.6992C1.37894 10.5314 1.3283 10.3158 1.16889 10.2104C1.01827 10.1109 0.78428 10.1578 0.78428 10.2875C0.78428 10.3271 0.806303 10.3559 0.855937 10.3813C0.939514 10.424 0.945581 10.4721 0.879823 10.5703C0.81323 10.6698 0.818604 10.7573 0.894991 10.8167C1.0181 10.9125 1.19237 10.8598 1.28364 10.6992Z"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M4.92523 5.99865C4.70988 6.06439 4.50054 6.29124 4.43574 6.5291C4.39621 6.67421 4.41864 6.92875 4.47785 7.00736C4.57351 7.13433 4.66602 7.16778 4.91651 7.16603C5.40693 7.16263 5.83327 6.95358 5.88284 6.69224C5.92347 6.47801 5.73622 6.18112 5.4783 6.05078C5.34521 5.98355 5.06217 5.95688 4.92523 5.99865ZM5.49853 6.44422C5.57416 6.33741 5.54107 6.22198 5.41245 6.14391C5.1675 5.99525 4.79708 6.11827 4.79708 6.34826C4.79708 6.46274 4.99025 6.58765 5.16731 6.58765C5.28516 6.58765 5.44644 6.5178 5.49853 6.44422Z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 7.8 KiB |
@@ -1,17 +0,0 @@
|
||||
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<mask id="mask">
|
||||
<circle cx="12" cy="12" r="10" stroke="white" stroke-width="2" />
|
||||
<rect width="12" height="12" fill="black" stroke-width="0" />
|
||||
<circle cx="2" cy="12" r="1" fill="white" stroke-width="0" />
|
||||
<circle cx="12" cy="2" r="1" fill="white" stroke-width="0" />
|
||||
</mask>
|
||||
<circle
|
||||
cx="12"
|
||||
cy="12"
|
||||
r="10"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
mask="url(#mask)"
|
||||
/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 592 B |
@@ -1,5 +0,0 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M2 7C2 5.89543 2.89543 5 4 5H20C21.1046 5 22 5.89543 22 7V18C22 19.1046 21.1046 20 20 20H4C2.89543 20 2 19.1046 2 18V7Z" stroke="currentColor" stroke-width="2"/>
|
||||
<path d="M4 19H20C21.1046 19 22 18.1046 22 17V14C22 12.8954 21.1046 12 20 12H16C15.4477 12 14.9935 12.4624 14.7645 12.965C14.4438 13.6688 13.789 14.5 12 14.5C10.29 14.5 9.48213 13.7406 9.1936 13.0589C8.96576 12.5206 8.49905 12 7.91447 12H4C2.89543 12 2 12.8954 2 14V17C2 18.1046 2.89543 19 4 19Z" fill="currentColor"/>
|
||||
<path d="M22 13V11C22 9.89543 21.1034 9 19.9989 9C14.0294 9 9.97062 9 4.00115 9C2.89658 9 2 9.89543 2 11V13" stroke="currentColor" stroke-width="2"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 743 B |
@@ -1,92 +0,0 @@
|
||||
import { AlertTriangle, Icon, LargeIcon } from 'lib/icons'
|
||||
import styled, { Color, css, keyframes, ThemedText } from 'lib/theme'
|
||||
import { ReactNode, useMemo } from 'react'
|
||||
|
||||
import Button from './Button'
|
||||
import Row from './Row'
|
||||
|
||||
const StyledButton = styled(Button)`
|
||||
border-radius: ${({ theme }) => theme.borderRadius * 0.75}em;
|
||||
flex-grow: 1;
|
||||
transition: background-color 0.25s ease-out, border-radius 0.25s ease-out, flex-grow 0.25s ease-out;
|
||||
|
||||
:disabled {
|
||||
margin: -1px;
|
||||
}
|
||||
`
|
||||
|
||||
const ActionRow = styled(Row)``
|
||||
|
||||
const grow = keyframes`
|
||||
from {
|
||||
opacity: 0;
|
||||
width: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
width: max-content;
|
||||
}
|
||||
`
|
||||
|
||||
const actionCss = css`
|
||||
border: 1px solid ${({ theme }) => theme.outline};
|
||||
padding: calc(0.25em - 1px);
|
||||
padding-left: calc(0.75em - 1px);
|
||||
|
||||
${ActionRow} {
|
||||
animation: ${grow} 0.25s ease-in;
|
||||
flex-grow: 1;
|
||||
justify-content: flex-start;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
${StyledButton} {
|
||||
border-radius: ${({ theme }) => theme.borderRadius}em;
|
||||
flex-grow: 0;
|
||||
padding: 1em;
|
||||
}
|
||||
`
|
||||
|
||||
export const Overlay = styled(Row)<{ hasAction: boolean }>`
|
||||
border-radius: ${({ theme }) => theme.borderRadius}em;
|
||||
flex-direction: row-reverse;
|
||||
min-height: 3.5em;
|
||||
transition: padding 0.25s ease-out;
|
||||
|
||||
${({ hasAction }) => hasAction && actionCss}
|
||||
`
|
||||
|
||||
export interface Action {
|
||||
message: ReactNode
|
||||
icon?: Icon
|
||||
onClick?: () => void
|
||||
children?: ReactNode
|
||||
}
|
||||
|
||||
export interface BaseProps {
|
||||
color?: Color
|
||||
action?: Action
|
||||
}
|
||||
|
||||
export type ActionButtonProps = BaseProps & Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, keyof BaseProps>
|
||||
|
||||
export default function ActionButton({ color = 'accent', disabled, action, onClick, children }: ActionButtonProps) {
|
||||
const textColor = useMemo(() => (color === 'accent' && !disabled ? 'onAccent' : 'currentColor'), [color, disabled])
|
||||
return (
|
||||
<Overlay hasAction={Boolean(action)} flex align="stretch">
|
||||
{(action ? action.onClick : true) && (
|
||||
<StyledButton color={color} disabled={disabled} onClick={action?.onClick || onClick}>
|
||||
<ThemedText.TransitionButton buttonSize={action ? 'medium' : 'large'} color={textColor}>
|
||||
{action?.children || children}
|
||||
</ThemedText.TransitionButton>
|
||||
</StyledButton>
|
||||
)}
|
||||
{action && (
|
||||
<ActionRow gap={0.5}>
|
||||
<LargeIcon color="currentColor" icon={action.icon || AlertTriangle} />
|
||||
<ThemedText.Subhead2>{action?.message}</ThemedText.Subhead2>
|
||||
</ActionRow>
|
||||
)}
|
||||
</Overlay>
|
||||
)
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import Row from 'lib/components/Row'
|
||||
import { Logo } from 'lib/icons'
|
||||
import styled, { brand, ThemedText } from 'lib/theme'
|
||||
import { memo } from 'react'
|
||||
|
||||
import ExternalLink from './ExternalLink'
|
||||
|
||||
const UniswapA = styled(ExternalLink)`
|
||||
color: ${({ theme }) => theme.secondary};
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
|
||||
${Logo} {
|
||||
fill: ${({ theme }) => theme.secondary};
|
||||
height: 1em;
|
||||
transition: transform 0.25s ease, fill 0.25s ease;
|
||||
width: 1em;
|
||||
will-change: transform;
|
||||
}
|
||||
|
||||
:hover ${Logo} {
|
||||
fill: ${brand};
|
||||
transform: rotate(-5deg);
|
||||
}
|
||||
`
|
||||
|
||||
export default memo(function BrandedFooter() {
|
||||
return (
|
||||
<Row justify="center">
|
||||
<UniswapA href={`https://uniswap.org/`}>
|
||||
<Row gap={0.25}>
|
||||
<Logo />
|
||||
<ThemedText.Caption>
|
||||
<Trans>Powered by the Uniswap protocol</Trans>
|
||||
</ThemedText.Caption>
|
||||
</Row>
|
||||
</UniswapA>
|
||||
</Row>
|
||||
)
|
||||
})
|
||||
@@ -1,75 +0,0 @@
|
||||
import { Icon } from 'lib/icons'
|
||||
import styled, { Color, css } from 'lib/theme'
|
||||
import { ComponentProps, forwardRef } from 'react'
|
||||
|
||||
export const BaseButton = styled.button`
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
border-radius: 0.5em;
|
||||
color: currentColor;
|
||||
cursor: pointer;
|
||||
font-size: inherit;
|
||||
font-weight: inherit;
|
||||
height: inherit;
|
||||
line-height: inherit;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
|
||||
:enabled {
|
||||
transition: filter 0.125s linear;
|
||||
}
|
||||
|
||||
:disabled {
|
||||
cursor: initial;
|
||||
filter: saturate(0) opacity(0.4);
|
||||
}
|
||||
`
|
||||
const transitionCss = css`
|
||||
transition: background-color 0.125s linear, border-color 0.125s linear, filter 0.125s linear;
|
||||
`
|
||||
|
||||
export default styled(BaseButton)<{ color?: Color; transition?: boolean }>`
|
||||
border: 1px solid transparent;
|
||||
color: ${({ color = 'interactive', theme }) => color === 'interactive' && theme.onInteractive};
|
||||
|
||||
:enabled {
|
||||
background-color: ${({ color = 'interactive', theme }) => theme[color]};
|
||||
${({ transition = true }) => transition && transitionCss};
|
||||
}
|
||||
|
||||
:enabled:hover {
|
||||
background-color: ${({ color = 'interactive', theme }) => theme.onHover(theme[color])};
|
||||
}
|
||||
|
||||
:disabled {
|
||||
border-color: ${({ theme }) => theme.outline};
|
||||
color: ${({ theme }) => theme.secondary};
|
||||
}
|
||||
`
|
||||
|
||||
const transparentButton = (defaultColor: Color) => styled(BaseButton)<{ color?: Color }>`
|
||||
color: ${({ color = defaultColor, theme }) => theme[color]};
|
||||
|
||||
:enabled:hover {
|
||||
color: ${({ color = defaultColor, theme }) => theme.onHover(theme[color])};
|
||||
}
|
||||
`
|
||||
|
||||
export const TextButton = transparentButton('accent')
|
||||
|
||||
const SecondaryButton = transparentButton('secondary')
|
||||
|
||||
interface IconButtonProps {
|
||||
icon: Icon
|
||||
iconProps?: ComponentProps<Icon>
|
||||
}
|
||||
|
||||
export const IconButton = forwardRef<HTMLButtonElement, IconButtonProps & ComponentProps<typeof BaseButton>>(
|
||||
function IconButton({ icon: Icon, iconProps, ...props }: IconButtonProps & ComponentProps<typeof BaseButton>, ref) {
|
||||
return (
|
||||
<SecondaryButton {...props} ref={ref}>
|
||||
<Icon {...iconProps} />
|
||||
</SecondaryButton>
|
||||
)
|
||||
}
|
||||
)
|
||||
@@ -1,28 +0,0 @@
|
||||
import styled, { Color, css, Theme } from 'lib/theme'
|
||||
|
||||
const Column = styled.div<{
|
||||
align?: string
|
||||
color?: Color
|
||||
justify?: string
|
||||
gap?: number
|
||||
padded?: true
|
||||
flex?: true
|
||||
grow?: true
|
||||
theme: Theme
|
||||
css?: ReturnType<typeof css>
|
||||
}>`
|
||||
align-items: ${({ align }) => align ?? 'center'};
|
||||
color: ${({ color, theme }) => color && theme[color]};
|
||||
display: ${({ flex }) => (flex ? 'flex' : 'grid')};
|
||||
flex-direction: column;
|
||||
flex-grow: ${({ grow }) => grow && 1};
|
||||
gap: ${({ gap }) => gap && `${gap}em`};
|
||||
grid-auto-flow: row;
|
||||
grid-template-columns: 1fr;
|
||||
justify-content: ${({ justify }) => justify ?? 'space-between'};
|
||||
padding: ${({ padded }) => padded && '0.75em'};
|
||||
|
||||
${({ css }) => css}
|
||||
`
|
||||
|
||||
export default Column
|
||||
@@ -1,122 +0,0 @@
|
||||
import 'wicg-inert'
|
||||
|
||||
import { X } from 'lib/icons'
|
||||
import styled, { Color, Layer, ThemeProvider } from 'lib/theme'
|
||||
import { delayUnmountForAnimation } from 'lib/utils/animations'
|
||||
import { createContext, ReactElement, ReactNode, useContext, useEffect, useRef, useState } from 'react'
|
||||
import { createPortal } from 'react-dom'
|
||||
|
||||
import { IconButton } from './Button'
|
||||
import Column from './Column'
|
||||
import { default as BaseHeader } from './Header'
|
||||
import Rule from './Rule'
|
||||
|
||||
// Include inert from wicg-inert
|
||||
declare global {
|
||||
interface HTMLElement {
|
||||
inert?: boolean
|
||||
}
|
||||
}
|
||||
|
||||
const Context = createContext({
|
||||
element: null as HTMLElement | null,
|
||||
active: false,
|
||||
setActive: (active: boolean) => undefined as void,
|
||||
})
|
||||
|
||||
interface ProviderProps {
|
||||
value: HTMLElement | null
|
||||
children: ReactNode
|
||||
}
|
||||
|
||||
export function Provider({ value, children }: ProviderProps) {
|
||||
// If a Dialog is active, mark the main content inert
|
||||
const ref = useRef<HTMLDivElement>(null)
|
||||
const [active, setActive] = useState(false)
|
||||
const context = { element: value, active, setActive }
|
||||
useEffect(() => {
|
||||
if (ref.current) {
|
||||
ref.current.inert = active
|
||||
}
|
||||
}, [active])
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
style={{ isolation: 'isolate' }} // creates a new stacking context, preventing the dialog from intercepting non-dialog clicks
|
||||
>
|
||||
<Context.Provider value={context}>{children}</Context.Provider>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const OnCloseContext = createContext<() => void>(() => void 0)
|
||||
|
||||
interface HeaderProps {
|
||||
title?: ReactElement
|
||||
ruled?: boolean
|
||||
children?: ReactNode
|
||||
}
|
||||
|
||||
export function Header({ title, children, ruled }: HeaderProps) {
|
||||
return (
|
||||
<>
|
||||
<Column>
|
||||
<BaseHeader title={title}>
|
||||
{children}
|
||||
<IconButton color="primary" onClick={useContext(OnCloseContext)} icon={X} />
|
||||
</BaseHeader>
|
||||
{ruled && <Rule padded />}
|
||||
</Column>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export const Modal = styled.div<{ color: Color }>`
|
||||
background-color: ${({ color, theme }) => theme[color]};
|
||||
border-radius: ${({ theme }) => theme.borderRadius * 0.75}em;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
left: 0;
|
||||
overflow: hidden;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
z-index: ${Layer.DIALOG};
|
||||
`
|
||||
|
||||
interface DialogProps {
|
||||
color: Color
|
||||
children: ReactNode
|
||||
onClose?: () => void
|
||||
}
|
||||
|
||||
export default function Dialog({ color, children, onClose = () => void 0 }: DialogProps) {
|
||||
const context = useContext(Context)
|
||||
useEffect(() => {
|
||||
context.setActive(true)
|
||||
return () => context.setActive(false)
|
||||
}, [context])
|
||||
|
||||
const modal = useRef<HTMLDivElement>(null)
|
||||
useEffect(() => delayUnmountForAnimation(modal), [])
|
||||
|
||||
useEffect(() => {
|
||||
const close = (e: KeyboardEvent) => e.key === 'Escape' && onClose?.()
|
||||
document.addEventListener('keydown', close, true)
|
||||
return () => document.removeEventListener('keydown', close, true)
|
||||
}, [onClose])
|
||||
return (
|
||||
context.element &&
|
||||
createPortal(
|
||||
<ThemeProvider>
|
||||
<OnCloseContext.Provider value={onClose}>
|
||||
<Modal color={color} ref={modal}>
|
||||
{children}
|
||||
</Modal>
|
||||
</OnCloseContext.Provider>
|
||||
</ThemeProvider>,
|
||||
context.element
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import React, { ErrorInfo } from 'react'
|
||||
|
||||
import Dialog from '../Dialog'
|
||||
import ErrorDialog from './ErrorDialog'
|
||||
|
||||
export type ErrorHandler = (error: Error, info: ErrorInfo) => void
|
||||
|
||||
interface ErrorBoundaryProps {
|
||||
onError?: ErrorHandler
|
||||
}
|
||||
|
||||
type ErrorBoundaryState = {
|
||||
error: Error | null
|
||||
}
|
||||
|
||||
export default class ErrorBoundary extends React.Component<ErrorBoundaryProps, ErrorBoundaryState> {
|
||||
constructor(props: ErrorBoundaryProps) {
|
||||
super(props)
|
||||
this.state = { error: null }
|
||||
}
|
||||
|
||||
static getDerivedStateFromError(error: Error) {
|
||||
return { error }
|
||||
}
|
||||
|
||||
componentDidCatch(error: Error, errorInfo: ErrorInfo) {
|
||||
this.props.onError?.(error, errorInfo)
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.state.error) {
|
||||
return (
|
||||
<Dialog color="dialog">
|
||||
<ErrorDialog
|
||||
error={this.state.error}
|
||||
header={<Trans>Something went wrong.</Trans>}
|
||||
action={<Trans>Reload the page</Trans>}
|
||||
onClick={() => window.location.reload()}
|
||||
/>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
return this.props.children
|
||||
}
|
||||
}
|
||||
@@ -1,80 +0,0 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import ActionButton from 'lib/components/ActionButton'
|
||||
import Column from 'lib/components/Column'
|
||||
import Expando from 'lib/components/Expando'
|
||||
import { AlertTriangle, Icon, LargeIcon } from 'lib/icons'
|
||||
import styled, { Color, ThemedText } from 'lib/theme'
|
||||
import { ReactNode, useCallback, useState } from 'react'
|
||||
|
||||
const HeaderIcon = styled(LargeIcon)`
|
||||
flex-grow: 1;
|
||||
transition: height 0.25s, width 0.25s;
|
||||
|
||||
svg {
|
||||
transition: height 0.25s, width 0.25s;
|
||||
}
|
||||
`
|
||||
|
||||
interface StatusHeaderProps {
|
||||
icon: Icon
|
||||
iconColor?: Color
|
||||
iconSize?: number
|
||||
children: ReactNode
|
||||
}
|
||||
|
||||
export function StatusHeader({ icon: Icon, iconColor, iconSize = 4, children }: StatusHeaderProps) {
|
||||
return (
|
||||
<>
|
||||
<Column flex style={{ flexGrow: 1 }}>
|
||||
<HeaderIcon icon={Icon} color={iconColor} size={iconSize} />
|
||||
<Column gap={0.75} flex style={{ textAlign: 'center' }}>
|
||||
{children}
|
||||
</Column>
|
||||
</Column>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const ErrorHeader = styled(Column)<{ open: boolean }>`
|
||||
transition: gap 0.25s;
|
||||
|
||||
div:last-child {
|
||||
max-height: ${({ open }) => (open ? 0 : 60 / 14)}em; // 3 * line-height
|
||||
overflow-y: hidden;
|
||||
transition: max-height 0.25s;
|
||||
}
|
||||
`
|
||||
|
||||
interface ErrorDialogProps {
|
||||
header?: ReactNode
|
||||
error: Error
|
||||
action: ReactNode
|
||||
onClick: () => void
|
||||
}
|
||||
|
||||
export default function ErrorDialog({ header, error, action, onClick }: ErrorDialogProps) {
|
||||
const [open, setOpen] = useState(false)
|
||||
const onExpand = useCallback(() => setOpen((open) => !open), [])
|
||||
|
||||
return (
|
||||
<Column flex padded gap={0.75} align="stretch" style={{ height: '100%' }}>
|
||||
<StatusHeader icon={AlertTriangle} iconColor="error" iconSize={open ? 3 : 4}>
|
||||
<ErrorHeader gap={open ? 0 : 0.75} open={open}>
|
||||
<ThemedText.Subhead1>
|
||||
<Trans>Something went wrong.</Trans>
|
||||
</ThemedText.Subhead1>
|
||||
<ThemedText.Body2>{header}</ThemedText.Body2>
|
||||
</ErrorHeader>
|
||||
</StatusHeader>
|
||||
<Column gap={open ? 0 : 0.75} style={{ transition: 'gap 0.25s' }}>
|
||||
<Expando title={<Trans>Error details</Trans>} open={open} onExpand={onExpand} height={7.5}>
|
||||
<ThemedText.Code userSelect>
|
||||
{error.name}
|
||||
{error.message ? `: ${error.message}` : ''}
|
||||
</ThemedText.Code>
|
||||
</Expando>
|
||||
<ActionButton onClick={onClick}>{action}</ActionButton>
|
||||
</Column>
|
||||
</Column>
|
||||
)
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
import { SupportedChainId } from 'constants/chains'
|
||||
import useActiveWeb3React from 'lib/hooks/useActiveWeb3React'
|
||||
import { Link } from 'lib/icons'
|
||||
import styled, { Color } from 'lib/theme'
|
||||
import { ReactNode, useMemo } from 'react'
|
||||
import { ExplorerDataType, getExplorerLink } from 'utils/getExplorerLink'
|
||||
|
||||
import ExternalLink from './ExternalLink'
|
||||
import Row from './Row'
|
||||
|
||||
const StyledExternalLink = styled(ExternalLink)<{ color: Color }>`
|
||||
color: ${({ theme, color }) => theme[color]};
|
||||
text-decoration: none;
|
||||
`
|
||||
|
||||
interface EtherscanLinkProps {
|
||||
type: ExplorerDataType
|
||||
data?: string
|
||||
color?: Color
|
||||
children: ReactNode
|
||||
}
|
||||
|
||||
export default function EtherscanLink({ data, type, color = 'currentColor', children }: EtherscanLinkProps) {
|
||||
const { chainId } = useActiveWeb3React()
|
||||
const url = useMemo(
|
||||
() => data && getExplorerLink(chainId || SupportedChainId.MAINNET, data, type),
|
||||
[chainId, data, type]
|
||||
)
|
||||
|
||||
return (
|
||||
<StyledExternalLink href={url} color={color} target="_blank">
|
||||
<Row gap={0.25}>
|
||||
{children}
|
||||
{url && <Link />}
|
||||
</Row>
|
||||
</StyledExternalLink>
|
||||
)
|
||||
}
|
||||
@@ -1,66 +0,0 @@
|
||||
import { IconButton } from 'lib/components/Button'
|
||||
import Column from 'lib/components/Column'
|
||||
import Row from 'lib/components/Row'
|
||||
import Rule from 'lib/components/Rule'
|
||||
import useScrollbar from 'lib/hooks/useScrollbar'
|
||||
import { Expando as ExpandoIcon } from 'lib/icons'
|
||||
import styled from 'lib/theme'
|
||||
import { PropsWithChildren, ReactNode, useState } from 'react'
|
||||
|
||||
const HeaderColumn = styled(Column)`
|
||||
transition: gap 0.25s;
|
||||
`
|
||||
|
||||
const ExpandoColumn = styled(Column)<{ height: number; open: boolean }>`
|
||||
height: ${({ height, open }) => (open ? height : 0)}em;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
transition: height 0.25s, padding 0.25s;
|
||||
|
||||
:after {
|
||||
background: linear-gradient(transparent, ${({ theme }) => theme.dialog});
|
||||
bottom: 0;
|
||||
content: '';
|
||||
height: 0.75em;
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
width: calc(100% - 1em);
|
||||
}
|
||||
`
|
||||
|
||||
const InnerColumn = styled(Column)<{ height: number }>`
|
||||
height: ${({ height }) => height}em;
|
||||
padding: 0.5em 0;
|
||||
`
|
||||
|
||||
interface ExpandoProps {
|
||||
title: ReactNode
|
||||
open: boolean
|
||||
onExpand: () => void
|
||||
// The absolute height of the expanded container, in em.
|
||||
height: number
|
||||
}
|
||||
|
||||
/** A scrollable Expando with an absolute height. */
|
||||
export default function Expando({ title, open, onExpand, height, children }: PropsWithChildren<ExpandoProps>) {
|
||||
const [scrollingEl, setScrollingEl] = useState<HTMLDivElement | null>(null)
|
||||
const scrollbar = useScrollbar(scrollingEl)
|
||||
return (
|
||||
<Column>
|
||||
<HeaderColumn gap={open ? 0.5 : 0.75}>
|
||||
<Rule />
|
||||
<Row>
|
||||
{title}
|
||||
<IconButton color="secondary" onClick={onExpand} icon={ExpandoIcon} iconProps={{ open }} />
|
||||
</Row>
|
||||
<Rule />
|
||||
</HeaderColumn>
|
||||
<ExpandoColumn open={open} height={height}>
|
||||
<InnerColumn flex align="stretch" height={height} ref={setScrollingEl} css={scrollbar}>
|
||||
{children}
|
||||
</InnerColumn>
|
||||
</ExpandoColumn>
|
||||
</Column>
|
||||
)
|
||||
return null
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
import { HTMLProps } from 'react'
|
||||
|
||||
/**
|
||||
* Outbound link
|
||||
*/
|
||||
export default function ExternalLink({
|
||||
target = '_blank',
|
||||
href,
|
||||
rel = 'noopener noreferrer',
|
||||
...rest
|
||||
}: Omit<HTMLProps<HTMLAnchorElement>, 'as' | 'ref' | 'onClick'> & { href?: string }) {
|
||||
return (
|
||||
<a target={target} rel={rel} href={href} {...rest}>
|
||||
{rest.children}
|
||||
</a>
|
||||
)
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
import { largeIconCss } from 'lib/icons'
|
||||
import styled, { ThemedText } from 'lib/theme'
|
||||
import { ReactElement, ReactNode } from 'react'
|
||||
|
||||
import Row from './Row'
|
||||
|
||||
const HeaderRow = styled(Row)`
|
||||
height: 1.75em;
|
||||
margin: 0 0.75em 0.75em;
|
||||
padding-top: 0.5em;
|
||||
${largeIconCss}
|
||||
`
|
||||
|
||||
export interface HeaderProps {
|
||||
title?: ReactElement
|
||||
children: ReactNode
|
||||
}
|
||||
|
||||
export default function Header({ title, children }: HeaderProps) {
|
||||
return (
|
||||
<HeaderRow iconSize={1.2}>
|
||||
<Row gap={0.5}>{title && <ThemedText.Subhead1>{title}</ThemedText.Subhead1>}</Row>
|
||||
<Row gap={1}>{children}</Row>
|
||||
</HeaderRow>
|
||||
)
|
||||
}
|
||||
@@ -1,168 +0,0 @@
|
||||
import { loadingOpacity } from 'lib/css/loading'
|
||||
import styled, { css } from 'lib/theme'
|
||||
import { transparentize } from 'polished'
|
||||
import { ChangeEvent, forwardRef, HTMLProps, useCallback } from 'react'
|
||||
|
||||
const Input = styled.input`
|
||||
-webkit-appearance: textfield;
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
color: currentColor;
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
font-weight: inherit;
|
||||
line-height: inherit;
|
||||
margin: 0;
|
||||
outline: none;
|
||||
overflow: hidden;
|
||||
padding: 0;
|
||||
text-align: left;
|
||||
text-overflow: ellipsis;
|
||||
width: 100%;
|
||||
|
||||
::-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.secondary};
|
||||
}
|
||||
|
||||
:enabled {
|
||||
transition: color 0.125s linear;
|
||||
}
|
||||
|
||||
:disabled {
|
||||
// Overrides WebKit's override of input:disabled color.
|
||||
-webkit-text-fill-color: ${({ theme }) => transparentize(1 - loadingOpacity, theme.primary)};
|
||||
color: ${({ theme }) => transparentize(1 - loadingOpacity, theme.primary)};
|
||||
}
|
||||
`
|
||||
|
||||
export default Input
|
||||
|
||||
interface StringInputProps extends Omit<HTMLProps<HTMLInputElement>, 'onChange' | 'as' | 'value'> {
|
||||
value: string
|
||||
onChange: (input: string) => void
|
||||
}
|
||||
|
||||
export const StringInput = forwardRef<HTMLInputElement, StringInputProps>(function StringInput(
|
||||
{ value, onChange, ...props }: StringInputProps,
|
||||
ref
|
||||
) {
|
||||
return (
|
||||
<Input
|
||||
value={value}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
// universal input options
|
||||
inputMode="text"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
// text-specific options
|
||||
type="text"
|
||||
placeholder={props.placeholder || '-'}
|
||||
minLength={1}
|
||||
spellCheck="false"
|
||||
ref={ref as any}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
})
|
||||
|
||||
interface NumericInputProps extends Omit<HTMLProps<HTMLInputElement>, 'onChange' | 'as' | 'value'> {
|
||||
value: string
|
||||
onChange: (input: string) => void
|
||||
}
|
||||
|
||||
interface EnforcedNumericInputProps extends NumericInputProps {
|
||||
// Validates nextUserInput; returns stringified value, or null if invalid
|
||||
enforcer: (nextUserInput: string) => string | null
|
||||
}
|
||||
|
||||
const NumericInput = forwardRef<HTMLInputElement, EnforcedNumericInputProps>(function NumericInput(
|
||||
{ value, onChange, enforcer, pattern, ...props }: EnforcedNumericInputProps,
|
||||
ref
|
||||
) {
|
||||
const validateChange = useCallback(
|
||||
(event: ChangeEvent<HTMLInputElement>) => {
|
||||
const nextInput = enforcer(event.target.value.replace(/,/g, '.'))?.replace(/^0+$/, '0')
|
||||
if (nextInput !== undefined) {
|
||||
onChange(nextInput)
|
||||
}
|
||||
},
|
||||
[enforcer, onChange]
|
||||
)
|
||||
|
||||
return (
|
||||
<Input
|
||||
value={value}
|
||||
onChange={validateChange}
|
||||
// universal input options
|
||||
inputMode="decimal"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
// text-specific options
|
||||
type="text"
|
||||
pattern={pattern}
|
||||
placeholder={props.placeholder || '0'}
|
||||
minLength={1}
|
||||
maxLength={79}
|
||||
spellCheck="false"
|
||||
ref={ref as any}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
})
|
||||
|
||||
const integerRegexp = /^\d*$/
|
||||
const integerEnforcer = (nextUserInput: string) => {
|
||||
if (nextUserInput === '' || integerRegexp.test(nextUserInput)) {
|
||||
const nextInput = parseInt(nextUserInput)
|
||||
return isNaN(nextInput) ? '' : nextInput.toString()
|
||||
}
|
||||
return null
|
||||
}
|
||||
export const IntegerInput = forwardRef(function IntegerInput(props: NumericInputProps, ref) {
|
||||
return <NumericInput pattern="^[0-9]*$" enforcer={integerEnforcer} ref={ref as any} {...props} />
|
||||
})
|
||||
|
||||
const decimalRegexp = /^\d*(?:[.])?\d*$/
|
||||
const decimalEnforcer = (nextUserInput: string) => {
|
||||
if (nextUserInput === '') {
|
||||
return ''
|
||||
} else if (nextUserInput === '.') {
|
||||
return '0.'
|
||||
} else if (decimalRegexp.test(nextUserInput)) {
|
||||
return nextUserInput
|
||||
}
|
||||
return null
|
||||
}
|
||||
export const DecimalInput = forwardRef(function DecimalInput(props: NumericInputProps, ref) {
|
||||
return <NumericInput pattern="^[0-9]*[.,]?[0-9]*$" enforcer={decimalEnforcer} ref={ref as any} {...props} />
|
||||
})
|
||||
|
||||
export const inputCss = css`
|
||||
background-color: ${({ theme }) => theme.container};
|
||||
border: 1px solid ${({ theme }) => theme.container};
|
||||
border-radius: ${({ theme }) => theme.borderRadius}em;
|
||||
cursor: text;
|
||||
padding: calc(0.5em - 1px);
|
||||
|
||||
:hover:not(:focus-within) {
|
||||
background-color: ${({ theme }) => theme.onHover(theme.container)};
|
||||
border-color: ${({ theme }) => theme.onHover(theme.container)};
|
||||
}
|
||||
|
||||
:focus-within {
|
||||
border-color: ${({ theme }) => theme.active};
|
||||
}
|
||||
`
|
||||
@@ -1,150 +0,0 @@
|
||||
import { Options, Placement } from '@popperjs/core'
|
||||
import styled, { Layer } from 'lib/theme'
|
||||
import maxSize from 'popper-max-size-modifier'
|
||||
import React, { createContext, useContext, useMemo, useRef, useState } from 'react'
|
||||
import { createPortal } from 'react-dom'
|
||||
import { usePopper } from 'react-popper'
|
||||
|
||||
const BoundaryContext = createContext<HTMLDivElement | null>(null)
|
||||
|
||||
export const BoundaryProvider = BoundaryContext.Provider
|
||||
|
||||
const PopoverContainer = styled.div<{ show: boolean }>`
|
||||
background-color: ${({ theme }) => theme.dialog};
|
||||
border: 1px solid ${({ theme }) => theme.outline};
|
||||
border-radius: 0.5em;
|
||||
opacity: ${(props) => (props.show ? 1 : 0)};
|
||||
padding: 10px 12px;
|
||||
transition: visibility 0.25s linear, opacity 0.25s linear;
|
||||
visibility: ${(props) => (props.show ? 'visible' : 'hidden')};
|
||||
z-index: ${Layer.TOOLTIP};
|
||||
`
|
||||
|
||||
const Reference = styled.div`
|
||||
align-self: flex-start;
|
||||
display: inline-block;
|
||||
height: 1em;
|
||||
`
|
||||
|
||||
const Arrow = styled.div`
|
||||
height: 8px;
|
||||
width: 8px;
|
||||
z-index: ${Layer.TOOLTIP};
|
||||
|
||||
::before {
|
||||
background: ${({ theme }) => theme.dialog};
|
||||
border: 1px solid ${({ theme }) => theme.outline};
|
||||
content: '';
|
||||
height: 8px;
|
||||
position: absolute;
|
||||
transform: rotate(45deg);
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
&.arrow-top {
|
||||
bottom: -4px;
|
||||
::before {
|
||||
border-radius: 1px;
|
||||
border-left: none;
|
||||
border-top: none;
|
||||
}
|
||||
}
|
||||
|
||||
&.arrow-bottom {
|
||||
top: -5px; // includes -1px from border
|
||||
::before {
|
||||
border-bottom: none;
|
||||
border-right: none;
|
||||
border-radius: 1px;
|
||||
}
|
||||
}
|
||||
|
||||
&.arrow-left {
|
||||
right: -4px;
|
||||
::before {
|
||||
border-bottom: none;
|
||||
border-left: none;
|
||||
border-radius: 1px;
|
||||
}
|
||||
}
|
||||
|
||||
&.arrow-right {
|
||||
left: -5px; // includes -1px from border
|
||||
::before {
|
||||
border-radius: 1px;
|
||||
border-right: none;
|
||||
border-top: none;
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export interface PopoverProps {
|
||||
content: React.ReactNode
|
||||
show: boolean
|
||||
children: React.ReactNode
|
||||
placement: Placement
|
||||
offset?: number
|
||||
contained?: true
|
||||
}
|
||||
|
||||
export default function Popover({ content, show, children, placement, offset, contained }: PopoverProps) {
|
||||
const boundary = useContext(BoundaryContext)
|
||||
const reference = useRef<HTMLDivElement>(null)
|
||||
|
||||
// Use callback refs to be notified when instantiated
|
||||
const [popover, setPopover] = useState<HTMLDivElement | null>(null)
|
||||
const [arrow, setArrow] = useState<HTMLDivElement | null>(null)
|
||||
|
||||
const options = useMemo((): Options => {
|
||||
const modifiers: Options['modifiers'] = [
|
||||
{ name: 'offset', options: { offset: [4, offset || 4] } },
|
||||
{ name: 'arrow', options: { element: arrow, padding: 4 } },
|
||||
]
|
||||
if (contained) {
|
||||
modifiers.push(
|
||||
{ name: 'preventOverflow', options: { boundary, padding: 8 } },
|
||||
{ name: 'flip', options: { boundary, padding: 8 } },
|
||||
{ ...maxSize, options: { boundary, padding: 8 } },
|
||||
{
|
||||
name: 'applyMaxSize',
|
||||
enabled: true,
|
||||
phase: 'beforeWrite',
|
||||
requires: ['maxSize'],
|
||||
fn({ state }) {
|
||||
const { width } = state.modifiersData.maxSize
|
||||
state.styles.popper = {
|
||||
...state.styles.popper,
|
||||
maxWidth: `${width}px`,
|
||||
}
|
||||
},
|
||||
}
|
||||
)
|
||||
}
|
||||
return {
|
||||
placement,
|
||||
strategy: 'absolute',
|
||||
modifiers,
|
||||
}
|
||||
}, [offset, arrow, contained, placement, boundary])
|
||||
|
||||
const { styles, attributes } = usePopper(reference.current, popover, options)
|
||||
|
||||
return (
|
||||
<>
|
||||
<Reference ref={reference}>{children}</Reference>
|
||||
{boundary &&
|
||||
createPortal(
|
||||
<PopoverContainer show={show} ref={setPopover} style={styles.popper} {...attributes.popper}>
|
||||
{content}
|
||||
<Arrow
|
||||
className={`arrow-${attributes.popper?.['data-popper-placement'] ?? ''}`}
|
||||
ref={setArrow}
|
||||
style={styles.arrow}
|
||||
{...attributes.arrow}
|
||||
/>
|
||||
</PopoverContainer>,
|
||||
boundary
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -1,93 +0,0 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import { Currency } from '@uniswap/sdk-core'
|
||||
import { AlertTriangle, ArrowRight, CheckCircle, Spinner, Trash2 } from 'lib/icons'
|
||||
import styled, { ThemedText } from 'lib/theme'
|
||||
import { useMemo, useState } from 'react'
|
||||
|
||||
import Button from './Button'
|
||||
import Column from './Column'
|
||||
import { Header } from './Dialog'
|
||||
import Row from './Row'
|
||||
import TokenImg from './TokenImg'
|
||||
|
||||
interface ITokenAmount {
|
||||
value: number
|
||||
token: Currency
|
||||
}
|
||||
|
||||
export enum TransactionStatus {
|
||||
SUCCESS = 0,
|
||||
ERROR,
|
||||
PENDING,
|
||||
}
|
||||
|
||||
interface ITransaction {
|
||||
input: ITokenAmount
|
||||
output: ITokenAmount
|
||||
status: TransactionStatus
|
||||
}
|
||||
|
||||
const TransactionRow = styled(Row)`
|
||||
padding: 0.5em 1em;
|
||||
|
||||
:first-of-type {
|
||||
padding-top: 1em;
|
||||
}
|
||||
`
|
||||
|
||||
function TokenAmount({ value: { value, token } }: { value: ITokenAmount }) {
|
||||
return (
|
||||
<Row gap={0.375}>
|
||||
<TokenImg token={token} />
|
||||
<ThemedText.Body2>
|
||||
{value.toLocaleString('en')} {token.symbol}
|
||||
</ThemedText.Body2>
|
||||
</Row>
|
||||
)
|
||||
}
|
||||
|
||||
function Transaction({ tx }: { tx: ITransaction }) {
|
||||
const statusIcon = useMemo(() => {
|
||||
switch (tx.status) {
|
||||
case TransactionStatus.SUCCESS:
|
||||
return <CheckCircle color="success" />
|
||||
case TransactionStatus.ERROR:
|
||||
return <AlertTriangle color="error" />
|
||||
case TransactionStatus.PENDING:
|
||||
return <Spinner />
|
||||
}
|
||||
}, [tx.status])
|
||||
return (
|
||||
<TransactionRow grow>
|
||||
<Row gap={0.75}>
|
||||
<Row flex gap={0.5}>
|
||||
<TokenAmount value={tx.input} />
|
||||
<Row flex justify="flex-end" gap={0.5} grow>
|
||||
<ArrowRight />
|
||||
<TokenAmount value={tx.output} />
|
||||
</Row>
|
||||
</Row>
|
||||
{statusIcon}
|
||||
</Row>
|
||||
</TransactionRow>
|
||||
)
|
||||
}
|
||||
|
||||
export default function RecentTransactionsDialog() {
|
||||
const [txs, setTxs] = useState([])
|
||||
|
||||
return (
|
||||
<>
|
||||
<Header title={<Trans>Recent transactions</Trans>} ruled>
|
||||
<Button>
|
||||
<Trash2 onClick={() => setTxs([])} />
|
||||
</Button>
|
||||
</Header>
|
||||
<Column>
|
||||
{txs.map((tx, key) => (
|
||||
<Transaction tx={tx} key={key} />
|
||||
))}
|
||||
</Column>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
import styled, { Color, Theme } from 'lib/theme'
|
||||
import { Children, ReactNode } from 'react'
|
||||
|
||||
const Row = styled.div<{
|
||||
color?: Color
|
||||
align?: string
|
||||
justify?: string
|
||||
pad?: number
|
||||
gap?: number
|
||||
flex?: true
|
||||
grow?: true | 'first' | 'last'
|
||||
children?: ReactNode
|
||||
theme: Theme
|
||||
}>`
|
||||
align-items: ${({ align }) => align ?? 'center'};
|
||||
color: ${({ color, theme }) => color && theme[color]};
|
||||
display: ${({ flex }) => (flex ? 'flex' : 'grid')};
|
||||
flex-flow: wrap;
|
||||
flex-grow: ${({ grow }) => grow && 1};
|
||||
gap: ${({ gap }) => gap && `${gap}em`};
|
||||
grid-auto-flow: column;
|
||||
grid-template-columns: ${({ grow, children }) => {
|
||||
if (grow === 'first') return '1fr'
|
||||
if (grow === 'last') return `repeat(${Children.count(children) - 1}, auto) 1fr`
|
||||
if (grow) return `repeat(${Children.count(children)}, 1fr)`
|
||||
return undefined
|
||||
}};
|
||||
justify-content: ${({ justify }) => justify ?? 'space-between'};
|
||||
padding: ${({ pad }) => pad && `0 ${pad}em`};
|
||||
`
|
||||
|
||||
export default Row
|
||||
@@ -1,15 +0,0 @@
|
||||
import styled from 'lib/theme'
|
||||
|
||||
const Rule = styled.hr<{ padded?: true; scrollingEdge?: 'top' | 'bottom' }>`
|
||||
border: none;
|
||||
border-bottom: 1px solid ${({ theme }) => theme.outline};
|
||||
margin: 0 ${({ padded }) => (padded ? '0.75em' : 0)};
|
||||
margin-bottom: ${({ scrollingEdge }) => (scrollingEdge === 'bottom' ? -1 : 0)}px;
|
||||
margin-top: ${({ scrollingEdge }) => (scrollingEdge !== 'bottom' ? -1 : 0)}px;
|
||||
|
||||
// Integrators will commonly modify hr width - this overrides any modifications within the widget.
|
||||
max-width: auto;
|
||||
width: auto;
|
||||
`
|
||||
|
||||
export default Rule
|
||||
@@ -1,134 +0,0 @@
|
||||
import { useLingui } from '@lingui/react'
|
||||
import { Currency, CurrencyAmount } from '@uniswap/sdk-core'
|
||||
import { loadingTransitionCss } from 'lib/css/loading'
|
||||
import {
|
||||
useIsSwapFieldIndependent,
|
||||
useSwapAmount,
|
||||
useSwapCurrency,
|
||||
useSwapCurrencyAmount,
|
||||
useSwapInfo,
|
||||
} from 'lib/hooks/swap'
|
||||
import { usePrefetchCurrencyColor } from 'lib/hooks/useCurrencyColor'
|
||||
import { Field } from 'lib/state/swap'
|
||||
import styled, { ThemedText } from 'lib/theme'
|
||||
import { useMemo } from 'react'
|
||||
import { TradeState } from 'state/routing/types'
|
||||
import { formatCurrencyAmount } from 'utils/formatCurrencyAmount'
|
||||
import { maxAmountSpend } from 'utils/maxAmountSpend'
|
||||
|
||||
import Column from '../Column'
|
||||
import Row from '../Row'
|
||||
import TokenImg from '../TokenImg'
|
||||
import TokenInput from './TokenInput'
|
||||
|
||||
export const USDC = styled(Row)`
|
||||
${loadingTransitionCss};
|
||||
`
|
||||
|
||||
export const Balance = styled(ThemedText.Body2)<{ focused: boolean }>`
|
||||
opacity: ${({ focused }) => (focused ? 1 : 0)};
|
||||
transition: opacity 0.25s ${({ focused }) => (focused ? 'ease-in' : 'ease-out')};
|
||||
`
|
||||
|
||||
const InputColumn = styled(Column)<{ approved?: boolean }>`
|
||||
margin: 0.75em;
|
||||
position: relative;
|
||||
|
||||
${TokenImg} {
|
||||
filter: ${({ approved }) => (approved ? undefined : 'saturate(0) opacity(0.4)')};
|
||||
transition: filter 0.25s;
|
||||
}
|
||||
`
|
||||
|
||||
export interface InputProps {
|
||||
disabled: boolean
|
||||
focused: boolean
|
||||
}
|
||||
|
||||
interface UseFormattedFieldAmountArguments {
|
||||
disabled: boolean
|
||||
currencyAmount?: CurrencyAmount<Currency>
|
||||
fieldAmount?: string
|
||||
}
|
||||
|
||||
export function useFormattedFieldAmount({ disabled, currencyAmount, fieldAmount }: UseFormattedFieldAmountArguments) {
|
||||
return useMemo(() => {
|
||||
if (disabled) {
|
||||
return ''
|
||||
}
|
||||
if (fieldAmount !== undefined) {
|
||||
return fieldAmount
|
||||
}
|
||||
if (currencyAmount) {
|
||||
return currencyAmount.toSignificant(6)
|
||||
}
|
||||
return ''
|
||||
}, [disabled, currencyAmount, fieldAmount])
|
||||
}
|
||||
|
||||
export default function Input({ disabled, focused }: InputProps) {
|
||||
const { i18n } = useLingui()
|
||||
const {
|
||||
[Field.INPUT]: { balance, amount: tradeCurrencyAmount, usdc },
|
||||
trade: { state: tradeState },
|
||||
} = useSwapInfo()
|
||||
|
||||
const [inputAmount, updateInputAmount] = useSwapAmount(Field.INPUT)
|
||||
const [inputCurrency, updateInputCurrency] = useSwapCurrency(Field.INPUT)
|
||||
const inputCurrencyAmount = useSwapCurrencyAmount(Field.INPUT)
|
||||
|
||||
// extract eagerly in case of reversal
|
||||
usePrefetchCurrencyColor(inputCurrency)
|
||||
|
||||
const isRouteLoading = disabled || tradeState === TradeState.SYNCING || tradeState === TradeState.LOADING
|
||||
const isDependentField = !useIsSwapFieldIndependent(Field.INPUT)
|
||||
const isLoading = isRouteLoading && isDependentField
|
||||
|
||||
//TODO(ianlapham): mimic logic from app swap page
|
||||
const mockApproved = true
|
||||
|
||||
// account for gas needed if using max on native token
|
||||
const max = useMemo(() => {
|
||||
const maxAmount = maxAmountSpend(balance)
|
||||
return maxAmount?.greaterThan(0) ? maxAmount.toExact() : undefined
|
||||
}, [balance])
|
||||
|
||||
const balanceColor = useMemo(() => {
|
||||
const insufficientBalance =
|
||||
balance &&
|
||||
(inputCurrencyAmount ? inputCurrencyAmount.greaterThan(balance) : tradeCurrencyAmount?.greaterThan(balance))
|
||||
return insufficientBalance ? 'error' : undefined
|
||||
}, [balance, inputCurrencyAmount, tradeCurrencyAmount])
|
||||
|
||||
const amount = useFormattedFieldAmount({
|
||||
disabled,
|
||||
currencyAmount: tradeCurrencyAmount,
|
||||
fieldAmount: inputAmount,
|
||||
})
|
||||
|
||||
return (
|
||||
<InputColumn gap={0.5} approved={mockApproved}>
|
||||
<TokenInput
|
||||
currency={inputCurrency}
|
||||
amount={amount}
|
||||
max={max}
|
||||
disabled={disabled}
|
||||
onChangeInput={updateInputAmount}
|
||||
onChangeCurrency={updateInputCurrency}
|
||||
loading={isLoading}
|
||||
>
|
||||
<ThemedText.Body2 color="secondary" userSelect>
|
||||
<Row>
|
||||
<USDC isLoading={isRouteLoading}>{usdc ? `$${formatCurrencyAmount(usdc, 6, 'en', 2)}` : '-'}</USDC>
|
||||
{balance && (
|
||||
<Balance color={balanceColor} focused={focused}>
|
||||
Balance: <span>{formatCurrencyAmount(balance, 4, i18n.locale)}</span>
|
||||
</Balance>
|
||||
)}
|
||||
</Row>
|
||||
</ThemedText.Body2>
|
||||
</TokenInput>
|
||||
<Row />
|
||||
</InputColumn>
|
||||
)
|
||||
}
|
||||
@@ -1,101 +0,0 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import { useLingui } from '@lingui/react'
|
||||
import { atom } from 'jotai'
|
||||
import { useAtomValue } from 'jotai/utils'
|
||||
import BrandedFooter from 'lib/components/BrandedFooter'
|
||||
import { useIsSwapFieldIndependent, useSwapAmount, useSwapCurrency, useSwapInfo } from 'lib/hooks/swap'
|
||||
import useCurrencyColor from 'lib/hooks/useCurrencyColor'
|
||||
import { Field } from 'lib/state/swap'
|
||||
import styled, { DynamicThemeProvider, ThemedText } from 'lib/theme'
|
||||
import { PropsWithChildren } from 'react'
|
||||
import { TradeState } from 'state/routing/types'
|
||||
import { formatCurrencyAmount } from 'utils/formatCurrencyAmount'
|
||||
|
||||
import Column from '../Column'
|
||||
import Row from '../Row'
|
||||
import { Balance, InputProps, USDC, useFormattedFieldAmount } from './Input'
|
||||
import TokenInput from './TokenInput'
|
||||
|
||||
export const colorAtom = atom<string | undefined>(undefined)
|
||||
|
||||
const OutputColumn = styled(Column)<{ hasColor: boolean | null }>`
|
||||
background-color: ${({ theme }) => theme.module};
|
||||
border-radius: ${({ theme }) => theme.borderRadius - 0.25}em;
|
||||
padding: 0.75em;
|
||||
padding-bottom: 0.5em;
|
||||
position: relative;
|
||||
|
||||
// Set transitions to reduce color flashes when switching color/token.
|
||||
// When color loads, transition the background so that it transitions from the empty or last state, but not _to_ the empty state.
|
||||
transition: ${({ hasColor }) => (hasColor ? 'background-color 0.25s ease-out' : undefined)};
|
||||
> {
|
||||
// When color is loading, delay the color/stroke so that it seems to transition from the last state.
|
||||
transition: ${({ hasColor }) => (hasColor === null ? 'color 0.25s ease-in, stroke 0.25s ease-in' : undefined)};
|
||||
}
|
||||
`
|
||||
|
||||
export default function Output({ disabled, focused, children }: PropsWithChildren<InputProps>) {
|
||||
const { i18n } = useLingui()
|
||||
|
||||
const {
|
||||
[Field.OUTPUT]: { balance, amount: outputCurrencyAmount, usdc: outputUSDC },
|
||||
trade: { state: tradeState },
|
||||
impact,
|
||||
} = useSwapInfo()
|
||||
|
||||
const [swapOutputAmount, updateSwapOutputAmount] = useSwapAmount(Field.OUTPUT)
|
||||
const [swapOutputCurrency, updateSwapOutputCurrency] = useSwapCurrency(Field.OUTPUT)
|
||||
|
||||
const isRouteLoading = disabled || tradeState === TradeState.SYNCING || tradeState === TradeState.LOADING
|
||||
const isDependentField = !useIsSwapFieldIndependent(Field.OUTPUT)
|
||||
const isLoading = isRouteLoading && isDependentField
|
||||
|
||||
const overrideColor = useAtomValue(colorAtom)
|
||||
const dynamicColor = useCurrencyColor(swapOutputCurrency)
|
||||
const color = overrideColor || dynamicColor
|
||||
|
||||
// different state true/null/false allow smoother color transition
|
||||
const hasColor = swapOutputCurrency ? Boolean(color) || null : false
|
||||
|
||||
const amount = useFormattedFieldAmount({
|
||||
disabled,
|
||||
currencyAmount: outputCurrencyAmount,
|
||||
fieldAmount: swapOutputAmount,
|
||||
})
|
||||
|
||||
return (
|
||||
<DynamicThemeProvider color={color}>
|
||||
<OutputColumn hasColor={hasColor} gap={0.5}>
|
||||
<Row>
|
||||
<ThemedText.Subhead1 color="secondary">
|
||||
<Trans>For</Trans>
|
||||
</ThemedText.Subhead1>
|
||||
</Row>
|
||||
<TokenInput
|
||||
currency={swapOutputCurrency}
|
||||
amount={amount}
|
||||
disabled={disabled}
|
||||
onChangeInput={updateSwapOutputAmount}
|
||||
onChangeCurrency={updateSwapOutputCurrency}
|
||||
loading={isLoading}
|
||||
>
|
||||
<ThemedText.Body2 color="secondary" userSelect>
|
||||
<Row>
|
||||
<USDC gap={0.5} isLoading={isRouteLoading}>
|
||||
{outputUSDC ? `$${formatCurrencyAmount(outputUSDC, 6, 'en', 2)}` : '-'}{' '}
|
||||
{impact && <ThemedText.Body2 color={impact.warning}>({impact.toString()})</ThemedText.Body2>}
|
||||
</USDC>
|
||||
{balance && (
|
||||
<Balance focused={focused}>
|
||||
Balance: <span>{formatCurrencyAmount(balance, 4, i18n.locale)}</span>
|
||||
</Balance>
|
||||
)}
|
||||
</Row>
|
||||
</ThemedText.Body2>
|
||||
</TokenInput>
|
||||
{children}
|
||||
<BrandedFooter />
|
||||
</OutputColumn>
|
||||
</DynamicThemeProvider>
|
||||
)
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
import { useLingui } from '@lingui/react'
|
||||
import { Trade } from '@uniswap/router-sdk'
|
||||
import { Currency, CurrencyAmount, TradeType } from '@uniswap/sdk-core'
|
||||
import Row from 'lib/components/Row'
|
||||
import { ThemedText } from 'lib/theme'
|
||||
import formatLocaleNumber from 'lib/utils/formatLocaleNumber'
|
||||
import { useCallback, useMemo, useState } from 'react'
|
||||
import { formatCurrencyAmount, formatPrice } from 'utils/formatCurrencyAmount'
|
||||
|
||||
import { TextButton } from '../Button'
|
||||
|
||||
interface PriceProps {
|
||||
trade: Trade<Currency, Currency, TradeType>
|
||||
outputUSDC?: CurrencyAmount<Currency>
|
||||
}
|
||||
|
||||
/** Displays the price of a trade. If outputUSDC is included, also displays the unit price. */
|
||||
export default function Price({ trade, outputUSDC }: PriceProps) {
|
||||
const { i18n } = useLingui()
|
||||
const { inputAmount, outputAmount, executionPrice } = trade
|
||||
|
||||
const [base, setBase] = useState<'input' | 'output'>('input')
|
||||
const onClick = useCallback(() => setBase((base) => (base === 'input' ? 'output' : 'input')), [])
|
||||
|
||||
// Compute the usdc price from the output price, so that it aligns with the displayed price.
|
||||
const { price, usdcPrice } = useMemo(() => {
|
||||
switch (base) {
|
||||
case 'input':
|
||||
return {
|
||||
price: executionPrice,
|
||||
usdcPrice: outputUSDC?.multiply(inputAmount.decimalScale).divide(inputAmount),
|
||||
}
|
||||
case 'output':
|
||||
return {
|
||||
price: executionPrice.invert(),
|
||||
usdcPrice: outputUSDC?.multiply(outputAmount.decimalScale).divide(outputAmount),
|
||||
}
|
||||
}
|
||||
}, [base, executionPrice, inputAmount, outputAmount, outputUSDC])
|
||||
|
||||
return (
|
||||
<TextButton color="primary" onClick={onClick}>
|
||||
<ThemedText.Caption>
|
||||
<Row gap={0.25}>
|
||||
{formatLocaleNumber({ number: 1, sigFigs: 1, locale: i18n.locale })} {price.baseCurrency.symbol} ={' '}
|
||||
{formatPrice(price, 6, i18n.locale)} {price.quoteCurrency.symbol}
|
||||
{usdcPrice && (
|
||||
<ThemedText.Caption color="secondary">(${formatCurrencyAmount(usdcPrice, 6, 'en', 2)})</ThemedText.Caption>
|
||||
)}
|
||||
</Row>
|
||||
</ThemedText.Caption>
|
||||
</TextButton>
|
||||
)
|
||||
}
|
||||
@@ -1,68 +0,0 @@
|
||||
import { useSwitchSwapCurrencies } from 'lib/hooks/swap'
|
||||
import { ArrowDown as ArrowDownIcon, ArrowUp as ArrowUpIcon } from 'lib/icons'
|
||||
import styled, { Layer } from 'lib/theme'
|
||||
import { useCallback, useState } from 'react'
|
||||
|
||||
import Button from '../Button'
|
||||
import Row from '../Row'
|
||||
|
||||
const ReverseRow = styled(Row)`
|
||||
left: 50%;
|
||||
position: absolute;
|
||||
transform: translate(-50%, -50%);
|
||||
z-index: ${Layer.OVERLAY};
|
||||
`
|
||||
|
||||
const ArrowUp = styled(ArrowUpIcon)`
|
||||
left: calc(50% - 0.37em);
|
||||
position: absolute;
|
||||
top: calc(50% - 0.82em);
|
||||
`
|
||||
|
||||
const ArrowDown = styled(ArrowDownIcon)`
|
||||
bottom: calc(50% - 0.82em);
|
||||
position: absolute;
|
||||
right: calc(50% - 0.37em);
|
||||
`
|
||||
|
||||
const Overlay = styled.div`
|
||||
background-color: ${({ theme }) => theme.container};
|
||||
border-radius: ${({ theme }) => theme.borderRadius}em;
|
||||
padding: 0.25em;
|
||||
`
|
||||
|
||||
const StyledReverseButton = styled(Button)<{ turns: number }>`
|
||||
border-radius: ${({ theme }) => theme.borderRadius * 0.75}em;
|
||||
color: ${({ theme }) => theme.primary};
|
||||
height: 2.5em;
|
||||
position: relative;
|
||||
width: 2.5em;
|
||||
|
||||
div {
|
||||
transform: rotate(${({ turns }) => turns / 2}turn);
|
||||
transition: transform 0.25s ease-in-out;
|
||||
will-change: transform;
|
||||
}
|
||||
`
|
||||
|
||||
export default function ReverseButton({ disabled }: { disabled?: boolean }) {
|
||||
const [turns, setTurns] = useState(0)
|
||||
const switchCurrencies = useSwitchSwapCurrencies()
|
||||
const onClick = useCallback(() => {
|
||||
switchCurrencies()
|
||||
setTurns((turns) => ++turns)
|
||||
}, [switchCurrencies])
|
||||
|
||||
return (
|
||||
<ReverseRow justify="center">
|
||||
<Overlay>
|
||||
<StyledReverseButton disabled={disabled} onClick={onClick} turns={turns}>
|
||||
<div>
|
||||
<ArrowUp strokeWidth={3} />
|
||||
<ArrowDown strokeWidth={3} />
|
||||
</div>
|
||||
</StyledReverseButton>
|
||||
</Overlay>
|
||||
</ReverseRow>
|
||||
)
|
||||
}
|
||||
@@ -1,139 +0,0 @@
|
||||
import { Plural, Trans } from '@lingui/macro'
|
||||
import { Currency, TradeType } from '@uniswap/sdk-core'
|
||||
import { FeeAmount } from '@uniswap/v3-sdk'
|
||||
import { ReactComponent as DotLine } from 'assets/svg/dot_line.svg'
|
||||
import Column from 'lib/components/Column'
|
||||
import Row from 'lib/components/Row'
|
||||
import Rule from 'lib/components/Rule'
|
||||
import TokenImg from 'lib/components/TokenImg'
|
||||
import { AutoRouter } from 'lib/icons'
|
||||
import styled, { Layer, ThemedText } from 'lib/theme'
|
||||
import { useMemo } from 'react'
|
||||
import { InterfaceTrade } from 'state/routing/types'
|
||||
|
||||
import { getTokenPath, RoutingDiagramEntry } from './utils'
|
||||
|
||||
const StyledAutoRouterLabel = styled(ThemedText.ButtonSmall)`
|
||||
@supports (-webkit-background-clip: text) and (-webkit-text-fill-color: transparent) {
|
||||
background-image: linear-gradient(90deg, #2172e5 0%, #54e521 163.16%);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
}
|
||||
`
|
||||
|
||||
function Header({ routes }: { routes: RoutingDiagramEntry[] }) {
|
||||
return (
|
||||
<Row justify="space-between" gap={1}>
|
||||
<ThemedText.Subhead2>
|
||||
<Row gap={0.25}>
|
||||
<AutoRouter />
|
||||
<StyledAutoRouterLabel color="primary" lineHeight={'16px'}>
|
||||
<Trans>Auto Router</Trans>
|
||||
</StyledAutoRouterLabel>
|
||||
</Row>
|
||||
</ThemedText.Subhead2>
|
||||
<ThemedText.Body2>
|
||||
<Plural value={routes.length} _1="Best route via 1 hop" other="Best route via # hops" />
|
||||
</ThemedText.Body2>
|
||||
</Row>
|
||||
)
|
||||
}
|
||||
|
||||
const Dots = styled(DotLine)`
|
||||
color: ${({ theme }) => theme.outline};
|
||||
position: absolute;
|
||||
z-index: ${Layer.UNDERLAYER};
|
||||
`
|
||||
|
||||
const RouteRow = styled(Row)`
|
||||
flex-wrap: nowrap;
|
||||
`
|
||||
|
||||
const RouteNode = styled(Row)`
|
||||
background-color: ${({ theme }) => theme.interactive};
|
||||
border-radius: ${({ theme }) => `${(theme.borderRadius ?? 1) * 0.5}em`};
|
||||
margin-left: 1.625em;
|
||||
padding: 0.25em 0.375em;
|
||||
width: max-content;
|
||||
`
|
||||
|
||||
const RouteBadge = styled.div`
|
||||
background-color: ${({ theme }) => theme.module};
|
||||
border-radius: ${({ theme }) => `${(theme.borderRadius ?? 1) * 0.25}em`};
|
||||
padding: 0.125em;
|
||||
`
|
||||
|
||||
function RouteDetail({ route }: { route: RoutingDiagramEntry }) {
|
||||
const protocol = route.protocol.toUpperCase()
|
||||
return (
|
||||
<RouteNode>
|
||||
<Row gap={0.375}>
|
||||
<ThemedText.Caption>{route.percent.toSignificant(2)}%</ThemedText.Caption>
|
||||
<RouteBadge>
|
||||
<ThemedText.Badge color="secondary">{protocol}</ThemedText.Badge>
|
||||
</RouteBadge>
|
||||
</Row>
|
||||
</RouteNode>
|
||||
)
|
||||
}
|
||||
|
||||
const RoutePool = styled(RouteNode)`
|
||||
margin: 0 0.75em;
|
||||
`
|
||||
|
||||
function Pool({
|
||||
originCurrency,
|
||||
targetCurrency,
|
||||
feeAmount,
|
||||
}: {
|
||||
originCurrency: Currency
|
||||
targetCurrency: Currency
|
||||
feeAmount: FeeAmount
|
||||
}) {
|
||||
return (
|
||||
<RoutePool>
|
||||
<ThemedText.Caption>
|
||||
<Row gap={0.25}>
|
||||
<TokenImg token={originCurrency} />
|
||||
<TokenImg token={targetCurrency} style={{ marginLeft: '-0.65em' }} />
|
||||
{feeAmount / 10_000}%
|
||||
</Row>
|
||||
</ThemedText.Caption>
|
||||
</RoutePool>
|
||||
)
|
||||
}
|
||||
|
||||
function Route({ route }: { route: RoutingDiagramEntry }) {
|
||||
const [originCurrency] = route.path[0]
|
||||
const [, targetCurrency] = route.path[route.path.length - 1]
|
||||
|
||||
return (
|
||||
<Row align="center" style={{ gridTemplateColumns: '1em 1fr 1em' }}>
|
||||
<TokenImg token={originCurrency} />
|
||||
<RouteRow flex style={{ position: 'relative' }}>
|
||||
<Dots />
|
||||
<RouteDetail route={route} />
|
||||
<RouteRow justify="space-evenly" flex>
|
||||
{route.path.map(([originCurrency, targetCurrency, feeAmount], index) => (
|
||||
<Pool key={index} originCurrency={originCurrency} targetCurrency={targetCurrency} feeAmount={feeAmount} />
|
||||
))}
|
||||
</RouteRow>
|
||||
</RouteRow>
|
||||
<TokenImg token={targetCurrency} />
|
||||
</Row>
|
||||
)
|
||||
}
|
||||
|
||||
export default function RoutingDiagram({ trade }: { trade: InterfaceTrade<Currency, Currency, TradeType> }) {
|
||||
const routes: RoutingDiagramEntry[] = useMemo(() => getTokenPath(trade), [trade])
|
||||
|
||||
return (
|
||||
<Column gap={0.75}>
|
||||
<Header routes={routes} />
|
||||
<Rule />
|
||||
{routes.map((route, index) => (
|
||||
<Route key={index} route={route} />
|
||||
))}
|
||||
</Column>
|
||||
)
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
import { Protocol } from '@uniswap/router-sdk'
|
||||
import { Currency, Percent, TradeType } from '@uniswap/sdk-core'
|
||||
import { Pair } from '@uniswap/v2-sdk'
|
||||
import { FeeAmount } from '@uniswap/v3-sdk'
|
||||
import { InterfaceTrade } from 'state/routing/types'
|
||||
|
||||
export interface RoutingDiagramEntry {
|
||||
percent: Percent
|
||||
path: [Currency, Currency, FeeAmount][]
|
||||
protocol: Protocol
|
||||
}
|
||||
|
||||
const V2_DEFAULT_FEE_TIER = 3000
|
||||
|
||||
/**
|
||||
* Loops through all routes on a trade and returns an array of diagram entries.
|
||||
*/
|
||||
export function getTokenPath(trade: InterfaceTrade<Currency, Currency, TradeType>): RoutingDiagramEntry[] {
|
||||
return trade.swaps.map(({ route: { path: tokenPath, pools, protocol }, inputAmount, outputAmount }) => {
|
||||
const portion =
|
||||
trade.tradeType === TradeType.EXACT_INPUT
|
||||
? inputAmount.divide(trade.inputAmount)
|
||||
: outputAmount.divide(trade.outputAmount)
|
||||
const percent = new Percent(portion.numerator, portion.denominator)
|
||||
const path: RoutingDiagramEntry['path'] = []
|
||||
for (let i = 0; i < pools.length; i++) {
|
||||
const nextPool = pools[i]
|
||||
const tokenIn = tokenPath[i]
|
||||
const tokenOut = tokenPath[i + 1]
|
||||
const entry: RoutingDiagramEntry['path'][0] = [
|
||||
tokenIn,
|
||||
tokenOut,
|
||||
nextPool instanceof Pair ? V2_DEFAULT_FEE_TIER : nextPool.fee,
|
||||
]
|
||||
path.push(entry)
|
||||
}
|
||||
return {
|
||||
percent,
|
||||
path,
|
||||
protocol,
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
import { Modal } from '../Dialog'
|
||||
import { SettingsDialog } from './Settings'
|
||||
|
||||
export default (
|
||||
<Modal color="module">
|
||||
<SettingsDialog />
|
||||
</Modal>
|
||||
)
|
||||
@@ -1,151 +0,0 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import { useAtom } from 'jotai'
|
||||
import Popover from 'lib/components/Popover'
|
||||
import { useTooltip } from 'lib/components/Tooltip'
|
||||
import { getSlippageWarning, toPercent } from 'lib/hooks/useSlippage'
|
||||
import { AlertTriangle, Check, Icon, LargeIcon, XOctagon } from 'lib/icons'
|
||||
import { autoSlippageAtom, maxSlippageAtom } from 'lib/state/settings'
|
||||
import styled, { ThemedText } from 'lib/theme'
|
||||
import { forwardRef, memo, ReactNode, useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
|
||||
import { BaseButton, TextButton } from '../../Button'
|
||||
import Column from '../../Column'
|
||||
import { DecimalInput, inputCss } from '../../Input'
|
||||
import Row from '../../Row'
|
||||
import { Label, optionCss } from './components'
|
||||
|
||||
const tooltip = (
|
||||
<Trans>Your transaction will revert if the price changes unfavorably by more than this percentage.</Trans>
|
||||
)
|
||||
const highSlippage = <Trans>High slippage increases the risk of price movement</Trans>
|
||||
const invalidSlippage = <Trans>Please enter a valid slippage %</Trans>
|
||||
|
||||
const placeholder = '0.10'
|
||||
|
||||
const Button = styled(TextButton)<{ selected: boolean }>`
|
||||
${({ selected }) => optionCss(selected)}
|
||||
`
|
||||
|
||||
const Custom = styled(BaseButton)<{ selected: boolean }>`
|
||||
${({ selected }) => optionCss(selected)}
|
||||
${inputCss}
|
||||
padding: calc(0.75em - 3px) 0.625em;
|
||||
`
|
||||
|
||||
interface OptionProps {
|
||||
wrapper: typeof Button | typeof Custom
|
||||
selected: boolean
|
||||
onSelect: () => void
|
||||
icon?: ReactNode
|
||||
tabIndex?: number
|
||||
children: ReactNode
|
||||
}
|
||||
|
||||
const Option = forwardRef<HTMLButtonElement, OptionProps>(function Option(
|
||||
{ wrapper: Wrapper, children, selected, onSelect, icon, tabIndex }: OptionProps,
|
||||
ref
|
||||
) {
|
||||
return (
|
||||
<Wrapper selected={selected} onClick={onSelect} ref={ref} tabIndex={tabIndex}>
|
||||
<Row gap={0.5}>
|
||||
{children}
|
||||
{icon ? icon : <LargeIcon icon={selected ? Check : undefined} size={1.25} />}
|
||||
</Row>
|
||||
</Wrapper>
|
||||
)
|
||||
})
|
||||
|
||||
const Warning = memo(function Warning({ state, showTooltip }: { state?: 'warning' | 'error'; showTooltip: boolean }) {
|
||||
let icon: Icon | undefined
|
||||
let content: ReactNode
|
||||
let show = showTooltip
|
||||
switch (state) {
|
||||
case 'error':
|
||||
icon = XOctagon
|
||||
content = invalidSlippage
|
||||
show = true
|
||||
break
|
||||
case 'warning':
|
||||
icon = AlertTriangle
|
||||
content = highSlippage
|
||||
break
|
||||
}
|
||||
return (
|
||||
<Popover
|
||||
key={state}
|
||||
content={<ThemedText.Caption>{content}</ThemedText.Caption>}
|
||||
show={show}
|
||||
placement="top"
|
||||
offset={16}
|
||||
contained
|
||||
>
|
||||
<LargeIcon icon={icon} color={state} size={1.25} />
|
||||
</Popover>
|
||||
)
|
||||
})
|
||||
|
||||
export default function MaxSlippageSelect() {
|
||||
const [autoSlippage, setAutoSlippage] = useAtom(autoSlippageAtom)
|
||||
const [maxSlippage, setMaxSlippage] = useAtom(maxSlippageAtom)
|
||||
const maxSlippageInput = useMemo(() => maxSlippage?.toString() || '', [maxSlippage])
|
||||
|
||||
const option = useRef<HTMLButtonElement>(null)
|
||||
const showTooltip = useTooltip(option.current)
|
||||
|
||||
const input = useRef<HTMLInputElement>(null)
|
||||
const focus = useCallback(() => input.current?.focus(), [input])
|
||||
|
||||
const [warning, setWarning] = useState<'warning' | 'error' | undefined>(getSlippageWarning(toPercent(maxSlippage)))
|
||||
useEffect(() => {
|
||||
setWarning(getSlippageWarning(toPercent(maxSlippage)))
|
||||
}, [maxSlippage])
|
||||
|
||||
const onInputSelect = useCallback(() => {
|
||||
focus()
|
||||
const percent = toPercent(maxSlippage)
|
||||
const warning = getSlippageWarning(percent)
|
||||
setAutoSlippage(!percent || warning === 'error')
|
||||
}, [focus, maxSlippage, setAutoSlippage])
|
||||
|
||||
const processValue = useCallback(
|
||||
(value: number | undefined) => {
|
||||
const percent = toPercent(value)
|
||||
const warning = getSlippageWarning(percent)
|
||||
setMaxSlippage(value)
|
||||
setAutoSlippage(!percent || warning === 'error')
|
||||
},
|
||||
[setAutoSlippage, setMaxSlippage]
|
||||
)
|
||||
|
||||
return (
|
||||
<Column gap={0.75}>
|
||||
<Label name={<Trans>Max slippage</Trans>} tooltip={tooltip} />
|
||||
<Row gap={0.5} grow="last">
|
||||
<Option wrapper={Button} selected={autoSlippage} onSelect={() => setAutoSlippage(true)}>
|
||||
<ThemedText.ButtonMedium>
|
||||
<Trans>Auto</Trans>
|
||||
</ThemedText.ButtonMedium>
|
||||
</Option>
|
||||
<Option
|
||||
wrapper={Custom}
|
||||
selected={!autoSlippage}
|
||||
onSelect={onInputSelect}
|
||||
icon={warning && <Warning state={warning} showTooltip={showTooltip} />}
|
||||
ref={option}
|
||||
tabIndex={-1}
|
||||
>
|
||||
<Row color={warning === 'error' ? 'error' : undefined}>
|
||||
<DecimalInput
|
||||
size={Math.max(maxSlippageInput.length, 4)}
|
||||
value={maxSlippageInput}
|
||||
onChange={(input) => processValue(+input)}
|
||||
placeholder={placeholder}
|
||||
ref={input}
|
||||
/>
|
||||
%
|
||||
</Row>
|
||||
</Option>
|
||||
</Row>
|
||||
</Column>
|
||||
)
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import { useAtom } from 'jotai'
|
||||
import { mockTogglableAtom } from 'lib/state/settings'
|
||||
|
||||
import Row from '../../Row'
|
||||
import Toggle from '../../Toggle'
|
||||
import { Label } from './components'
|
||||
|
||||
export default function MockToggle() {
|
||||
const [mockTogglable, toggleMockTogglable] = useAtom(mockTogglableAtom)
|
||||
return (
|
||||
<Row>
|
||||
<Label name={<Trans>Mock Toggle</Trans>} />
|
||||
<Toggle checked={mockTogglable} onToggle={toggleMockTogglable} />
|
||||
</Row>
|
||||
)
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import { useDefaultTransactionTtl, useTransactionTtl } from 'lib/hooks/useTransactionDeadline'
|
||||
import styled, { ThemedText } from 'lib/theme'
|
||||
import { useRef } from 'react'
|
||||
|
||||
import Column from '../../Column'
|
||||
import { inputCss, IntegerInput } from '../../Input'
|
||||
import Row from '../../Row'
|
||||
import { Label } from './components'
|
||||
|
||||
const tooltip = <Trans>Your transaction will revert if it has been pending for longer than this period of time.</Trans>
|
||||
|
||||
const Input = styled(Row)`
|
||||
${inputCss}
|
||||
`
|
||||
|
||||
export default function TransactionTtlInput() {
|
||||
const [ttl, setTtl] = useTransactionTtl()
|
||||
const defaultTtl = useDefaultTransactionTtl()
|
||||
const placeholder = defaultTtl.toString()
|
||||
const input = useRef<HTMLInputElement>(null)
|
||||
return (
|
||||
<Column gap={0.75}>
|
||||
<Label name={<Trans>Transaction deadline</Trans>} tooltip={tooltip} />
|
||||
<ThemedText.Body1>
|
||||
<Input justify="start" onClick={() => input.current?.focus()}>
|
||||
<IntegerInput
|
||||
placeholder={placeholder}
|
||||
value={ttl?.toString() ?? ''}
|
||||
onChange={(value) => setTtl(value ? parseFloat(value) : 0)}
|
||||
size={Math.max(ttl?.toString().length || 0, placeholder.length)}
|
||||
ref={input}
|
||||
/>
|
||||
<Trans>minutes</Trans>
|
||||
</Input>
|
||||
</ThemedText.Body1>
|
||||
</Column>
|
||||
)
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
import styled, { css, ThemedText } from 'lib/theme'
|
||||
import { ReactNode } from 'react'
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { AnyStyledComponent } from 'styled-components'
|
||||
|
||||
import Row from '../../Row'
|
||||
import Tooltip from '../../Tooltip'
|
||||
|
||||
export const optionCss = (selected: boolean) => css`
|
||||
border: 1px solid ${({ theme }) => (selected ? theme.active : theme.outline)};
|
||||
border-radius: ${({ theme }) => theme.borderRadius * 0.75}em;
|
||||
color: ${({ theme }) => theme.primary} !important;
|
||||
display: grid;
|
||||
grid-gap: 0.25em;
|
||||
padding: calc(0.75em - 1px) 0.625em;
|
||||
|
||||
:enabled {
|
||||
border: 1px solid ${({ theme }) => (selected ? theme.active : theme.outline)};
|
||||
}
|
||||
|
||||
:enabled:hover {
|
||||
border-color: ${({ theme }) => theme.onHover(selected ? theme.active : theme.outline)};
|
||||
}
|
||||
|
||||
:enabled:focus-within {
|
||||
border-color: ${({ theme }) => theme.active};
|
||||
}
|
||||
`
|
||||
|
||||
export function value(Value: AnyStyledComponent) {
|
||||
return styled(Value)<{ selected?: boolean; cursor?: string }>`
|
||||
cursor: ${({ cursor }) => cursor ?? 'pointer'};
|
||||
`
|
||||
}
|
||||
|
||||
interface LabelProps {
|
||||
name: ReactNode
|
||||
tooltip?: ReactNode
|
||||
}
|
||||
|
||||
export function Label({ name, tooltip }: LabelProps) {
|
||||
return (
|
||||
<Row gap={0.5} justify="flex-start">
|
||||
<ThemedText.Subhead2>{name}</ThemedText.Subhead2>
|
||||
{tooltip && (
|
||||
<Tooltip placement="top" contained>
|
||||
<ThemedText.Caption>{tooltip}</ThemedText.Caption>
|
||||
</Tooltip>
|
||||
)}
|
||||
</Row>
|
||||
)
|
||||
}
|
||||
@@ -1,67 +0,0 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import { useResetAtom } from 'jotai/utils'
|
||||
import useScrollbar from 'lib/hooks/useScrollbar'
|
||||
import { Settings as SettingsIcon } from 'lib/icons'
|
||||
import { settingsAtom } from 'lib/state/settings'
|
||||
import styled, { ThemedText } from 'lib/theme'
|
||||
import React, { useState } from 'react'
|
||||
|
||||
import { IconButton, TextButton } from '../../Button'
|
||||
import Column from '../../Column'
|
||||
import Dialog, { Header } from '../../Dialog'
|
||||
import { BoundaryProvider } from '../../Popover'
|
||||
import MaxSlippageSelect from './MaxSlippageSelect'
|
||||
import TransactionTtlInput from './TransactionTtlInput'
|
||||
|
||||
export function SettingsDialog() {
|
||||
const [boundary, setBoundary] = useState<HTMLDivElement | null>(null)
|
||||
const scrollbar = useScrollbar(boundary, { padded: true })
|
||||
const resetSettings = useResetAtom(settingsAtom)
|
||||
return (
|
||||
<>
|
||||
<Header title={<Trans>Settings</Trans>} ruled>
|
||||
<TextButton onClick={resetSettings}>
|
||||
<ThemedText.ButtonSmall>
|
||||
<Trans>Reset</Trans>
|
||||
</ThemedText.ButtonSmall>
|
||||
</TextButton>
|
||||
</Header>
|
||||
<Column gap={1} style={{ paddingTop: '1em' }} ref={setBoundary} padded css={scrollbar}>
|
||||
<BoundaryProvider value={boundary}>
|
||||
<MaxSlippageSelect />
|
||||
<TransactionTtlInput />
|
||||
</BoundaryProvider>
|
||||
</Column>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const SettingsButton = styled(IconButton)<{ hover: boolean }>`
|
||||
${SettingsIcon} {
|
||||
transform: ${({ hover }) => hover && 'rotate(45deg)'};
|
||||
transition: ${({ hover }) => hover && 'transform 0.25s'};
|
||||
will-change: transform;
|
||||
}
|
||||
`
|
||||
|
||||
export default function Settings({ disabled }: { disabled?: boolean }) {
|
||||
const [open, setOpen] = useState(false)
|
||||
const [hover, setHover] = useState(false)
|
||||
return (
|
||||
<>
|
||||
<SettingsButton
|
||||
disabled={disabled}
|
||||
hover={hover}
|
||||
onClick={() => setOpen(true)}
|
||||
onMouseEnter={() => setHover(true)}
|
||||
onMouseLeave={() => setHover(false)}
|
||||
icon={SettingsIcon}
|
||||
/>
|
||||
{open && (
|
||||
<Dialog color="module" onClose={() => setOpen(false)}>
|
||||
<SettingsDialog />
|
||||
</Dialog>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
import { Modal } from '../Dialog'
|
||||
|
||||
function Fixture() {
|
||||
return null
|
||||
// TODO(zzmp): Mock <StatusDialog tx={} onClose={() => void 0} />
|
||||
}
|
||||
|
||||
export default (
|
||||
<Modal color="dialog">
|
||||
<Fixture />
|
||||
</Modal>
|
||||
)
|
||||
@@ -1,115 +0,0 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import ErrorDialog, { StatusHeader } from 'lib/components/Error/ErrorDialog'
|
||||
import EtherscanLink from 'lib/components/EtherscanLink'
|
||||
import Rule from 'lib/components/Rule'
|
||||
import SwapSummary from 'lib/components/Swap/Summary'
|
||||
import useInterval from 'lib/hooks/useInterval'
|
||||
import { CheckCircle, Clock, Spinner } from 'lib/icons'
|
||||
import { SwapTransactionInfo, Transaction, TransactionType, WrapTransactionInfo } from 'lib/state/transactions'
|
||||
import styled, { ThemedText } from 'lib/theme'
|
||||
import ms from 'ms.macro'
|
||||
import { useCallback, useMemo, useState } from 'react'
|
||||
import { ExplorerDataType } from 'utils/getExplorerLink'
|
||||
|
||||
import ActionButton from '../../ActionButton'
|
||||
import Column from '../../Column'
|
||||
import Row from '../../Row'
|
||||
|
||||
const errorMessage = (
|
||||
<Trans>
|
||||
Try increasing your slippage tolerance.
|
||||
<br />
|
||||
NOTE: Fee on transfer and rebase tokens are incompatible with Uniswap V3.
|
||||
</Trans>
|
||||
)
|
||||
|
||||
const TransactionRow = styled(Row)`
|
||||
flex-direction: row-reverse;
|
||||
`
|
||||
|
||||
type PendingTransaction = Transaction<SwapTransactionInfo | WrapTransactionInfo>
|
||||
|
||||
function ElapsedTime({ tx }: { tx: PendingTransaction }) {
|
||||
const [elapsedMs, setElapsedMs] = useState(0)
|
||||
|
||||
useInterval(() => setElapsedMs(Date.now() - tx.addedTime), tx.receipt ? null : ms`1s`)
|
||||
|
||||
const toElapsedTime = useCallback((ms: number) => {
|
||||
let sec = Math.floor(ms / 1000)
|
||||
const min = Math.floor(sec / 60)
|
||||
sec = sec % 60
|
||||
if (min) {
|
||||
return (
|
||||
<Trans>
|
||||
{min}m {sec}s
|
||||
</Trans>
|
||||
)
|
||||
} else {
|
||||
return <Trans>{sec}s</Trans>
|
||||
}
|
||||
}, [])
|
||||
return (
|
||||
<Row gap={0.5}>
|
||||
<Clock />
|
||||
<ThemedText.Body2>{toElapsedTime(elapsedMs)}</ThemedText.Body2>
|
||||
</Row>
|
||||
)
|
||||
}
|
||||
|
||||
interface TransactionStatusProps {
|
||||
tx: PendingTransaction
|
||||
onClose: () => void
|
||||
}
|
||||
|
||||
function TransactionStatus({ tx, onClose }: TransactionStatusProps) {
|
||||
const Icon = useMemo(() => {
|
||||
return tx.receipt?.status ? CheckCircle : Spinner
|
||||
}, [tx.receipt?.status])
|
||||
const heading = useMemo(() => {
|
||||
if (tx.info.type === TransactionType.SWAP) {
|
||||
return tx.receipt?.status ? <Trans>Swap confirmed</Trans> : <Trans>Swap pending</Trans>
|
||||
} else if (tx.info.type === TransactionType.WRAP) {
|
||||
if (tx.info.unwrapped) {
|
||||
return tx.receipt?.status ? <Trans>Unwrap confirmed</Trans> : <Trans>Unwrap pending</Trans>
|
||||
}
|
||||
return tx.receipt?.status ? <Trans>Wrap confirmed</Trans> : <Trans>Wrap pending</Trans>
|
||||
}
|
||||
return tx.receipt?.status ? <Trans>Transaction confirmed</Trans> : <Trans>Transaction pending</Trans>
|
||||
}, [tx.info, tx.receipt?.status])
|
||||
|
||||
return (
|
||||
<Column flex padded gap={0.75} align="stretch" style={{ height: '100%' }}>
|
||||
<StatusHeader icon={Icon} iconColor={tx.receipt?.status ? 'success' : undefined}>
|
||||
<ThemedText.Subhead1>{heading}</ThemedText.Subhead1>
|
||||
{tx.info.type === TransactionType.SWAP ? (
|
||||
<SwapSummary input={tx.info.inputCurrencyAmount} output={tx.info.outputCurrencyAmount} />
|
||||
) : null}
|
||||
</StatusHeader>
|
||||
<Rule />
|
||||
<TransactionRow flex>
|
||||
<ThemedText.ButtonSmall>
|
||||
<EtherscanLink type={ExplorerDataType.TRANSACTION} data={tx.info.response.hash}>
|
||||
<Trans>View on Etherscan</Trans>
|
||||
</EtherscanLink>
|
||||
</ThemedText.ButtonSmall>
|
||||
<ElapsedTime tx={tx} />
|
||||
</TransactionRow>
|
||||
<ActionButton onClick={onClose}>
|
||||
<Trans>Close</Trans>
|
||||
</ActionButton>
|
||||
</Column>
|
||||
)
|
||||
}
|
||||
|
||||
export default function TransactionStatusDialog({ tx, onClose }: TransactionStatusProps) {
|
||||
return tx.receipt?.status === 0 ? (
|
||||
<ErrorDialog
|
||||
header={errorMessage}
|
||||
error={new Error('TODO(zzmp)')}
|
||||
action={<Trans>Dismiss</Trans>}
|
||||
onClick={onClose}
|
||||
/>
|
||||
) : (
|
||||
<TransactionStatus tx={tx} onClose={onClose} />
|
||||
)
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
export { default as StatusDialog } from './StatusDialog'
|
||||
@@ -1,61 +0,0 @@
|
||||
import { tokens } from '@uniswap/default-token-list'
|
||||
import { SupportedChainId } from 'constants/chains'
|
||||
import { nativeOnChain } from 'constants/tokens'
|
||||
import { useUpdateAtom } from 'jotai/utils'
|
||||
import { useSwapInfo } from 'lib/hooks/swap'
|
||||
import { SwapInfoProvider } from 'lib/hooks/swap/useSwapInfo'
|
||||
import { Field, swapAtom } from 'lib/state/swap'
|
||||
import { useEffect } from 'react'
|
||||
import { WrappedTokenInfo } from 'state/lists/wrappedTokenInfo'
|
||||
import invariant from 'tiny-invariant'
|
||||
|
||||
import { Modal } from '../Dialog'
|
||||
import { SummaryDialog } from './Summary'
|
||||
|
||||
const ETH = nativeOnChain(SupportedChainId.MAINNET)
|
||||
const UNI = (function () {
|
||||
const token = tokens.find(({ symbol }) => symbol === 'UNI')
|
||||
invariant(token)
|
||||
return new WrappedTokenInfo(token)
|
||||
})()
|
||||
|
||||
function Fixture() {
|
||||
const setState = useUpdateAtom(swapAtom)
|
||||
const {
|
||||
[Field.INPUT]: { usdc: inputUSDC },
|
||||
[Field.OUTPUT]: { usdc: outputUSDC },
|
||||
trade: { trade },
|
||||
slippage,
|
||||
impact,
|
||||
} = useSwapInfo()
|
||||
|
||||
useEffect(() => {
|
||||
setState({
|
||||
independentField: Field.INPUT,
|
||||
amount: '1',
|
||||
[Field.INPUT]: ETH,
|
||||
[Field.OUTPUT]: UNI,
|
||||
})
|
||||
}, [setState])
|
||||
|
||||
return trade ? (
|
||||
<Modal color="dialog">
|
||||
<SummaryDialog
|
||||
onConfirm={async () => void 0}
|
||||
trade={trade}
|
||||
slippage={slippage}
|
||||
inputUSDC={inputUSDC}
|
||||
outputUSDC={outputUSDC}
|
||||
impact={impact}
|
||||
/>
|
||||
</Modal>
|
||||
) : null
|
||||
}
|
||||
|
||||
export default (
|
||||
<>
|
||||
<SwapInfoProvider>
|
||||
<Fixture />
|
||||
</SwapInfoProvider>
|
||||
</>
|
||||
)
|
||||
@@ -1,108 +0,0 @@
|
||||
import { t } from '@lingui/macro'
|
||||
import { useLingui } from '@lingui/react'
|
||||
import { Trade } from '@uniswap/router-sdk'
|
||||
import { Currency, TradeType } from '@uniswap/sdk-core'
|
||||
import { useAtomValue } from 'jotai/utils'
|
||||
import Column from 'lib/components/Column'
|
||||
import Row from 'lib/components/Row'
|
||||
import { Slippage } from 'lib/hooks/useSlippage'
|
||||
import { PriceImpact } from 'lib/hooks/useUSDCPriceImpact'
|
||||
import { feeOptionsAtom } from 'lib/state/swap'
|
||||
import styled, { Color, ThemedText } from 'lib/theme'
|
||||
import { useMemo } from 'react'
|
||||
import { currencyId } from 'utils/currencyId'
|
||||
import { formatCurrencyAmount } from 'utils/formatCurrencyAmount'
|
||||
import { computeRealizedLPFeeAmount } from 'utils/prices'
|
||||
|
||||
const Value = styled.span<{ color?: Color }>`
|
||||
color: ${({ color, theme }) => color && theme[color]};
|
||||
white-space: nowrap;
|
||||
`
|
||||
|
||||
interface DetailProps {
|
||||
label: string
|
||||
value: string
|
||||
color?: Color
|
||||
}
|
||||
|
||||
function Detail({ label, value, color }: DetailProps) {
|
||||
return (
|
||||
<ThemedText.Caption userSelect>
|
||||
<Row gap={2}>
|
||||
<span>{label}</span>
|
||||
<Value color={color}>{value}</Value>
|
||||
</Row>
|
||||
</ThemedText.Caption>
|
||||
)
|
||||
}
|
||||
|
||||
interface DetailsProps {
|
||||
trade: Trade<Currency, Currency, TradeType>
|
||||
slippage: Slippage
|
||||
impact?: PriceImpact
|
||||
}
|
||||
|
||||
export default function Details({ trade, slippage, impact }: DetailsProps) {
|
||||
const { inputAmount, outputAmount } = trade
|
||||
const inputCurrency = inputAmount.currency
|
||||
const outputCurrency = outputAmount.currency
|
||||
const integrator = window.location.hostname
|
||||
const feeOptions = useAtomValue(feeOptionsAtom)
|
||||
const lpFeeAmount = useMemo(() => computeRealizedLPFeeAmount(trade), [trade])
|
||||
const { i18n } = useLingui()
|
||||
|
||||
const details = useMemo(() => {
|
||||
const rows: Array<[string, string] | [string, string, Color | undefined]> = []
|
||||
// @TODO(ianlapham): Check that provider fee is even a valid list item
|
||||
|
||||
if (feeOptions) {
|
||||
const fee = outputAmount.multiply(feeOptions.fee)
|
||||
if (fee.greaterThan(0)) {
|
||||
const parsedFee = formatCurrencyAmount(fee, 6, i18n.locale)
|
||||
rows.push([t`${integrator} fee`, `${parsedFee} ${outputCurrency.symbol || currencyId(outputCurrency)}`])
|
||||
}
|
||||
}
|
||||
|
||||
if (impact) {
|
||||
rows.push([t`Price impact`, impact.toString(), impact.warning])
|
||||
}
|
||||
|
||||
if (lpFeeAmount) {
|
||||
const parsedLpFee = formatCurrencyAmount(lpFeeAmount, 6, i18n.locale)
|
||||
rows.push([t`Liquidity provider fee`, `${parsedLpFee} ${inputCurrency.symbol || currencyId(inputCurrency)}`])
|
||||
}
|
||||
|
||||
if (trade.tradeType === TradeType.EXACT_OUTPUT) {
|
||||
const localizedMaxSent = formatCurrencyAmount(trade.maximumAmountIn(slippage.allowed), 6, i18n.locale)
|
||||
rows.push([t`Maximum sent`, `${localizedMaxSent} ${inputCurrency.symbol}`])
|
||||
}
|
||||
|
||||
if (trade.tradeType === TradeType.EXACT_INPUT) {
|
||||
const localizedMaxSent = formatCurrencyAmount(trade.minimumAmountOut(slippage.allowed), 6, i18n.locale)
|
||||
rows.push([t`Minimum received`, `${localizedMaxSent} ${outputCurrency.symbol}`])
|
||||
}
|
||||
|
||||
rows.push([t`Slippage tolerance`, `${slippage.allowed.toFixed(2)}%`, slippage.warning])
|
||||
|
||||
return rows
|
||||
}, [
|
||||
feeOptions,
|
||||
i18n.locale,
|
||||
impact,
|
||||
inputCurrency,
|
||||
integrator,
|
||||
lpFeeAmount,
|
||||
outputAmount,
|
||||
outputCurrency,
|
||||
slippage,
|
||||
trade,
|
||||
])
|
||||
|
||||
return (
|
||||
<Column gap={0.5}>
|
||||
{details.map(([label, detail, color]) => (
|
||||
<Detail key={label} label={label} value={detail} color={color} />
|
||||
))}
|
||||
</Column>
|
||||
)
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
import { useLingui } from '@lingui/react'
|
||||
import { Currency, CurrencyAmount } from '@uniswap/sdk-core'
|
||||
import { PriceImpact } from 'lib/hooks/useUSDCPriceImpact'
|
||||
import { ArrowRight } from 'lib/icons'
|
||||
import { ThemedText } from 'lib/theme'
|
||||
import { PropsWithChildren } from 'react'
|
||||
import { formatCurrencyAmount } from 'utils/formatCurrencyAmount'
|
||||
|
||||
import Column from '../../Column'
|
||||
import Row from '../../Row'
|
||||
import TokenImg from '../../TokenImg'
|
||||
|
||||
interface TokenValueProps {
|
||||
input: CurrencyAmount<Currency>
|
||||
usdc?: CurrencyAmount<Currency>
|
||||
}
|
||||
|
||||
function TokenValue({ input, usdc, children }: PropsWithChildren<TokenValueProps>) {
|
||||
const { i18n } = useLingui()
|
||||
return (
|
||||
<Column justify="flex-start">
|
||||
<Row gap={0.375} justify="flex-start">
|
||||
<TokenImg token={input.currency} />
|
||||
<ThemedText.Body2 userSelect>
|
||||
{formatCurrencyAmount(input, 6, i18n.locale)} {input.currency.symbol}
|
||||
</ThemedText.Body2>
|
||||
</Row>
|
||||
{usdc && (
|
||||
<ThemedText.Caption color="secondary" userSelect>
|
||||
<Row justify="flex-start" gap={0.25}>
|
||||
${formatCurrencyAmount(usdc, 6, 'en', 2)}
|
||||
{children}
|
||||
</Row>
|
||||
</ThemedText.Caption>
|
||||
)}
|
||||
</Column>
|
||||
)
|
||||
}
|
||||
|
||||
interface SummaryProps {
|
||||
input: CurrencyAmount<Currency>
|
||||
output: CurrencyAmount<Currency>
|
||||
inputUSDC?: CurrencyAmount<Currency>
|
||||
outputUSDC?: CurrencyAmount<Currency>
|
||||
impact?: PriceImpact
|
||||
}
|
||||
|
||||
export default function Summary({ input, output, inputUSDC, outputUSDC, impact }: SummaryProps) {
|
||||
return (
|
||||
<Row gap={impact ? 1 : 0.25}>
|
||||
<TokenValue input={input} usdc={inputUSDC} />
|
||||
<ArrowRight />
|
||||
<TokenValue input={output} usdc={outputUSDC}>
|
||||
{impact && <ThemedText.Caption color={impact.warning}>({impact.toString()})</ThemedText.Caption>}
|
||||
</TokenValue>
|
||||
</Row>
|
||||
)
|
||||
}
|
||||
@@ -1,182 +0,0 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import { useLingui } from '@lingui/react'
|
||||
import { Trade } from '@uniswap/router-sdk'
|
||||
import { Currency, CurrencyAmount, TradeType } from '@uniswap/sdk-core'
|
||||
import ActionButton, { Action } from 'lib/components/ActionButton'
|
||||
import Column from 'lib/components/Column'
|
||||
import { Header } from 'lib/components/Dialog'
|
||||
import Expando from 'lib/components/Expando'
|
||||
import Row from 'lib/components/Row'
|
||||
import { Slippage } from 'lib/hooks/useSlippage'
|
||||
import { PriceImpact } from 'lib/hooks/useUSDCPriceImpact'
|
||||
import { AlertTriangle, BarChart, Info, Spinner } from 'lib/icons'
|
||||
import styled, { ThemedText } from 'lib/theme'
|
||||
import { useCallback, useMemo, useState } from 'react'
|
||||
import { formatCurrencyAmount } from 'utils/formatCurrencyAmount'
|
||||
import { tradeMeaningfullyDiffers } from 'utils/tradeMeaningFullyDiffer'
|
||||
|
||||
import Price from '../Price'
|
||||
import Details from './Details'
|
||||
import Summary from './Summary'
|
||||
|
||||
export default Summary
|
||||
|
||||
const Content = styled(Column)``
|
||||
const Heading = styled(Column)``
|
||||
const Footing = styled(Column)``
|
||||
const Body = styled(Column)<{ open: boolean }>`
|
||||
height: calc(100% - 2.5em);
|
||||
|
||||
${Content}, ${Heading} {
|
||||
flex-grow: 1;
|
||||
transition: flex-grow 0.25s;
|
||||
}
|
||||
|
||||
${Footing} {
|
||||
margin-bottom: ${({ open }) => (open ? '-0.75em' : undefined)};
|
||||
max-height: ${({ open }) => (open ? 0 : '3em')};
|
||||
opacity: ${({ open }) => (open ? 0 : 1)};
|
||||
transition: max-height 0.25s, margin-bottom 0.25s, opacity 0.15s 0.1s;
|
||||
visibility: ${({ open }) => (open ? 'hidden' : undefined)};
|
||||
}
|
||||
`
|
||||
|
||||
function Subhead({ impact, slippage }: { impact?: PriceImpact; slippage: Slippage }) {
|
||||
return (
|
||||
<Row gap={0.5}>
|
||||
{impact?.warning || slippage.warning ? (
|
||||
<AlertTriangle color={impact?.warning || slippage.warning} />
|
||||
) : (
|
||||
<Info color="secondary" />
|
||||
)}
|
||||
<ThemedText.Subhead2 color={impact?.warning || slippage.warning || 'secondary'}>
|
||||
{impact?.warning ? (
|
||||
<Trans>High price impact</Trans>
|
||||
) : slippage.warning ? (
|
||||
<Trans>High slippage</Trans>
|
||||
) : (
|
||||
<Trans>Swap details</Trans>
|
||||
)}
|
||||
</ThemedText.Subhead2>
|
||||
</Row>
|
||||
)
|
||||
}
|
||||
|
||||
function Estimate({ trade, slippage }: { trade: Trade<Currency, Currency, TradeType>; slippage: Slippage }) {
|
||||
const { i18n } = useLingui()
|
||||
const text = useMemo(() => {
|
||||
switch (trade.tradeType) {
|
||||
case TradeType.EXACT_INPUT:
|
||||
return (
|
||||
<Trans>
|
||||
Output is estimated. You will receive at least{' '}
|
||||
{formatCurrencyAmount(trade.minimumAmountOut(slippage.allowed), 6, i18n.locale)}{' '}
|
||||
{trade.outputAmount.currency.symbol} or the transaction will revert.
|
||||
</Trans>
|
||||
)
|
||||
case TradeType.EXACT_OUTPUT:
|
||||
return (
|
||||
<Trans>
|
||||
Output is estimated. You will send at most{' '}
|
||||
{formatCurrencyAmount(trade.maximumAmountIn(slippage.allowed), 6, i18n.locale)}{' '}
|
||||
{trade.inputAmount.currency.symbol} or the transaction will revert.
|
||||
</Trans>
|
||||
)
|
||||
}
|
||||
}, [i18n.locale, slippage.allowed, trade])
|
||||
return <ThemedText.Caption color="secondary">{text}</ThemedText.Caption>
|
||||
}
|
||||
|
||||
function ConfirmButton({
|
||||
trade,
|
||||
highPriceImpact,
|
||||
onConfirm,
|
||||
}: {
|
||||
trade: Trade<Currency, Currency, TradeType>
|
||||
highPriceImpact: boolean
|
||||
onConfirm: () => Promise<void>
|
||||
}) {
|
||||
const [ackPriceImpact, setAckPriceImpact] = useState(false)
|
||||
|
||||
const [ackTrade, setAckTrade] = useState(trade)
|
||||
const doesTradeDiffer = useMemo(
|
||||
() => Boolean(trade && ackTrade && tradeMeaningfullyDiffers(trade, ackTrade)),
|
||||
[ackTrade, trade]
|
||||
)
|
||||
|
||||
const [isPending, setIsPending] = useState(false)
|
||||
const onClick = useCallback(async () => {
|
||||
setIsPending(true)
|
||||
await onConfirm()
|
||||
setIsPending(false)
|
||||
}, [onConfirm])
|
||||
|
||||
const action = useMemo((): Action | undefined => {
|
||||
if (isPending) {
|
||||
return { message: <Trans>Confirm in your wallet</Trans>, icon: Spinner }
|
||||
} else if (doesTradeDiffer) {
|
||||
return {
|
||||
message: <Trans>Price updated</Trans>,
|
||||
icon: BarChart,
|
||||
onClick: () => setAckTrade(trade),
|
||||
children: <Trans>Accept</Trans>,
|
||||
}
|
||||
} else if (highPriceImpact && !ackPriceImpact) {
|
||||
return {
|
||||
message: <Trans>High price impact</Trans>,
|
||||
onClick: () => setAckPriceImpact(true),
|
||||
children: <Trans>Acknowledge</Trans>,
|
||||
}
|
||||
}
|
||||
return
|
||||
}, [ackPriceImpact, doesTradeDiffer, highPriceImpact, isPending, trade])
|
||||
|
||||
return (
|
||||
<ActionButton onClick={onClick} action={action}>
|
||||
<Trans>Confirm swap</Trans>
|
||||
</ActionButton>
|
||||
)
|
||||
}
|
||||
|
||||
interface SummaryDialogProps {
|
||||
trade: Trade<Currency, Currency, TradeType>
|
||||
slippage: Slippage
|
||||
inputUSDC?: CurrencyAmount<Currency>
|
||||
outputUSDC?: CurrencyAmount<Currency>
|
||||
impact?: PriceImpact
|
||||
onConfirm: () => Promise<void>
|
||||
}
|
||||
|
||||
export function SummaryDialog({ trade, slippage, inputUSDC, outputUSDC, impact, onConfirm }: SummaryDialogProps) {
|
||||
const { inputAmount, outputAmount } = trade
|
||||
|
||||
const [open, setOpen] = useState(false)
|
||||
const onExpand = useCallback(() => setOpen((open) => !open), [])
|
||||
|
||||
return (
|
||||
<>
|
||||
<Header title={<Trans>Swap summary</Trans>} ruled />
|
||||
<Body flex align="stretch" padded gap={0.75} open={open}>
|
||||
<Heading gap={0.75} flex justify="center">
|
||||
<Summary
|
||||
input={inputAmount}
|
||||
output={outputAmount}
|
||||
inputUSDC={inputUSDC}
|
||||
outputUSDC={outputUSDC}
|
||||
impact={impact}
|
||||
/>
|
||||
<Price trade={trade} />
|
||||
</Heading>
|
||||
<Column gap={open ? 0 : 0.75} style={{ transition: 'gap 0.25s' }}>
|
||||
<Expando title={<Subhead impact={impact} slippage={slippage} />} open={open} onExpand={onExpand} height={7}>
|
||||
<Details trade={trade} slippage={slippage} impact={impact} />
|
||||
</Expando>
|
||||
<Footing>
|
||||
<Estimate trade={trade} slippage={slippage} />
|
||||
</Footing>
|
||||
<ConfirmButton trade={trade} highPriceImpact={impact?.warning === 'error'} onConfirm={onConfirm} />
|
||||
</Column>
|
||||
</Body>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -1,72 +0,0 @@
|
||||
import { DAI, USDC_MAINNET } from 'constants/tokens'
|
||||
import { useUpdateAtom } from 'jotai/utils'
|
||||
import { useEffect } from 'react'
|
||||
import { useSelect, useValue } from 'react-cosmos/fixture'
|
||||
|
||||
import Swap from '.'
|
||||
import { colorAtom } from './Output'
|
||||
|
||||
const validateColor = (() => {
|
||||
const validator = document.createElement('div').style
|
||||
return (color: string) => {
|
||||
validator.color = ''
|
||||
validator.color = color
|
||||
return validator.color !== ''
|
||||
}
|
||||
})()
|
||||
|
||||
function Fixture() {
|
||||
const setColor = useUpdateAtom(colorAtom)
|
||||
const [color] = useValue('token color', { defaultValue: '' })
|
||||
useEffect(() => {
|
||||
if (!color || validateColor(color)) {
|
||||
setColor(color)
|
||||
}
|
||||
}, [color, setColor])
|
||||
|
||||
const [convenienceFee] = useValue('convenienceFee', { defaultValue: 100 })
|
||||
const FEE_RECIPIENT_OPTIONS = [
|
||||
'',
|
||||
'0x1D9Cd50Dde9C19073B81303b3d930444d11552f7',
|
||||
'0x0dA5533d5a9aA08c1792Ef2B6a7444E149cCB0AD',
|
||||
'0xE6abE059E5e929fd17bef158902E73f0FEaCD68c',
|
||||
]
|
||||
const [convenienceFeeRecipient] = useSelect('convenienceFeeRecipient', {
|
||||
options: FEE_RECIPIENT_OPTIONS,
|
||||
defaultValue: FEE_RECIPIENT_OPTIONS[1],
|
||||
})
|
||||
|
||||
const optionsToAddressMap: Record<string, string | undefined> = {
|
||||
None: undefined,
|
||||
Native: 'NATIVE',
|
||||
DAI: DAI.address,
|
||||
USDC: USDC_MAINNET.address,
|
||||
}
|
||||
const addressOptions = Object.keys(optionsToAddressMap)
|
||||
|
||||
const [defaultInputToken] = useSelect('defaultInputToken', {
|
||||
options: addressOptions,
|
||||
defaultValue: addressOptions[1],
|
||||
})
|
||||
const [defaultInputAmount] = useValue('defaultInputAmount', { defaultValue: 1 })
|
||||
|
||||
const [defaultOutputToken] = useSelect('defaultOutputToken', {
|
||||
options: addressOptions,
|
||||
defaultValue: addressOptions[2],
|
||||
})
|
||||
const [defaultOutputAmount] = useValue('defaultOutputAmount', { defaultValue: 0 })
|
||||
|
||||
return (
|
||||
<Swap
|
||||
convenienceFee={convenienceFee}
|
||||
convenienceFeeRecipient={convenienceFeeRecipient}
|
||||
defaultInputTokenAddress={optionsToAddressMap[defaultInputToken]}
|
||||
defaultInputAmount={defaultInputAmount}
|
||||
defaultOutputTokenAddress={optionsToAddressMap[defaultOutputToken]}
|
||||
defaultOutputAmount={defaultOutputAmount}
|
||||
onConnectWallet={() => console.log('onConnectWallet')} // this handler is included as a test of functionality, but only logs
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default <Fixture />
|
||||
@@ -1,200 +0,0 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import { useAtomValue, useUpdateAtom } from 'jotai/utils'
|
||||
import { useSwapInfo } from 'lib/hooks/swap'
|
||||
import { useSwapApprovalOptimizedTrade } from 'lib/hooks/swap/useSwapApproval'
|
||||
import { useSwapCallback } from 'lib/hooks/swap/useSwapCallback'
|
||||
import useWrapCallback, { WrapType } from 'lib/hooks/swap/useWrapCallback'
|
||||
import { useAddTransaction } from 'lib/hooks/transactions'
|
||||
import useActiveWeb3React from 'lib/hooks/useActiveWeb3React'
|
||||
import { useSetOldestValidBlock } from 'lib/hooks/useIsValidBlock'
|
||||
import useTransactionDeadline from 'lib/hooks/useTransactionDeadline'
|
||||
import { Spinner } from 'lib/icons'
|
||||
import { displayTxHashAtom, feeOptionsAtom, Field } from 'lib/state/swap'
|
||||
import { TransactionType } from 'lib/state/transactions'
|
||||
import { useTheme } from 'lib/theme'
|
||||
import { isAnimating } from 'lib/utils/animations'
|
||||
import { memo, useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import { TradeState } from 'state/routing/types'
|
||||
import invariant from 'tiny-invariant'
|
||||
|
||||
import ActionButton, { ActionButtonProps } from '../../ActionButton'
|
||||
import Dialog from '../../Dialog'
|
||||
import { SummaryDialog } from '../Summary'
|
||||
import useApprovalData, { useIsPendingApproval } from './useApprovalData'
|
||||
|
||||
interface SwapButtonProps {
|
||||
disabled?: boolean
|
||||
}
|
||||
|
||||
export default memo(function SwapButton({ disabled }: SwapButtonProps) {
|
||||
const { account, chainId } = useActiveWeb3React()
|
||||
const {
|
||||
[Field.INPUT]: {
|
||||
currency: inputCurrency,
|
||||
amount: inputCurrencyAmount,
|
||||
balance: inputCurrencyBalance,
|
||||
usdc: inputUSDC,
|
||||
},
|
||||
[Field.OUTPUT]: { usdc: outputUSDC },
|
||||
trade,
|
||||
slippage,
|
||||
impact,
|
||||
} = useSwapInfo()
|
||||
const feeOptions = useAtomValue(feeOptionsAtom)
|
||||
|
||||
// TODO(zzmp): Return an optimized trade directly from useSwapInfo.
|
||||
const optimizedTrade =
|
||||
// Use trade.trade if there is no swap optimized trade. This occurs if approvals are still pending.
|
||||
useSwapApprovalOptimizedTrade(trade.trade, slippage.allowed, useIsPendingApproval) || trade.trade
|
||||
const deadline = useTransactionDeadline()
|
||||
|
||||
const { type: wrapType, callback: wrapCallback } = useWrapCallback()
|
||||
const { approvalAction, signatureData } = useApprovalData(optimizedTrade, slippage, inputCurrencyAmount)
|
||||
const { callback: swapCallback } = useSwapCallback({
|
||||
trade: optimizedTrade,
|
||||
allowedSlippage: slippage.allowed,
|
||||
recipientAddressOrName: account ?? null,
|
||||
signatureData,
|
||||
deadline,
|
||||
feeOptions,
|
||||
})
|
||||
|
||||
const [open, setOpen] = useState(false)
|
||||
// Close the review modal if there is no available trade.
|
||||
useEffect(() => setOpen((open) => (trade.trade ? open : false)), [trade.trade])
|
||||
// Close the review modal on chain change.
|
||||
useEffect(() => setOpen(false), [chainId])
|
||||
|
||||
const addTransaction = useAddTransaction()
|
||||
const setDisplayTxHash = useUpdateAtom(displayTxHashAtom)
|
||||
const setOldestValidBlock = useSetOldestValidBlock()
|
||||
|
||||
const [isPending, setIsPending] = useState(false)
|
||||
const onWrap = useCallback(async () => {
|
||||
setIsPending(true)
|
||||
try {
|
||||
const transaction = await wrapCallback?.()
|
||||
if (!transaction) return
|
||||
addTransaction({
|
||||
response: transaction,
|
||||
type: TransactionType.WRAP,
|
||||
unwrapped: wrapType === WrapType.UNWRAP,
|
||||
currencyAmountRaw: transaction.value?.toString() ?? '0',
|
||||
chainId,
|
||||
})
|
||||
setDisplayTxHash(transaction.hash)
|
||||
} catch (e) {
|
||||
// TODO(zzmp): Surface errors from wrap.
|
||||
console.log(e)
|
||||
}
|
||||
|
||||
// Only reset pending after any queued animations to avoid layout thrashing, because a
|
||||
// successful wrap will open the status dialog and immediately cover the button.
|
||||
const postWrap = () => {
|
||||
setIsPending(false)
|
||||
document.removeEventListener('animationend', postWrap)
|
||||
}
|
||||
if (isAnimating(document)) {
|
||||
document.addEventListener('animationend', postWrap)
|
||||
} else {
|
||||
postWrap()
|
||||
}
|
||||
}, [addTransaction, chainId, setDisplayTxHash, wrapCallback, wrapType])
|
||||
// Reset the pending state if user updates the swap.
|
||||
useEffect(() => setIsPending(false), [inputCurrencyAmount, trade])
|
||||
|
||||
const onSwap = useCallback(async () => {
|
||||
try {
|
||||
const transaction = await swapCallback?.()
|
||||
if (!transaction) return
|
||||
invariant(trade.trade)
|
||||
addTransaction({
|
||||
response: transaction,
|
||||
type: TransactionType.SWAP,
|
||||
tradeType: trade.trade.tradeType,
|
||||
inputCurrencyAmount: trade.trade.inputAmount,
|
||||
outputCurrencyAmount: trade.trade.outputAmount,
|
||||
})
|
||||
setDisplayTxHash(transaction.hash)
|
||||
|
||||
// Set the block containing the response to the oldest valid block to ensure that the
|
||||
// completed trade's impact is reflected in future fetched trades.
|
||||
transaction.wait(1).then((receipt) => {
|
||||
setOldestValidBlock(receipt.blockNumber)
|
||||
})
|
||||
|
||||
// Only reset open after any queued animations to avoid layout thrashing, because a
|
||||
// successful swap will open the status dialog and immediately cover the summary dialog.
|
||||
const postSwap = () => {
|
||||
setOpen(false)
|
||||
document.removeEventListener('animationend', postSwap)
|
||||
}
|
||||
if (isAnimating(document)) {
|
||||
document.addEventListener('animationend', postSwap)
|
||||
} else {
|
||||
postSwap()
|
||||
}
|
||||
} catch (e) {
|
||||
// TODO(zzmp): Surface errors from swap.
|
||||
console.log(e)
|
||||
}
|
||||
}, [addTransaction, setDisplayTxHash, setOldestValidBlock, swapCallback, trade.trade])
|
||||
|
||||
const disableSwap = useMemo(
|
||||
() =>
|
||||
disabled ||
|
||||
!chainId ||
|
||||
(wrapType === WrapType.NONE && !optimizedTrade) ||
|
||||
!(inputCurrencyAmount && inputCurrencyBalance) ||
|
||||
inputCurrencyBalance.lessThan(inputCurrencyAmount),
|
||||
[disabled, wrapType, optimizedTrade, chainId, inputCurrencyAmount, inputCurrencyBalance]
|
||||
)
|
||||
const actionProps = useMemo((): Partial<ActionButtonProps> | undefined => {
|
||||
if (disableSwap) {
|
||||
return { disabled: true }
|
||||
} else if (wrapType === WrapType.NONE) {
|
||||
return approvalAction
|
||||
? { action: approvalAction }
|
||||
: trade.state === TradeState.VALID
|
||||
? { onClick: () => setOpen(true) }
|
||||
: { disabled: true }
|
||||
} else {
|
||||
return isPending
|
||||
? { action: { message: <Trans>Confirm in your wallet</Trans>, icon: Spinner } }
|
||||
: { onClick: onWrap }
|
||||
}
|
||||
}, [approvalAction, disableSwap, isPending, onWrap, trade.state, wrapType])
|
||||
const Label = useCallback(() => {
|
||||
switch (wrapType) {
|
||||
case WrapType.UNWRAP:
|
||||
return <Trans>Unwrap {inputCurrency?.symbol}</Trans>
|
||||
case WrapType.WRAP:
|
||||
return <Trans>Wrap {inputCurrency?.symbol}</Trans>
|
||||
case WrapType.NONE:
|
||||
default:
|
||||
return <Trans>Review swap</Trans>
|
||||
}
|
||||
}, [inputCurrency?.symbol, wrapType])
|
||||
const onClose = useCallback(() => setOpen(false), [])
|
||||
|
||||
const { tokenColorExtraction } = useTheme()
|
||||
return (
|
||||
<>
|
||||
<ActionButton color={tokenColorExtraction ? 'interactive' : 'accent'} {...actionProps}>
|
||||
<Label />
|
||||
</ActionButton>
|
||||
{open && trade.trade && (
|
||||
<Dialog color="dialog" onClose={onClose}>
|
||||
<SummaryDialog
|
||||
trade={trade.trade}
|
||||
slippage={slippage}
|
||||
inputUSDC={inputUSDC}
|
||||
outputUSDC={outputUSDC}
|
||||
impact={impact}
|
||||
onConfirm={onSwap}
|
||||
/>
|
||||
</Dialog>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
})
|
||||
@@ -1,88 +0,0 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import { Currency, CurrencyAmount, Token } from '@uniswap/sdk-core'
|
||||
import { Action } from 'lib/components/ActionButton'
|
||||
import EtherscanLink from 'lib/components/EtherscanLink'
|
||||
import {
|
||||
ApproveOrPermitState,
|
||||
useApproveOrPermit,
|
||||
useSwapApprovalOptimizedTrade,
|
||||
useSwapRouterAddress,
|
||||
} from 'lib/hooks/swap/useSwapApproval'
|
||||
import { useAddTransaction, usePendingApproval } from 'lib/hooks/transactions'
|
||||
import { Slippage } from 'lib/hooks/useSlippage'
|
||||
import { Spinner } from 'lib/icons'
|
||||
import { TransactionType } from 'lib/state/transactions'
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import { ExplorerDataType } from 'utils/getExplorerLink'
|
||||
|
||||
export function useIsPendingApproval(token?: Token, spender?: string): boolean {
|
||||
return Boolean(usePendingApproval(token, spender))
|
||||
}
|
||||
|
||||
export default function useApprovalData(
|
||||
trade: ReturnType<typeof useSwapApprovalOptimizedTrade>,
|
||||
slippage: Slippage,
|
||||
currencyAmount?: CurrencyAmount<Currency>
|
||||
) {
|
||||
const currency = currencyAmount?.currency
|
||||
const { approvalState, signatureData, handleApproveOrPermit } = useApproveOrPermit(
|
||||
trade,
|
||||
slippage.allowed,
|
||||
useIsPendingApproval,
|
||||
currencyAmount
|
||||
)
|
||||
|
||||
const [isPending, setIsPending] = useState(false)
|
||||
const addTransaction = useAddTransaction()
|
||||
const onApprove = useCallback(async () => {
|
||||
setIsPending(true)
|
||||
const transaction = await handleApproveOrPermit()
|
||||
if (transaction) {
|
||||
addTransaction({ type: TransactionType.APPROVAL, ...transaction })
|
||||
}
|
||||
setIsPending(false)
|
||||
}, [addTransaction, handleApproveOrPermit])
|
||||
// Reset the pending state if currency changes.
|
||||
useEffect(() => setIsPending(false), [currency])
|
||||
|
||||
const approvalHash = usePendingApproval(currency?.isToken ? currency : undefined, useSwapRouterAddress(trade))
|
||||
const approvalAction = useMemo((): Action | undefined => {
|
||||
if (!trade || !currency) return
|
||||
|
||||
switch (approvalState) {
|
||||
case ApproveOrPermitState.REQUIRES_APPROVAL:
|
||||
if (isPending) {
|
||||
return { message: <Trans>Approve in your wallet</Trans>, icon: Spinner }
|
||||
}
|
||||
return {
|
||||
message: <Trans>Approve {currency.symbol} first</Trans>,
|
||||
onClick: onApprove,
|
||||
children: <Trans>Approve</Trans>,
|
||||
}
|
||||
case ApproveOrPermitState.REQUIRES_SIGNATURE:
|
||||
if (isPending) {
|
||||
return { message: <Trans>Allow in your wallet</Trans>, icon: Spinner }
|
||||
}
|
||||
return {
|
||||
message: <Trans>Allow {currency.symbol} first</Trans>,
|
||||
onClick: onApprove,
|
||||
children: <Trans>Allow</Trans>,
|
||||
}
|
||||
case ApproveOrPermitState.PENDING_APPROVAL:
|
||||
return {
|
||||
message: (
|
||||
<EtherscanLink type={ExplorerDataType.TRANSACTION} data={approvalHash}>
|
||||
<Trans>Approval pending</Trans>
|
||||
</EtherscanLink>
|
||||
),
|
||||
icon: Spinner,
|
||||
}
|
||||
case ApproveOrPermitState.PENDING_SIGNATURE:
|
||||
return { message: <Trans>Allowance pending</Trans>, icon: Spinner }
|
||||
case ApproveOrPermitState.APPROVED:
|
||||
return
|
||||
}
|
||||
}, [approvalHash, approvalState, currency, isPending, onApprove, trade])
|
||||
|
||||
return { approvalAction, signatureData: signatureData ?? undefined }
|
||||
}
|
||||