Compare commits
107 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
75ecc5810e | ||
|
|
c30eb89725 | ||
|
|
108feace02 | ||
|
|
e2c013a4d8 | ||
|
|
66308257d6 | ||
|
|
fd160531cc | ||
|
|
da36e638c2 | ||
|
|
fad55b8dbc | ||
|
|
c9c59698de | ||
|
|
828967031f | ||
|
|
440ac0cba0 | ||
|
|
b5a72cd63b | ||
|
|
37f273aab4 | ||
|
|
3acd993ec0 | ||
|
|
58778b5775 | ||
|
|
5bc21bebc3 | ||
|
|
c3d6727438 | ||
|
|
290f4bc1cb | ||
|
|
f95275d5ac | ||
|
|
0ec2dd4173 | ||
|
|
3b3db6f6d0 | ||
|
|
707abd0071 | ||
|
|
2efc1fb372 | ||
|
|
55b37825f3 | ||
|
|
bb27b7a2ef | ||
|
|
c595ba951b | ||
|
|
96a122d7b8 | ||
|
|
610f7d3581 | ||
|
|
781e774ce7 | ||
|
|
2aa1e40481 | ||
|
|
1c278d5012 | ||
|
|
a323a5c48b | ||
|
|
43931dd689 | ||
|
|
efa3d5529c | ||
|
|
5c0246cfc6 | ||
|
|
ee32418ff8 | ||
|
|
6e22389791 | ||
|
|
8064dd8ede | ||
|
|
921310ef52 | ||
|
|
7b90fe137e | ||
|
|
05b2711a8a | ||
|
|
d060782242 | ||
|
|
e19e8492c9 | ||
|
|
800b5e0bda | ||
|
|
fc637071f9 | ||
|
|
1b78ceec10 | ||
|
|
e5be3ebf8f | ||
|
|
1c73719766 | ||
|
|
14c91f9bba | ||
|
|
4b762ef5c9 | ||
|
|
c82b4fae64 | ||
|
|
ab8c1e3e90 | ||
|
|
7055d60406 | ||
|
|
c641cec651 | ||
|
|
b6a47c734f | ||
|
|
7aecf5d398 | ||
|
|
5bf2b81743 | ||
|
|
ed247065a7 | ||
|
|
0d0ad633fb | ||
|
|
4a8f1d9b96 | ||
|
|
043fb95d22 | ||
|
|
06536bc925 | ||
|
|
a598a15799 | ||
|
|
b0265c081e | ||
|
|
47aff6ff74 | ||
|
|
56717005e6 | ||
|
|
b50d10cbb2 | ||
|
|
ce96873a72 | ||
|
|
779625a04e | ||
|
|
d1e0812684 | ||
|
|
98e62b4f93 | ||
|
|
9fb0d424c2 | ||
|
|
8d145b908e | ||
|
|
c7633d910b | ||
|
|
1f89a46a3f | ||
|
|
8d54b01878 | ||
|
|
ffe334ccbf | ||
|
|
ffe2bd315e | ||
|
|
cee4b8c77a | ||
|
|
3153db9f73 | ||
|
|
bbdb5f3f56 | ||
|
|
7f9c56b68c | ||
|
|
2b69974fdc | ||
|
|
5236065769 | ||
|
|
52128a2dcd | ||
|
|
c9642c6cd0 | ||
|
|
b878d764e5 | ||
|
|
6a4f067ac0 | ||
|
|
e9407bb6bd | ||
|
|
8d822fd0e0 | ||
|
|
6404ee6e0b | ||
|
|
8ac3ed1128 | ||
|
|
b501974a76 | ||
|
|
567fb0181c | ||
|
|
8a37c427e6 | ||
|
|
034b3e3e58 | ||
|
|
053000e5fc | ||
|
|
b77e7deb49 | ||
|
|
c3321ae793 | ||
|
|
5dec0cf72b | ||
|
|
1efda07e7a | ||
|
|
fd819260f9 | ||
|
|
8e3b2cb4b8 | ||
|
|
d54783a324 | ||
|
|
850a20f6ad | ||
|
|
99f681818f | ||
|
|
1127e74357 |
5
.gitignore
vendored
5
.gitignore
vendored
@@ -17,11 +17,10 @@
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# production
|
||||
# builds
|
||||
/build
|
||||
|
||||
# widgets
|
||||
/dist
|
||||
/dts
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
|
||||
@@ -1,14 +1,26 @@
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
/* 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) =>
|
||||
plugin instanceof HtmlWebpackPlugin
|
||||
? new HtmlWebpackPlugin({
|
||||
templateContent: '<body></body>',
|
||||
})
|
||||
: plugin
|
||||
),
|
||||
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
|
||||
}),
|
||||
})
|
||||
|
||||
83
package.json
83
package.json
@@ -1,16 +1,20 @@
|
||||
{
|
||||
"name": "@uniswap/interface",
|
||||
"name": "@uniswap/widgets",
|
||||
"version": "0.0.10-beta",
|
||||
"description": "Uniswap Interface",
|
||||
"homepage": ".",
|
||||
"main": "dist/interface.js",
|
||||
"module": "dist/interface.esm.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"main": "dist/widgets.js",
|
||||
"module": "dist/widgets.esm.js",
|
||||
"types": "dist/widgets.d.ts",
|
||||
"files": [
|
||||
"lib",
|
||||
"dist"
|
||||
],
|
||||
"private": true,
|
||||
"devDependencies": {
|
||||
"@babel/plugin-transform-runtime": "^7.17.0",
|
||||
"@babel/preset-env": "^7.16.11",
|
||||
"@babel/preset-react": "^7.16.7",
|
||||
"@babel/preset-typescript": "^7.16.7",
|
||||
"@ethersproject/experimental": "^5.4.0",
|
||||
"@gnosis.pm/safe-apps-web3-react": "^0.6.0",
|
||||
"@graphql-codegen/cli": "1.21.5",
|
||||
@@ -22,10 +26,14 @@
|
||||
"@reach/dialog": "^0.10.3",
|
||||
"@reach/portal": "^0.10.3",
|
||||
"@react-hook/window-scroll": "^1.3.0",
|
||||
"@reduxjs/toolkit": "^1.6.1",
|
||||
"@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",
|
||||
@@ -56,22 +64,24 @@
|
||||
"@types/wcag-contrast": "^3.0.0",
|
||||
"@typescript-eslint/eslint-plugin": "^4.1.0",
|
||||
"@typescript-eslint/parser": "^4.1.0",
|
||||
"@uniswap/default-token-list": "^3.0.0",
|
||||
"@uniswap/governance": "^1.0.2",
|
||||
"@uniswap/liquidity-staker": "^1.0.2",
|
||||
"@uniswap/merkle-distributor": "1.0.1",
|
||||
"@uniswap/router-sdk": "^1.0.3",
|
||||
"@uniswap/smart-order-router": "^2.5.10",
|
||||
"@uniswap/v2-core": "1.0.0",
|
||||
"@uniswap/v2-periphery": "^1.1.0-beta.0",
|
||||
"@uniswap/v3-core": "1.0.0",
|
||||
"@uniswap/v3-periphery": "^1.1.1",
|
||||
"@web3-react/fortmatic-connector": "^6.0.9",
|
||||
"@web3-react/injected-connector": "^6.0.7",
|
||||
"@web3-react/portis-connector": "^6.0.9",
|
||||
"@web3-react/walletconnect-connector": "^7.0.2-alpha.0",
|
||||
"@web3-react/walletlink-connector": "^6.2.8",
|
||||
"web3-react-abstract-connector": "npm:@web3-react/abstract-connector@^6.0.7",
|
||||
"web3-react-fortmatic-connector": "npm:@web3-react/fortmatic-connector@^6.0.9",
|
||||
"web3-react-injected-connector": "npm:@web3-react/injected-connector@^6.0.7",
|
||||
"web3-react-portis-connector": "npm:@web3-react/portis-connector@^6.0.9",
|
||||
"web3-react-types": "npm:@web3-react/types@^6.0.7",
|
||||
"web3-react-walletconnect-connector": "npm:@web3-react/walletconnect-connector@^7.0.2-alpha.0",
|
||||
"web3-react-walletlink-connector": "npm:@web3-react/walletlink-connector@^6.2.11",
|
||||
"array.prototype.flat": "^1.2.4",
|
||||
"array.prototype.flatmap": "^1.2.4",
|
||||
"babel-plugin-macros": "^3.1.0",
|
||||
"copy-to-clipboard": "^3.2.0",
|
||||
"cross-env": "^7.0.3",
|
||||
"cypress": "^7.7.0",
|
||||
@@ -89,12 +99,13 @@
|
||||
"graphql-request": "^3.4.0",
|
||||
"inter-ui": "^3.13.1",
|
||||
"jest-styled-components": "^7.0.5",
|
||||
"ms.macro": "^2.0.0",
|
||||
"polyfill-object.fromentries": "^1.0.1",
|
||||
"prettier": "^2.2.1",
|
||||
"qs": "^6.9.4",
|
||||
"react": "^17.0.1",
|
||||
"react-confetti": "^6.0.0",
|
||||
"react-cosmos": "^5.6.3",
|
||||
"react-cosmos": "^5.6.6",
|
||||
"react-dom": "^17.0.1",
|
||||
"react-ga": "^2.5.7",
|
||||
"react-is": "^17.0.2",
|
||||
"react-markdown": "^4.3.1",
|
||||
@@ -104,14 +115,17 @@
|
||||
"react-use-gesture": "^6.0.14",
|
||||
"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-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",
|
||||
"typechain": "^5.0.0",
|
||||
"typescript": "^4.2.3",
|
||||
"typescript": "^4.4.3",
|
||||
"ua-parser-js": "^0.7.28",
|
||||
"use-count-up": "^2.2.5",
|
||||
"use-resize-observer": "^8.0.0",
|
||||
@@ -133,12 +147,13 @@
|
||||
"i18n:extract": "lingui extract --locale en-US",
|
||||
"i18n:compile": "yarn i18n:extract && lingui compile",
|
||||
"i18n:pseudo": "lingui extract --locale pseudo && lingui compile",
|
||||
"postinstall": "yarn contracts:compile && yarn graphql:generate && yarn i18n: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.js",
|
||||
"test:e2e": "start-server-and-test 'serve build -l 3000' http://localhost:3000 'cypress run --record'",
|
||||
"widgets:start": "cross-env FAST_REFRESH=false REACT_APP_IS_WIDGET=true cosmos",
|
||||
"widgets:start": "cosmos",
|
||||
"widgets:build": "rollup --config --failAfterWarnings --configPlugin typescript2"
|
||||
},
|
||||
"browserslist": {
|
||||
@@ -155,6 +170,7 @@
|
||||
},
|
||||
"license": "GPL-3.0-or-later",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.17.0",
|
||||
"@ethersproject/abi": "^5.4.1",
|
||||
"@ethersproject/abstract-provider": "^5.4.1",
|
||||
"@ethersproject/address": "^5.4.0",
|
||||
@@ -163,7 +179,7 @@
|
||||
"@ethersproject/constants": "^5.4.0",
|
||||
"@ethersproject/contracts": "^5.4.1",
|
||||
"@ethersproject/hash": "^5.4.0",
|
||||
"@ethersproject/providers": "^5.4.5",
|
||||
"@ethersproject/providers": "5.4.0",
|
||||
"@ethersproject/solidity": "^5.4.0",
|
||||
"@ethersproject/strings": "^5.4.0",
|
||||
"@ethersproject/units": "^5.4.0",
|
||||
@@ -174,25 +190,27 @@
|
||||
"@lingui/macro": "^3.9.0",
|
||||
"@lingui/react": "^3.9.0",
|
||||
"@popperjs/core": "^2.4.4",
|
||||
"@reduxjs/toolkit": "^1.6.1",
|
||||
"@uniswap/redux-multicall": "^1.0.0",
|
||||
"@uniswap/router-sdk": "^1.0.3",
|
||||
"@uniswap/sdk-core": "^3.0.1",
|
||||
"@uniswap/smart-order-router": "^2.5.10",
|
||||
"@uniswap/token-lists": "^1.0.0-beta.27",
|
||||
"@uniswap/v2-sdk": "^3.0.1",
|
||||
"@uniswap/v3-sdk": "^3.7.1",
|
||||
"@web3-react/core": "^6.0.9",
|
||||
"@uniswap/v3-sdk": "^3.8.2",
|
||||
"web3-react-core": "npm:@web3-react/core@^6.0.9",
|
||||
"ajv": "^6.12.3",
|
||||
"cids": "^1.0.0",
|
||||
"immer": "^9.0.6",
|
||||
"jotai": "^1.3.7",
|
||||
"jsbi": "^3.1.4",
|
||||
"make-plural": "^7.0.0",
|
||||
"ms.macro": "^2.0.0",
|
||||
"multicodec": "^3.0.1",
|
||||
"multihashes": "^4.0.2",
|
||||
"node-vibrant": "^3.2.1-alpha.1",
|
||||
"polished": "^3.3.2",
|
||||
"popper-max-size-modifier": "^0.2.0",
|
||||
"react": "^17.0.1",
|
||||
"react-dom": "^17.0.1",
|
||||
"react-feather": "^2.0.8",
|
||||
"react-popper": "^2.2.3",
|
||||
"react-redux": "^7.2.2",
|
||||
@@ -203,12 +221,17 @@
|
||||
"styled-components": "^5.3.0",
|
||||
"tiny-invariant": "^1.2.0",
|
||||
"wcag-contrast": "^3.0.0",
|
||||
"wicg-inert": "^3.1.1",
|
||||
"widgets-web3-react/core": "npm:@web3-react/core@8.0.16-alpha.0",
|
||||
"widgets-web3-react/eip1193": "npm:@web3-react/eip1193@8.0.16-alpha.0",
|
||||
"widgets-web3-react/empty": "npm:@web3-react/empty@8.0.17-alpha.0",
|
||||
"widgets-web3-react/metamask": "npm:@web3-react/metamask@8.0.16-alpha.0",
|
||||
"widgets-web3-react/types": "npm:@web3-react/types@8.0.16-alpha.0",
|
||||
"widgets-web3-react/url": "npm:@web3-react/url@8.0.17-alpha.0"
|
||||
"@web3-react/core": "8.0.16-alpha.0",
|
||||
"@web3-react/eip1193": "8.0.16-alpha.0",
|
||||
"@web3-react/empty": "8.0.17-alpha.0",
|
||||
"@web3-react/metamask": "8.0.16-alpha.0",
|
||||
"@web3-react/types": "8.0.16-alpha.0",
|
||||
"@web3-react/url": "8.0.17-alpha.0",
|
||||
"wicg-inert": "^3.1.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@babel/runtime": "^7.17.0",
|
||||
"react": "^17.0.1",
|
||||
"react-dom": "^17.0.1"
|
||||
}
|
||||
}
|
||||
|
||||
126
rollup.config.ts
126
rollup.config.ts
@@ -3,61 +3,125 @@
|
||||
* This library lives in src/lib, but shares code with the interface application.
|
||||
*/
|
||||
|
||||
import eslint from '@rollup/plugin-eslint'
|
||||
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'
|
||||
import externals from 'rollup-plugin-node-externals'
|
||||
import sass from 'rollup-plugin-scss'
|
||||
import typescript from 'rollup-plugin-typescript2'
|
||||
import { CompilerOptions } from 'typescript'
|
||||
|
||||
import { dependencies } from './package.json'
|
||||
|
||||
const deps = Object.keys(dependencies)
|
||||
|
||||
const replacements = {
|
||||
const REPLACEMENTS = {
|
||||
'process.env.REACT_APP_IS_WIDGET': true,
|
||||
'process.env.REACT_APP_LOCALES': "'./locales'",
|
||||
}
|
||||
|
||||
const library = {
|
||||
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)
|
||||
}
|
||||
|
||||
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
|
||||
externals({ exclude: ['constants'], deps: true, peerDeps: true }), // marks builtins, dependencies, and peerDependencies external
|
||||
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; doing so type-checking allows the json to be type-checked
|
||||
]
|
||||
|
||||
const check = {
|
||||
input: 'src/lib/index.tsx',
|
||||
output: { file: 'dist/widgets.tsc' },
|
||||
external: isAsset,
|
||||
plugins: [...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/widgets.d.ts' },
|
||||
external: isAsset,
|
||||
plugins: [
|
||||
dts({ compilerOptions: { baseUrl: 'dist/dts' } }),
|
||||
del({ hook: 'buildEnd', targets: ['dist/widgets.tsc', 'dist/dts'] }),
|
||||
],
|
||||
}
|
||||
|
||||
const transpile = {
|
||||
input: 'src/lib/index.tsx',
|
||||
output: [
|
||||
{
|
||||
file: 'dist/widgets.js',
|
||||
format: 'cjs',
|
||||
inlineDynamicImports: true,
|
||||
sourcemap: true,
|
||||
sourcemap: false,
|
||||
},
|
||||
{
|
||||
file: 'dist/widgets.esm.js',
|
||||
format: 'esm',
|
||||
inlineDynamicImports: true,
|
||||
sourcemap: true,
|
||||
sourcemap: false,
|
||||
},
|
||||
],
|
||||
// necessary because some nested imports (eg jotai/*) would otherwise not resolve.
|
||||
external: (source: string) => Boolean(deps.find((dep) => source === dep || source.startsWith(dep + '/'))),
|
||||
plugins: [
|
||||
eslint({ include: ['**/*.js', '**/*.jsx', '**/*.ts', '**/*.tsx'] }),
|
||||
json(), // imports json
|
||||
replace({ ...replacements, preventAssignment: true }),
|
||||
url(), // imports files (including svgs) as data URIs
|
||||
...plugins,
|
||||
|
||||
// Source code transformation
|
||||
url({ include: ASSET_EXTENSIONS.map((extname) => '**/*' + extname) }), // imports assets as data URIs
|
||||
svgr({ exportType: 'named', svgo: false }), // imports svgs as React components
|
||||
sass(), // imports sass styles
|
||||
typescript({ tsconfig: './tsconfig.lib.json', useTsconfigDeclarationDir: true }),
|
||||
sass({ insert: true }), // imports inlined sass styles
|
||||
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
|
||||
],
|
||||
}),
|
||||
|
||||
copy({
|
||||
copyOnce: true,
|
||||
targets: [{ src: 'src/locales/*.js', dest: 'dist/locales' }],
|
||||
}),
|
||||
],
|
||||
onwarn: squelchTypeWarnings, // this pipeline is only for transpilation
|
||||
}
|
||||
|
||||
const typings = {
|
||||
input: 'dist/dts/lib/index.d.ts',
|
||||
output: {
|
||||
file: 'dist/widgets.d.ts',
|
||||
format: 'es',
|
||||
},
|
||||
external: (source: string) => source.endsWith('.scss'),
|
||||
plugins: [dts({ compilerOptions: { baseUrl: 'dist/dts' } })],
|
||||
}
|
||||
|
||||
const config = [library, typings]
|
||||
const config = [check, type, transpile]
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import { AbstractConnector } from '@web3-react/abstract-connector'
|
||||
import { Connector } from '@web3-react/types'
|
||||
import useActiveWeb3React from 'hooks/useActiveWeb3React'
|
||||
import { useCallback, useContext } from 'react'
|
||||
import { ExternalLink as LinkIcon } from 'react-feather'
|
||||
import { useAppDispatch } from 'state/hooks'
|
||||
import styled, { ThemeContext } from 'styled-components/macro'
|
||||
import { Connector } from 'widgets-web3-react/types'
|
||||
import { AbstractConnector } from 'web3-react-abstract-connector'
|
||||
|
||||
import { ReactComponent as Close } from '../../assets/images/x.svg'
|
||||
import { injected, portis, walletlink } from '../../connectors'
|
||||
|
||||
@@ -31,6 +31,8 @@ const BLOCKED_ADDRESSES: string[] = [
|
||||
'0x6acdfba02d390b97ac2b2d42a63e85293bcc160e',
|
||||
'0x48549a34ae37b12f6a30566245176994e17c6b4a',
|
||||
'0x5512d943ed1f7c8a43f3435c85f7ab68b30121b0',
|
||||
'0xc455f7fd3e0e12afd51fba5c106909934d8a0e4a',
|
||||
'0x629e7Da20197a5429d30da36E77d06CdF796b71A',
|
||||
]
|
||||
|
||||
export default function Blocklist({ children }: { children: ReactNode }) {
|
||||
|
||||
@@ -1,60 +1,18 @@
|
||||
import { Currency } from '@uniswap/sdk-core'
|
||||
import EthereumLogo from 'assets/images/ethereum-logo.png'
|
||||
import MaticLogo from 'assets/svg/matic-token-icon.svg'
|
||||
import { SupportedChainId } from 'constants/chains'
|
||||
import useHttpLocations from 'hooks/useHttpLocations'
|
||||
import React, { useMemo } from 'react'
|
||||
import { WrappedTokenInfo } from 'state/lists/wrappedTokenInfo'
|
||||
import useCurrencyLogoURIs from 'lib/hooks/useCurrencyLogoURIs'
|
||||
import React from 'react'
|
||||
import styled from 'styled-components/macro'
|
||||
|
||||
import Logo from '../Logo'
|
||||
|
||||
type Network = 'ethereum' | 'arbitrum' | 'optimism'
|
||||
|
||||
function chainIdToNetworkName(networkId: SupportedChainId): Network {
|
||||
switch (networkId) {
|
||||
case SupportedChainId.MAINNET:
|
||||
return 'ethereum'
|
||||
case SupportedChainId.ARBITRUM_ONE:
|
||||
return 'arbitrum'
|
||||
case SupportedChainId.OPTIMISM:
|
||||
return 'optimism'
|
||||
default:
|
||||
return 'ethereum'
|
||||
}
|
||||
}
|
||||
|
||||
export const getTokenLogoURL = (
|
||||
address: string,
|
||||
chainId: SupportedChainId = SupportedChainId.MAINNET
|
||||
): string | void => {
|
||||
const networkName = chainIdToNetworkName(chainId)
|
||||
const networksWithUrls = [SupportedChainId.ARBITRUM_ONE, SupportedChainId.MAINNET, SupportedChainId.OPTIMISM]
|
||||
if (networksWithUrls.includes(chainId)) {
|
||||
return `https://raw.githubusercontent.com/Uniswap/assets/master/blockchains/${networkName}/assets/${address}/logo.png`
|
||||
}
|
||||
}
|
||||
|
||||
const StyledNativeLogo = styled.img<{ size: string }>`
|
||||
width: ${({ size }) => size};
|
||||
height: ${({ size }) => size};
|
||||
background: radial-gradient(white 50%, #ffffff00 calc(75% + 1px), #ffffff00 100%);
|
||||
|
||||
border-radius: 50%;
|
||||
-mox-box-shadow: 0 0 1px white;
|
||||
-webkit-box-shadow: 0 0 1px white;
|
||||
box-shadow: 0 0 1px white;
|
||||
border: 0px solid rgba(255, 255, 255, 0);
|
||||
`
|
||||
|
||||
const StyledLogo = styled(Logo)<{ size: string }>`
|
||||
const StyledLogo = styled(Logo)<{ size: string; native: boolean }>`
|
||||
width: ${({ size }) => size};
|
||||
height: ${({ size }) => size};
|
||||
background: radial-gradient(white 50%, #ffffff00 calc(75% + 1px), #ffffff00 100%);
|
||||
border-radius: 50%;
|
||||
-mox-box-shadow: 0 0 1px black;
|
||||
-webkit-box-shadow: 0 0 1px black;
|
||||
box-shadow: 0 0 1px black;
|
||||
-mox-box-shadow: 0 0 1px ${({ native }) => (native ? 'white' : 'black')};
|
||||
-webkit-box-shadow: 0 0 1px ${({ native }) => (native ? 'white' : 'black')};
|
||||
box-shadow: 0 0 1px ${({ native }) => (native ? 'white' : 'black')};
|
||||
border: 0px solid rgba(255, 255, 255, 0);
|
||||
`
|
||||
|
||||
@@ -68,38 +26,16 @@ export default function CurrencyLogo({
|
||||
size?: string
|
||||
style?: React.CSSProperties
|
||||
}) {
|
||||
const uriLocations = useHttpLocations(currency instanceof WrappedTokenInfo ? currency.logoURI : undefined)
|
||||
const logoURIs = useCurrencyLogoURIs(currency)
|
||||
|
||||
const srcs: string[] = useMemo(() => {
|
||||
if (!currency || currency.isNative) return []
|
||||
|
||||
if (currency.isToken) {
|
||||
const defaultUrls = []
|
||||
const url = getTokenLogoURL(currency.address, currency.chainId)
|
||||
if (url) {
|
||||
defaultUrls.push(url)
|
||||
}
|
||||
if (currency instanceof WrappedTokenInfo) {
|
||||
return [...uriLocations, ...defaultUrls]
|
||||
}
|
||||
return defaultUrls
|
||||
}
|
||||
return []
|
||||
}, [currency, uriLocations])
|
||||
|
||||
if (currency?.isNative) {
|
||||
let nativeLogoUrl: string
|
||||
switch (currency.chainId) {
|
||||
case SupportedChainId.POLYGON_MUMBAI:
|
||||
case SupportedChainId.POLYGON:
|
||||
nativeLogoUrl = MaticLogo
|
||||
break
|
||||
default:
|
||||
nativeLogoUrl = EthereumLogo
|
||||
break
|
||||
}
|
||||
return <StyledNativeLogo src={nativeLogoUrl} alt="ethereum logo" size={size} style={style} {...rest} />
|
||||
}
|
||||
|
||||
return <StyledLogo size={size} srcs={srcs} alt={`${currency?.symbol ?? 'token'} logo`} style={style} {...rest} />
|
||||
return (
|
||||
<StyledLogo
|
||||
size={size}
|
||||
native={currency?.isNative ?? false}
|
||||
srcs={logoURIs}
|
||||
alt={`${currency?.symbol ?? 'token'} logo`}
|
||||
style={style}
|
||||
{...rest}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,14 +1,19 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import { CHAIN_INFO } from 'constants/chainInfo'
|
||||
import { SupportedChainId } from 'constants/chains'
|
||||
import { CHAIN_IDS_TO_NAMES, SupportedChainId } from 'constants/chains'
|
||||
import useActiveWeb3React from 'hooks/useActiveWeb3React'
|
||||
import { useOnClickOutside } from 'hooks/useOnClickOutside'
|
||||
import { useCallback, useRef } from 'react'
|
||||
import useParsedQueryString from 'hooks/useParsedQueryString'
|
||||
import usePrevious from 'hooks/usePrevious'
|
||||
import { ParsedQs } from 'qs'
|
||||
import { useCallback, useEffect, useRef } from 'react'
|
||||
import { ArrowDownCircle, ChevronDown } from 'react-feather'
|
||||
import { useHistory } from 'react-router-dom'
|
||||
import { useModalOpen, useToggleModal } from 'state/application/hooks'
|
||||
import { addPopup, ApplicationModal } from 'state/application/reducer'
|
||||
import styled from 'styled-components/macro'
|
||||
import { ExternalLink, MEDIA_WIDTHS } from 'theme'
|
||||
import { replaceURLParam } from 'utils/routes'
|
||||
|
||||
import { useAppDispatch } from '../../state/hooks'
|
||||
import { switchToNetwork } from '../../utils/switchToNetwork'
|
||||
@@ -211,31 +216,90 @@ function Row({
|
||||
return rowContent
|
||||
}
|
||||
|
||||
const getParsedChainId = (parsedQs?: ParsedQs) => {
|
||||
const chain = parsedQs?.chain
|
||||
if (!chain || typeof chain !== 'string') return { urlChain: undefined, urlChainId: undefined }
|
||||
|
||||
return { urlChain: chain.toLowerCase(), urlChainId: getChainIdFromName(chain) }
|
||||
}
|
||||
|
||||
const getChainIdFromName = (name: string) => {
|
||||
const entry = Object.entries(CHAIN_IDS_TO_NAMES).find(([_, n]) => n === name)
|
||||
const chainId = entry?.[0]
|
||||
return chainId ? parseInt(chainId) : undefined
|
||||
}
|
||||
|
||||
const getChainNameFromId = (id: string | number) => {
|
||||
// casting here may not be right but fine to return undefined if it's not a supported chain ID
|
||||
return CHAIN_IDS_TO_NAMES[id as SupportedChainId] || ''
|
||||
}
|
||||
|
||||
export default function NetworkSelector() {
|
||||
const { chainId, library } = useActiveWeb3React()
|
||||
const parsedQs = useParsedQueryString()
|
||||
const { urlChain, urlChainId } = getParsedChainId(parsedQs)
|
||||
const prevChainId = usePrevious(chainId)
|
||||
const node = useRef<HTMLDivElement>()
|
||||
const open = useModalOpen(ApplicationModal.NETWORK_SELECTOR)
|
||||
const toggle = useToggleModal(ApplicationModal.NETWORK_SELECTOR)
|
||||
useOnClickOutside(node, open ? toggle : undefined)
|
||||
|
||||
const history = useHistory()
|
||||
|
||||
const info = chainId ? CHAIN_INFO[chainId] : undefined
|
||||
|
||||
const dispatch = useAppDispatch()
|
||||
|
||||
const handleRowClick = useCallback(
|
||||
(targetChain: number) => {
|
||||
const handleChainSwitch = useCallback(
|
||||
(targetChain: number, skipToggle?: boolean) => {
|
||||
if (!library) return
|
||||
switchToNetwork({ library, chainId: targetChain })
|
||||
.then(() => toggle())
|
||||
.then(() => {
|
||||
if (!skipToggle) {
|
||||
toggle()
|
||||
}
|
||||
history.replace({
|
||||
search: replaceURLParam(history.location.search, 'chain', getChainNameFromId(targetChain)),
|
||||
})
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to switch networks', error)
|
||||
toggle()
|
||||
|
||||
// we want app network <-> chainId param to be in sync, so if user changes the network by changing the URL
|
||||
// but the request fails, revert the URL back to current chainId
|
||||
if (chainId) {
|
||||
history.replace({ search: replaceURLParam(history.location.search, 'chain', getChainNameFromId(chainId)) })
|
||||
}
|
||||
|
||||
if (!skipToggle) {
|
||||
toggle()
|
||||
}
|
||||
|
||||
dispatch(addPopup({ content: { failedSwitchNetwork: targetChain }, key: `failed-network-switch` }))
|
||||
})
|
||||
},
|
||||
[dispatch, library, toggle]
|
||||
[dispatch, library, toggle, history, chainId]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (!chainId || !prevChainId) return
|
||||
|
||||
// when network change originates from wallet or dropdown selector, just update URL
|
||||
if (chainId !== prevChainId) {
|
||||
history.replace({ search: replaceURLParam(history.location.search, 'chain', getChainNameFromId(chainId)) })
|
||||
// otherwise assume network change originates from URL
|
||||
} else if (urlChainId && urlChainId !== chainId) {
|
||||
handleChainSwitch(urlChainId, true)
|
||||
}
|
||||
}, [chainId, urlChainId, prevChainId, handleChainSwitch, history])
|
||||
|
||||
// set chain parameter on initial load if not there
|
||||
useEffect(() => {
|
||||
if (chainId && !urlChainId) {
|
||||
history.replace({ search: replaceURLParam(history.location.search, 'chain', getChainNameFromId(chainId)) })
|
||||
}
|
||||
}, [chainId, history, urlChainId, urlChain])
|
||||
|
||||
if (!chainId || !info || !library) {
|
||||
return null
|
||||
}
|
||||
@@ -252,10 +316,10 @@ export default function NetworkSelector() {
|
||||
<FlyoutHeader>
|
||||
<Trans>Select a network</Trans>
|
||||
</FlyoutHeader>
|
||||
<Row onSelectChain={handleRowClick} targetChain={SupportedChainId.MAINNET} />
|
||||
<Row onSelectChain={handleRowClick} targetChain={SupportedChainId.POLYGON} />
|
||||
<Row onSelectChain={handleRowClick} targetChain={SupportedChainId.OPTIMISM} />
|
||||
<Row onSelectChain={handleRowClick} targetChain={SupportedChainId.ARBITRUM_ONE} />
|
||||
<Row onSelectChain={handleChainSwitch} targetChain={SupportedChainId.MAINNET} />
|
||||
<Row onSelectChain={handleChainSwitch} targetChain={SupportedChainId.POLYGON} />
|
||||
<Row onSelectChain={handleChainSwitch} targetChain={SupportedChainId.OPTIMISM} />
|
||||
<Row onSelectChain={handleChainSwitch} targetChain={SupportedChainId.ARBITRUM_ONE} />
|
||||
</FlyoutMenu>
|
||||
)}
|
||||
</SelectorWrapper>
|
||||
|
||||
@@ -264,9 +264,7 @@ export default function Header() {
|
||||
|
||||
const {
|
||||
infoLink,
|
||||
addNetworkInfo: {
|
||||
nativeCurrency: { symbol: nativeCurrencySymbol },
|
||||
},
|
||||
nativeCurrency: { symbol: nativeCurrencySymbol },
|
||||
} = CHAIN_INFO[chainId ? chainId : SupportedChainId.MAINNET]
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { AbstractConnector } from '@web3-react/abstract-connector'
|
||||
import { Connector } from 'widgets-web3-react/types'
|
||||
import { Connector } from '@web3-react/types'
|
||||
import { AbstractConnector } from 'web3-react-abstract-connector'
|
||||
|
||||
import CoinbaseWalletIcon from '../../assets/images/coinbaseWalletIcon.svg'
|
||||
import FortmaticIcon from '../../assets/images/fortmaticIcon.png'
|
||||
|
||||
@@ -1,16 +1,10 @@
|
||||
import { Currency } from '@uniswap/sdk-core'
|
||||
import { FeeAmount } from '@uniswap/v3-sdk'
|
||||
import { usePoolActiveLiquidity } from 'hooks/usePoolTickData'
|
||||
import JSBI from 'jsbi'
|
||||
import { TickProcessed, usePoolActiveLiquidity } from 'hooks/usePoolTickData'
|
||||
import { useCallback, useMemo } from 'react'
|
||||
|
||||
import { ChartEntry } from './types'
|
||||
|
||||
export interface TickProcessed {
|
||||
liquidityActive: JSBI
|
||||
price0: string
|
||||
}
|
||||
|
||||
export function useDensityChartData({
|
||||
currencyA,
|
||||
currencyB,
|
||||
|
||||
@@ -4,11 +4,10 @@ import styled from 'styled-components/macro'
|
||||
import { MEDIA_WIDTHS } from 'theme'
|
||||
|
||||
import { useActivePopups } from '../../state/application/hooks'
|
||||
import { useShowSurveyPopup, useURLWarningVisible } from '../../state/user/hooks'
|
||||
import { useURLWarningVisible } from '../../state/user/hooks'
|
||||
import { AutoColumn } from '../Column'
|
||||
import ClaimPopup from './ClaimPopup'
|
||||
import PopupItem from './PopupItem'
|
||||
import SurveyPopup from './SurveyPopup'
|
||||
|
||||
const MobilePopupWrapper = styled.div<{ height: string | number }>`
|
||||
position: relative;
|
||||
@@ -60,9 +59,6 @@ export default function Popups() {
|
||||
// get all popups
|
||||
const activePopups = useActivePopups()
|
||||
|
||||
// show survey popup if user has not closed
|
||||
const [showSurveyPopup] = useShowSurveyPopup()
|
||||
|
||||
const urlWarningActive = useURLWarningVisible()
|
||||
|
||||
// need extra padding if network is not L1 Ethereum
|
||||
@@ -73,14 +69,12 @@ export default function Popups() {
|
||||
<>
|
||||
<FixedPopupColumn gap="20px" extraPadding={urlWarningActive} xlPadding={isNotOnMainnet}>
|
||||
<ClaimPopup />
|
||||
<SurveyPopup />
|
||||
{activePopups.map((item) => (
|
||||
<PopupItem key={item.key} content={item.content} popKey={item.key} removeAfterMs={item.removeAfterMs} />
|
||||
))}
|
||||
</FixedPopupColumn>
|
||||
<MobilePopupWrapper height={activePopups?.length > 0 || showSurveyPopup ? 'fit-content' : 0}>
|
||||
<MobilePopupWrapper height={activePopups?.length > 0 ? 'fit-content' : 0}>
|
||||
<MobilePopupInner>
|
||||
<SurveyPopup />
|
||||
{activePopups // reverse so new items up front
|
||||
.slice(0)
|
||||
.reverse()
|
||||
|
||||
@@ -3,45 +3,45 @@
|
||||
exports[`renders multi route 1`] = `
|
||||
<DocumentFragment>
|
||||
<div
|
||||
class="RoutingDiagram__Wrapper-sc-o1ook0-0 ePDWDk css-vurnku"
|
||||
class="RoutingDiagram__Wrapper-sc-i2tbb-0 ivndgC css-vurnku"
|
||||
>
|
||||
<div
|
||||
class="sc-bdnxRM Row-sc-nrd8cx-0 RoutingDiagram__RouteContainerRow-sc-o1ook0-1 lmTMKd itvFNV ibRCpr"
|
||||
class="sc-bdnxRM Row-sc-u7azg8-0 RoutingDiagram__RouteContainerRow-sc-i2tbb-1 lmTMKd hLLNig hDkZVB"
|
||||
>
|
||||
CurrencyLogo currency=USDC
|
||||
<div
|
||||
class="sc-bdnxRM Row-sc-nrd8cx-0 RoutingDiagram__RouteRow-sc-o1ook0-2 lmTMKd itvFNV fzMiot"
|
||||
class="sc-bdnxRM Row-sc-u7azg8-0 RoutingDiagram__RouteRow-sc-i2tbb-2 lmTMKd hLLNig hUDqOH"
|
||||
>
|
||||
<div
|
||||
class="RoutingDiagram__DottedLine-sc-o1ook0-4 kkXINS"
|
||||
class="RoutingDiagram__DottedLine-sc-i2tbb-4 cKqYfU"
|
||||
>
|
||||
<svg
|
||||
class="RoutingDiagram__DotColor-sc-o1ook0-5 kgYqrO"
|
||||
class="RoutingDiagram__DotColor-sc-i2tbb-5 fhSaBA"
|
||||
>
|
||||
dot_line.svg
|
||||
</svg>
|
||||
</div>
|
||||
<div
|
||||
class="Badge-sc-1mhw5si-0 RoutingDiagram__OpaqueBadge-sc-o1ook0-6 gayll OurGh"
|
||||
class="Badge-sc-3epor3-0 RoutingDiagram__OpaqueBadge-sc-i2tbb-6 knpfHF gGARxH"
|
||||
>
|
||||
<div
|
||||
class="Badge-sc-1mhw5si-0 RoutingDiagram__ProtocolBadge-sc-o1ook0-7 gayll bNVqMw"
|
||||
class="Badge-sc-3epor3-0 RoutingDiagram__ProtocolBadge-sc-i2tbb-7 knpfHF lbdUti"
|
||||
>
|
||||
<div
|
||||
class="theme__TextWrapper-sc-18nh1jk-0 cWOfab RoutingDiagram__BadgeText-sc-o1ook0-8 dYpdfO css-15li2d9"
|
||||
class="theme__TextWrapper-sc-5lu8um-0 chxxqs RoutingDiagram__BadgeText-sc-i2tbb-8 ijjHig css-15li2d9"
|
||||
>
|
||||
V2
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="theme__TextWrapper-sc-18nh1jk-0 cWOfab RoutingDiagram__BadgeText-sc-o1ook0-8 dYpdfO css-1aekuku"
|
||||
class="theme__TextWrapper-sc-5lu8um-0 chxxqs RoutingDiagram__BadgeText-sc-i2tbb-8 ijjHig css-1aekuku"
|
||||
style="min-width: auto;"
|
||||
>
|
||||
75%
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="sc-bdnxRM Row-sc-nrd8cx-0 Row__AutoRow-sc-nrd8cx-3 iqvZFe itvFNV kkMfuq"
|
||||
class="sc-bdnxRM Row-sc-u7azg8-0 Row__AutoRow-sc-u7azg8-3 iqvZFe hLLNig cUhARX"
|
||||
style="justify-content: space-evenly; z-index: 2;"
|
||||
width="100%"
|
||||
>
|
||||
@@ -51,42 +51,42 @@ exports[`renders multi route 1`] = `
|
||||
CurrencyLogo currency=DAI
|
||||
</div>
|
||||
<div
|
||||
class="sc-bdnxRM Row-sc-nrd8cx-0 RoutingDiagram__RouteContainerRow-sc-o1ook0-1 lmTMKd itvFNV ibRCpr"
|
||||
class="sc-bdnxRM Row-sc-u7azg8-0 RoutingDiagram__RouteContainerRow-sc-i2tbb-1 lmTMKd hLLNig hDkZVB"
|
||||
>
|
||||
CurrencyLogo currency=USDC
|
||||
<div
|
||||
class="sc-bdnxRM Row-sc-nrd8cx-0 RoutingDiagram__RouteRow-sc-o1ook0-2 lmTMKd itvFNV fzMiot"
|
||||
class="sc-bdnxRM Row-sc-u7azg8-0 RoutingDiagram__RouteRow-sc-i2tbb-2 lmTMKd hLLNig hUDqOH"
|
||||
>
|
||||
<div
|
||||
class="RoutingDiagram__DottedLine-sc-o1ook0-4 kkXINS"
|
||||
class="RoutingDiagram__DottedLine-sc-i2tbb-4 cKqYfU"
|
||||
>
|
||||
<svg
|
||||
class="RoutingDiagram__DotColor-sc-o1ook0-5 kgYqrO"
|
||||
class="RoutingDiagram__DotColor-sc-i2tbb-5 fhSaBA"
|
||||
>
|
||||
dot_line.svg
|
||||
</svg>
|
||||
</div>
|
||||
<div
|
||||
class="Badge-sc-1mhw5si-0 RoutingDiagram__OpaqueBadge-sc-o1ook0-6 gayll OurGh"
|
||||
class="Badge-sc-3epor3-0 RoutingDiagram__OpaqueBadge-sc-i2tbb-6 knpfHF gGARxH"
|
||||
>
|
||||
<div
|
||||
class="Badge-sc-1mhw5si-0 RoutingDiagram__ProtocolBadge-sc-o1ook0-7 gayll bNVqMw"
|
||||
class="Badge-sc-3epor3-0 RoutingDiagram__ProtocolBadge-sc-i2tbb-7 knpfHF lbdUti"
|
||||
>
|
||||
<div
|
||||
class="theme__TextWrapper-sc-18nh1jk-0 cWOfab RoutingDiagram__BadgeText-sc-o1ook0-8 dYpdfO css-15li2d9"
|
||||
class="theme__TextWrapper-sc-5lu8um-0 chxxqs RoutingDiagram__BadgeText-sc-i2tbb-8 ijjHig css-15li2d9"
|
||||
>
|
||||
V3
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="theme__TextWrapper-sc-18nh1jk-0 cWOfab RoutingDiagram__BadgeText-sc-o1ook0-8 dYpdfO css-1aekuku"
|
||||
class="theme__TextWrapper-sc-5lu8um-0 chxxqs RoutingDiagram__BadgeText-sc-i2tbb-8 ijjHig css-1aekuku"
|
||||
style="min-width: auto;"
|
||||
>
|
||||
25%
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="sc-bdnxRM Row-sc-nrd8cx-0 Row__AutoRow-sc-nrd8cx-3 iqvZFe itvFNV kkMfuq"
|
||||
class="sc-bdnxRM Row-sc-u7azg8-0 Row__AutoRow-sc-u7azg8-3 iqvZFe hLLNig cUhARX"
|
||||
style="justify-content: space-evenly; z-index: 2;"
|
||||
width="100%"
|
||||
>
|
||||
@@ -102,45 +102,45 @@ exports[`renders multi route 1`] = `
|
||||
exports[`renders single route 1`] = `
|
||||
<DocumentFragment>
|
||||
<div
|
||||
class="RoutingDiagram__Wrapper-sc-o1ook0-0 ePDWDk css-vurnku"
|
||||
class="RoutingDiagram__Wrapper-sc-i2tbb-0 ivndgC css-vurnku"
|
||||
>
|
||||
<div
|
||||
class="sc-bdnxRM Row-sc-nrd8cx-0 RoutingDiagram__RouteContainerRow-sc-o1ook0-1 lmTMKd itvFNV ibRCpr"
|
||||
class="sc-bdnxRM Row-sc-u7azg8-0 RoutingDiagram__RouteContainerRow-sc-i2tbb-1 lmTMKd hLLNig hDkZVB"
|
||||
>
|
||||
CurrencyLogo currency=USDC
|
||||
<div
|
||||
class="sc-bdnxRM Row-sc-nrd8cx-0 RoutingDiagram__RouteRow-sc-o1ook0-2 lmTMKd itvFNV fzMiot"
|
||||
class="sc-bdnxRM Row-sc-u7azg8-0 RoutingDiagram__RouteRow-sc-i2tbb-2 lmTMKd hLLNig hUDqOH"
|
||||
>
|
||||
<div
|
||||
class="RoutingDiagram__DottedLine-sc-o1ook0-4 kkXINS"
|
||||
class="RoutingDiagram__DottedLine-sc-i2tbb-4 cKqYfU"
|
||||
>
|
||||
<svg
|
||||
class="RoutingDiagram__DotColor-sc-o1ook0-5 kgYqrO"
|
||||
class="RoutingDiagram__DotColor-sc-i2tbb-5 fhSaBA"
|
||||
>
|
||||
dot_line.svg
|
||||
</svg>
|
||||
</div>
|
||||
<div
|
||||
class="Badge-sc-1mhw5si-0 RoutingDiagram__OpaqueBadge-sc-o1ook0-6 gayll OurGh"
|
||||
class="Badge-sc-3epor3-0 RoutingDiagram__OpaqueBadge-sc-i2tbb-6 knpfHF gGARxH"
|
||||
>
|
||||
<div
|
||||
class="Badge-sc-1mhw5si-0 RoutingDiagram__ProtocolBadge-sc-o1ook0-7 gayll bNVqMw"
|
||||
class="Badge-sc-3epor3-0 RoutingDiagram__ProtocolBadge-sc-i2tbb-7 knpfHF lbdUti"
|
||||
>
|
||||
<div
|
||||
class="theme__TextWrapper-sc-18nh1jk-0 cWOfab RoutingDiagram__BadgeText-sc-o1ook0-8 dYpdfO css-15li2d9"
|
||||
class="theme__TextWrapper-sc-5lu8um-0 chxxqs RoutingDiagram__BadgeText-sc-i2tbb-8 ijjHig css-15li2d9"
|
||||
>
|
||||
V3
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="theme__TextWrapper-sc-18nh1jk-0 cWOfab RoutingDiagram__BadgeText-sc-o1ook0-8 dYpdfO css-1aekuku"
|
||||
class="theme__TextWrapper-sc-5lu8um-0 chxxqs RoutingDiagram__BadgeText-sc-i2tbb-8 ijjHig css-1aekuku"
|
||||
style="min-width: auto;"
|
||||
>
|
||||
100%
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="sc-bdnxRM Row-sc-nrd8cx-0 Row__AutoRow-sc-nrd8cx-3 iqvZFe itvFNV kkMfuq"
|
||||
class="sc-bdnxRM Row-sc-u7azg8-0 Row__AutoRow-sc-u7azg8-3 iqvZFe hLLNig cUhARX"
|
||||
style="justify-content: space-evenly; z-index: 2;"
|
||||
width="100%"
|
||||
>
|
||||
@@ -156,7 +156,7 @@ exports[`renders single route 1`] = `
|
||||
exports[`renders when no routes are provided 1`] = `
|
||||
<DocumentFragment>
|
||||
<div
|
||||
class="RoutingDiagram__Wrapper-sc-o1ook0-0 ePDWDk css-vurnku"
|
||||
class="RoutingDiagram__Wrapper-sc-i2tbb-0 ivndgC css-vurnku"
|
||||
/>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
||||
@@ -6,6 +6,7 @@ import useDebounce from 'hooks/useDebounce'
|
||||
import { useOnClickOutside } from 'hooks/useOnClickOutside'
|
||||
import useTheme from 'hooks/useTheme'
|
||||
import useToggle from 'hooks/useToggle'
|
||||
import useNativeCurrency from 'lib/hooks/useNativeCurrency'
|
||||
import { getTokenFilter } from 'lib/hooks/useTokenList/filtering'
|
||||
import { tokenComparator, useSortTokensByQuery } from 'lib/hooks/useTokenList/sorting'
|
||||
import { KeyboardEvent, RefObject, useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
@@ -17,13 +18,7 @@ import { Text } from 'rebass'
|
||||
import { useAllTokenBalances } from 'state/wallet/hooks'
|
||||
import styled from 'styled-components/macro'
|
||||
|
||||
import {
|
||||
useAllTokens,
|
||||
useIsUserAddedToken,
|
||||
useNativeCurrency,
|
||||
useSearchInactiveTokenLists,
|
||||
useToken,
|
||||
} from '../../hooks/Tokens'
|
||||
import { useAllTokens, useIsUserAddedToken, useSearchInactiveTokenLists, useToken } from '../../hooks/Tokens'
|
||||
import { ButtonText, CloseIcon, IconWrapper, ThemedText } from '../../theme'
|
||||
import { isAddress } from '../../utils'
|
||||
import Column from '../Column'
|
||||
|
||||
@@ -2,11 +2,11 @@
|
||||
import { t, Trans } from '@lingui/macro'
|
||||
import { Percent } from '@uniswap/sdk-core'
|
||||
import useActiveWeb3React from 'hooks/useActiveWeb3React'
|
||||
import { AUTO_ROUTER_SUPPORTED_CHAINS } from 'lib/hooks/routing/clientSideSmartOrderRouter'
|
||||
import { useContext, useRef, useState } from 'react'
|
||||
import { Settings, X } from 'react-feather'
|
||||
import ReactGA from 'react-ga'
|
||||
import { Text } from 'rebass'
|
||||
import { AUTO_ROUTER_SUPPORTED_CHAINS } from 'state/routing/clientSideSmartOrderRouter/constants'
|
||||
import styled, { ThemeContext } from 'styled-components/macro'
|
||||
|
||||
import { useOnClickOutside } from '../../hooks/useOnClickOutside'
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import { AbstractConnector } from '@web3-react/abstract-connector'
|
||||
import { darken } from 'polished'
|
||||
import styled from 'styled-components/macro'
|
||||
import { AbstractConnector } from 'web3-react-abstract-connector'
|
||||
|
||||
import { injected } from '../../connectors'
|
||||
import { SUPPORTED_WALLETS } from '../../constants/wallet'
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import { AbstractConnector } from '@web3-react/abstract-connector'
|
||||
import { UnsupportedChainIdError, useWeb3React } from '@web3-react/core'
|
||||
import { WalletConnectConnector } from '@web3-react/walletconnect-connector'
|
||||
import { AutoColumn } from 'components/Column'
|
||||
import { PrivacyPolicy } from 'components/PrivacyPolicy'
|
||||
import Row, { AutoRow, RowBetween } from 'components/Row'
|
||||
@@ -10,6 +7,9 @@ import { useEffect, useState } from 'react'
|
||||
import { ArrowLeft, ArrowRight, Info } from 'react-feather'
|
||||
import ReactGA from 'react-ga'
|
||||
import styled from 'styled-components/macro'
|
||||
import { AbstractConnector } from 'web3-react-abstract-connector'
|
||||
import { UnsupportedChainIdError, useWeb3React } from 'web3-react-core'
|
||||
import { WalletConnectConnector } from 'web3-react-walletconnect-connector'
|
||||
|
||||
import MetamaskIcon from '../../assets/images/metamask.png'
|
||||
import { ReactComponent as Close } from '../../assets/images/x.svg'
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import { useWeb3React } from '@web3-react/core'
|
||||
import { useEffect } from 'react'
|
||||
import styled from 'styled-components/macro'
|
||||
import { useWeb3React } from 'web3-react-core'
|
||||
|
||||
import { network } from '../../connectors'
|
||||
import { NetworkContextName } from '../../constants/misc'
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { t, Trans } from '@lingui/macro'
|
||||
import { AbstractConnector } from '@web3-react/abstract-connector'
|
||||
import { UnsupportedChainIdError, useWeb3React } from '@web3-react/core'
|
||||
import { Connector } from '@web3-react/types'
|
||||
import { darken } from 'polished'
|
||||
import { useMemo } from 'react'
|
||||
import { Activity } from 'react-feather'
|
||||
import styled, { css } from 'styled-components/macro'
|
||||
import { Connector } from 'widgets-web3-react/types'
|
||||
import { AbstractConnector } from 'web3-react-abstract-connector'
|
||||
import { UnsupportedChainIdError, useWeb3React } from 'web3-react-core'
|
||||
|
||||
import { NetworkContextName } from '../../constants/misc'
|
||||
import useENSName from '../../hooks/useENSName'
|
||||
|
||||
@@ -4,6 +4,8 @@ import ReactGA from 'react-ga'
|
||||
import { RouteComponentProps } from 'react-router-dom'
|
||||
import { getCLS, getFCP, getFID, getLCP, Metric } from 'web-vitals'
|
||||
|
||||
import { GOOGLE_ANALYTICS_CLIENT_ID_STORAGE_KEY } from './index'
|
||||
|
||||
function reportWebVitals({ name, delta, id }: Metric) {
|
||||
ReactGA.timing({
|
||||
category: 'Web Vitals',
|
||||
@@ -31,5 +33,15 @@ export default function GoogleAnalyticsReporter({ location: { pathname, search }
|
||||
useEffect(() => {
|
||||
ReactGA.pageview(`${pathname}${search}`)
|
||||
}, [pathname, search])
|
||||
|
||||
useEffect(() => {
|
||||
// typed as 'any' in react-ga -.-
|
||||
ReactGA.ga((tracker: any) => {
|
||||
if (!tracker) return
|
||||
|
||||
const clientId = tracker.get('clientId')
|
||||
window.localStorage.setItem(GOOGLE_ANALYTICS_CLIENT_ID_STORAGE_KEY, clientId)
|
||||
})
|
||||
}, [])
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -1,12 +1,17 @@
|
||||
import ReactGA from 'react-ga'
|
||||
import { isMobile } from 'utils/userAgent'
|
||||
|
||||
export const GOOGLE_ANALYTICS_CLIENT_ID_STORAGE_KEY = 'ga_client_id'
|
||||
const GOOGLE_ANALYTICS_ID: string | undefined = process.env.REACT_APP_GOOGLE_ANALYTICS_ID
|
||||
|
||||
const storedClientId = window.localStorage.getItem(GOOGLE_ANALYTICS_CLIENT_ID_STORAGE_KEY)
|
||||
|
||||
if (typeof GOOGLE_ANALYTICS_ID === 'string') {
|
||||
ReactGA.initialize(GOOGLE_ANALYTICS_ID, {
|
||||
gaOptions: {
|
||||
storage: 'none',
|
||||
storeGac: false,
|
||||
clientId: storedClientId ?? undefined,
|
||||
},
|
||||
})
|
||||
ReactGA.set({
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { TransactionResponse } from '@ethersproject/providers'
|
||||
import { Trans } from '@lingui/macro'
|
||||
import StakingRewardsJson from '@uniswap/liquidity-staker/build/StakingRewards.json'
|
||||
import useActiveWeb3React from 'hooks/useActiveWeb3React'
|
||||
import { ReactNode, useState } from 'react'
|
||||
import styled from 'styled-components/macro'
|
||||
|
||||
import { useStakingContract } from '../../hooks/useContract'
|
||||
import { useContract } from '../../hooks/useContract'
|
||||
import { StakingInfo } from '../../state/stake/hooks'
|
||||
import { TransactionType } from '../../state/transactions/actions'
|
||||
import { useTransactionAdder } from '../../state/transactions/hooks'
|
||||
@@ -15,6 +16,12 @@ import Modal from '../Modal'
|
||||
import { LoadingView, SubmittedView } from '../ModalViews'
|
||||
import { RowBetween } from '../Row'
|
||||
|
||||
const { abi: STAKING_REWARDS_ABI } = StakingRewardsJson
|
||||
|
||||
function useStakingContract(stakingAddress?: string, withSignerIfPossible?: boolean) {
|
||||
return useContract(stakingAddress, STAKING_REWARDS_ABI, withSignerIfPossible)
|
||||
}
|
||||
|
||||
const ContentWrapper = styled(AutoColumn)`
|
||||
width: 100%;
|
||||
padding: 1rem;
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
import { TransactionResponse } from '@ethersproject/providers'
|
||||
import { Trans } from '@lingui/macro'
|
||||
import StakingRewardsJson from '@uniswap/liquidity-staker/build/StakingRewards.json'
|
||||
import { CurrencyAmount, Token } from '@uniswap/sdk-core'
|
||||
import { Pair } from '@uniswap/v2-sdk'
|
||||
import useActiveWeb3React from 'hooks/useActiveWeb3React'
|
||||
import { useV2LiquidityTokenPermit } from 'hooks/useV2LiquidityTokenPermit'
|
||||
import { useCallback, useState } from 'react'
|
||||
import styled from 'styled-components/macro'
|
||||
|
||||
import { ApprovalState, useApproveCallback } from '../../hooks/useApproveCallback'
|
||||
import { usePairContract, useStakingContract, useV2RouterContract } from '../../hooks/useContract'
|
||||
import { useV2LiquidityTokenPermit } from '../../hooks/useERC20Permit'
|
||||
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'
|
||||
@@ -24,6 +25,12 @@ import { LoadingView, SubmittedView } from '../ModalViews'
|
||||
import ProgressCircles from '../ProgressSteps'
|
||||
import { RowBetween } from '../Row'
|
||||
|
||||
const { abi: STAKING_REWARDS_ABI } = StakingRewardsJson
|
||||
|
||||
function useStakingContract(stakingAddress?: string, withSignerIfPossible?: boolean) {
|
||||
return useContract(stakingAddress, STAKING_REWARDS_ABI, withSignerIfPossible)
|
||||
}
|
||||
|
||||
const HypotheticalRewardRate = styled.div<{ dim: boolean }>`
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { TransactionResponse } from '@ethersproject/providers'
|
||||
import { Trans } from '@lingui/macro'
|
||||
import StakingRewardsJson from '@uniswap/liquidity-staker/build/StakingRewards.json'
|
||||
import useActiveWeb3React from 'hooks/useActiveWeb3React'
|
||||
import { ReactNode, useState } from 'react'
|
||||
import styled from 'styled-components/macro'
|
||||
|
||||
import { useStakingContract } from '../../hooks/useContract'
|
||||
import { useContract } from '../../hooks/useContract'
|
||||
import { StakingInfo } from '../../state/stake/hooks'
|
||||
import { TransactionType } from '../../state/transactions/actions'
|
||||
import { useTransactionAdder } from '../../state/transactions/hooks'
|
||||
@@ -16,6 +17,12 @@ import Modal from '../Modal'
|
||||
import { LoadingView, SubmittedView } from '../ModalViews'
|
||||
import { RowBetween } from '../Row'
|
||||
|
||||
const { abi: STAKING_REWARDS_ABI } = StakingRewardsJson
|
||||
|
||||
function useStakingContract(stakingAddress?: string, withSignerIfPossible?: boolean) {
|
||||
return useContract(stakingAddress, STAKING_REWARDS_ABI, withSignerIfPossible)
|
||||
}
|
||||
|
||||
const ContentWrapper = styled(AutoColumn)`
|
||||
width: 100%;
|
||||
padding: 1rem;
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Trade } from '@uniswap/router-sdk'
|
||||
import { Currency, Percent, TradeType } from '@uniswap/sdk-core'
|
||||
import { ReactNode, useCallback, useMemo } from 'react'
|
||||
import { InterfaceTrade } from 'state/routing/types'
|
||||
import { tradeMeaningfullyDiffers } from 'utils/tradeMeaningFullyDiffer'
|
||||
|
||||
import TransactionConfirmationModal, {
|
||||
ConfirmationModalContent,
|
||||
@@ -11,23 +12,6 @@ import TransactionConfirmationModal, {
|
||||
import SwapModalFooter from './SwapModalFooter'
|
||||
import SwapModalHeader from './SwapModalHeader'
|
||||
|
||||
/**
|
||||
* Returns true if the trade requires a confirmation of details before we can submit it
|
||||
* @param args either a pair of V2 trades or a pair of V3 trades
|
||||
*/
|
||||
function tradeMeaningfullyDiffers(
|
||||
...args: [Trade<Currency, Currency, TradeType>, Trade<Currency, Currency, TradeType>]
|
||||
): boolean {
|
||||
const [tradeA, tradeB] = args
|
||||
return (
|
||||
tradeA.tradeType !== tradeB.tradeType ||
|
||||
!tradeA.inputAmount.currency.equals(tradeB.inputAmount.currency) ||
|
||||
!tradeA.inputAmount.equalTo(tradeB.inputAmount) ||
|
||||
!tradeA.outputAmount.currency.equals(tradeB.outputAmount.currency) ||
|
||||
!tradeA.outputAmount.equalTo(tradeB.outputAmount)
|
||||
)
|
||||
}
|
||||
|
||||
export default function ConfirmSwapModal({
|
||||
trade,
|
||||
originalTrade,
|
||||
|
||||
@@ -16,7 +16,7 @@ const StyledPriceContainer = styled.button`
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
align-items: center
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
padding: 0;
|
||||
grid-template-columns: 1fr auto;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import { L2_CHAIN_IDS } from 'constants/chains'
|
||||
import { SupportedChainId } from 'constants/chains'
|
||||
import useActiveWeb3React from 'hooks/useActiveWeb3React'
|
||||
import styled from 'styled-components/macro'
|
||||
import { ThemedText } from 'theme'
|
||||
@@ -38,7 +38,7 @@ const EmptyState = ({ HeaderContent, SubHeaderContent }: EmptyStateProps) => (
|
||||
|
||||
export default function ProposalEmptyState() {
|
||||
const { chainId } = useActiveWeb3React()
|
||||
if (chainId && L2_CHAIN_IDS.includes(chainId)) {
|
||||
if (chainId && chainId !== SupportedChainId.MAINNET) {
|
||||
return (
|
||||
<EmptyState
|
||||
HeaderContent={() => <Trans>Please connect to Layer 1 Ethereum</Trans>}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { FortmaticConnector as FortmaticConnectorCore } from '@web3-react/fortmatic-connector'
|
||||
import { FortmaticConnector as FortmaticConnectorCore } from 'web3-react-fortmatic-connector'
|
||||
|
||||
export const OVERLAY_READY = 'OVERLAY_READY'
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { AbstractConnector } from '@web3-react/abstract-connector'
|
||||
import { ConnectorUpdate } from '@web3-react/types'
|
||||
import invariant from 'tiny-invariant'
|
||||
import { AbstractConnector } from 'web3-react-abstract-connector'
|
||||
import { ConnectorUpdate } from 'web3-react-types'
|
||||
|
||||
interface NetworkConnectorArguments {
|
||||
urls: { [chainId: number]: string }
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { Web3Provider } from '@ethersproject/providers'
|
||||
import { SafeAppConnector } from '@gnosis.pm/safe-apps-web3-react'
|
||||
import { InjectedConnector } from '@web3-react/injected-connector'
|
||||
import { PortisConnector } from '@web3-react/portis-connector'
|
||||
import { WalletConnectConnector } from '@web3-react/walletconnect-connector'
|
||||
import { WalletLinkConnector } from '@web3-react/walletlink-connector'
|
||||
import { INFURA_NETWORK_URLS } from 'constants/chainInfo'
|
||||
import { ALL_SUPPORTED_CHAIN_IDS, SupportedChainId } from 'constants/chains'
|
||||
import { INFURA_NETWORK_URLS } from 'constants/infura'
|
||||
import { InjectedConnector } from 'web3-react-injected-connector'
|
||||
import { PortisConnector } from 'web3-react-portis-connector'
|
||||
import { WalletConnectConnector } from 'web3-react-walletconnect-connector'
|
||||
import { WalletLinkConnector } from 'web3-react-walletlink-connector'
|
||||
|
||||
import UNISWAP_LOGO_URL from '../assets/svg/logo.svg'
|
||||
import getLibrary from '../utils/getLibrary'
|
||||
@@ -53,5 +53,5 @@ export const walletlink = new WalletLinkConnector({
|
||||
url: INFURA_NETWORK_URLS[SupportedChainId.MAINNET],
|
||||
appName: 'Uniswap',
|
||||
appLogoUrl: UNISWAP_LOGO_URL,
|
||||
supportedChainIds: [SupportedChainId.MAINNET, SupportedChainId.POLYGON],
|
||||
supportedChainIds: ALL_SUPPORTED_CHAIN_IDS,
|
||||
})
|
||||
|
||||
@@ -107,3 +107,8 @@ export const V3_MIGRATOR_ADDRESSES: AddressMap = constructSameAddressMap('0xA564
|
||||
SupportedChainId.POLYGON_MUMBAI,
|
||||
SupportedChainId.POLYGON,
|
||||
])
|
||||
|
||||
export const TICK_LENS_ADDRESSES: AddressMap = {
|
||||
[SupportedChainId.ARBITRUM_ONE]: '0xbfd8137f7d1516D3ea5cA83523914859ec47F573',
|
||||
[SupportedChainId.ARBITRUM_RINKEBY]: '0xbfd8137f7d1516D3ea5cA83523914859ec47F573',
|
||||
}
|
||||
|
||||
@@ -7,40 +7,6 @@ import ms from 'ms.macro'
|
||||
import { SupportedChainId, SupportedL1ChainId, SupportedL2ChainId } from './chains'
|
||||
import { ARBITRUM_LIST, OPTIMISM_LIST } from './lists'
|
||||
|
||||
const INFURA_KEY = process.env.REACT_APP_INFURA_KEY
|
||||
if (typeof INFURA_KEY === 'undefined') {
|
||||
throw new Error(`REACT_APP_INFURA_KEY must be a defined environment variable`)
|
||||
}
|
||||
|
||||
/**
|
||||
* These are the network URLs used by the interface when there is not another available source of chain data
|
||||
*/
|
||||
export const INFURA_NETWORK_URLS: { [key in SupportedChainId]: string } = {
|
||||
[SupportedChainId.MAINNET]: `https://mainnet.infura.io/v3/${INFURA_KEY}`,
|
||||
[SupportedChainId.RINKEBY]: `https://rinkeby.infura.io/v3/${INFURA_KEY}`,
|
||||
[SupportedChainId.ROPSTEN]: `https://ropsten.infura.io/v3/${INFURA_KEY}`,
|
||||
[SupportedChainId.GOERLI]: `https://goerli.infura.io/v3/${INFURA_KEY}`,
|
||||
[SupportedChainId.KOVAN]: `https://kovan.infura.io/v3/${INFURA_KEY}`,
|
||||
[SupportedChainId.OPTIMISM]: `https://optimism-mainnet.infura.io/v3/${INFURA_KEY}`,
|
||||
[SupportedChainId.OPTIMISTIC_KOVAN]: `https://optimism-kovan.infura.io/v3/${INFURA_KEY}`,
|
||||
[SupportedChainId.ARBITRUM_ONE]: `https://arbitrum-mainnet.infura.io/v3/${INFURA_KEY}`,
|
||||
[SupportedChainId.ARBITRUM_RINKEBY]: `https://arbitrum-rinkeby.infura.io/v3/${INFURA_KEY}`,
|
||||
[SupportedChainId.POLYGON]: `https://polygon-mainnet.infura.io/v3/${INFURA_KEY}`,
|
||||
[SupportedChainId.POLYGON_MUMBAI]: `https://polygon-mumbai.infura.io/v3/${INFURA_KEY}`,
|
||||
}
|
||||
|
||||
/**
|
||||
* This is used to call the add network RPC
|
||||
*/
|
||||
interface AddNetworkInfo {
|
||||
readonly rpcUrl: string
|
||||
readonly nativeCurrency: {
|
||||
name: string // e.g. 'Goerli ETH',
|
||||
symbol: string // e.g. 'gorETH',
|
||||
decimals: number // e.g. 18,
|
||||
}
|
||||
}
|
||||
|
||||
export enum NetworkType {
|
||||
L1,
|
||||
L2,
|
||||
@@ -56,7 +22,11 @@ interface BaseChainInfo {
|
||||
readonly logoUrl: string
|
||||
readonly label: string
|
||||
readonly helpCenterUrl?: string
|
||||
readonly addNetworkInfo: AddNetworkInfo
|
||||
readonly nativeCurrency: {
|
||||
name: string // e.g. 'Goerli ETH',
|
||||
symbol: string // e.g. 'gorETH',
|
||||
decimals: number // e.g. 18,
|
||||
}
|
||||
}
|
||||
|
||||
export interface L1ChainInfo extends BaseChainInfo {
|
||||
@@ -83,10 +53,7 @@ export const CHAIN_INFO: ChainInfoMap = {
|
||||
infoLink: 'https://info.uniswap.org/#/',
|
||||
label: 'Ethereum',
|
||||
logoUrl: ethereumLogoUrl,
|
||||
addNetworkInfo: {
|
||||
nativeCurrency: { name: 'Ether', symbol: 'ETH', decimals: 18 },
|
||||
rpcUrl: INFURA_NETWORK_URLS[SupportedChainId.MAINNET],
|
||||
},
|
||||
nativeCurrency: { name: 'Ether', symbol: 'ETH', decimals: 18 },
|
||||
},
|
||||
[SupportedChainId.RINKEBY]: {
|
||||
networkType: NetworkType.L1,
|
||||
@@ -95,10 +62,7 @@ export const CHAIN_INFO: ChainInfoMap = {
|
||||
infoLink: 'https://info.uniswap.org/#/',
|
||||
label: 'Rinkeby',
|
||||
logoUrl: ethereumLogoUrl,
|
||||
addNetworkInfo: {
|
||||
nativeCurrency: { name: 'Rinkeby Ether', symbol: 'rETH', decimals: 18 },
|
||||
rpcUrl: INFURA_NETWORK_URLS[SupportedChainId.RINKEBY],
|
||||
},
|
||||
nativeCurrency: { name: 'Rinkeby Ether', symbol: 'rETH', decimals: 18 },
|
||||
},
|
||||
[SupportedChainId.ROPSTEN]: {
|
||||
networkType: NetworkType.L1,
|
||||
@@ -107,10 +71,7 @@ export const CHAIN_INFO: ChainInfoMap = {
|
||||
infoLink: 'https://info.uniswap.org/#/',
|
||||
label: 'Ropsten',
|
||||
logoUrl: ethereumLogoUrl,
|
||||
addNetworkInfo: {
|
||||
nativeCurrency: { name: 'Ropsten Ether', symbol: 'ropETH', decimals: 18 },
|
||||
rpcUrl: INFURA_NETWORK_URLS[SupportedChainId.ROPSTEN],
|
||||
},
|
||||
nativeCurrency: { name: 'Ropsten Ether', symbol: 'ropETH', decimals: 18 },
|
||||
},
|
||||
[SupportedChainId.KOVAN]: {
|
||||
networkType: NetworkType.L1,
|
||||
@@ -119,10 +80,7 @@ export const CHAIN_INFO: ChainInfoMap = {
|
||||
infoLink: 'https://info.uniswap.org/#/',
|
||||
label: 'Kovan',
|
||||
logoUrl: ethereumLogoUrl,
|
||||
addNetworkInfo: {
|
||||
nativeCurrency: { name: 'Kovan Ether', symbol: 'kovETH', decimals: 18 },
|
||||
rpcUrl: INFURA_NETWORK_URLS[SupportedChainId.KOVAN],
|
||||
},
|
||||
nativeCurrency: { name: 'Kovan Ether', symbol: 'kovETH', decimals: 18 },
|
||||
},
|
||||
[SupportedChainId.GOERLI]: {
|
||||
networkType: NetworkType.L1,
|
||||
@@ -131,10 +89,7 @@ export const CHAIN_INFO: ChainInfoMap = {
|
||||
infoLink: 'https://info.uniswap.org/#/',
|
||||
label: 'Görli',
|
||||
logoUrl: ethereumLogoUrl,
|
||||
addNetworkInfo: {
|
||||
nativeCurrency: { name: 'Görli Ether', symbol: 'görETH', decimals: 18 },
|
||||
rpcUrl: INFURA_NETWORK_URLS[SupportedChainId.GOERLI],
|
||||
},
|
||||
nativeCurrency: { name: 'Görli Ether', symbol: 'görETH', decimals: 18 },
|
||||
},
|
||||
[SupportedChainId.OPTIMISM]: {
|
||||
networkType: NetworkType.L2,
|
||||
@@ -148,10 +103,7 @@ export const CHAIN_INFO: ChainInfoMap = {
|
||||
logoUrl: optimismLogoUrl,
|
||||
statusPage: 'https://optimism.io/status',
|
||||
helpCenterUrl: 'https://help.uniswap.org/en/collections/3137778-uniswap-on-optimistic-ethereum-oξ',
|
||||
addNetworkInfo: {
|
||||
nativeCurrency: { name: 'Ether', symbol: 'ETH', decimals: 18 },
|
||||
rpcUrl: 'https://mainnet.optimism.io',
|
||||
},
|
||||
nativeCurrency: { name: 'Ether', symbol: 'ETH', decimals: 18 },
|
||||
},
|
||||
[SupportedChainId.OPTIMISTIC_KOVAN]: {
|
||||
networkType: NetworkType.L2,
|
||||
@@ -165,10 +117,7 @@ export const CHAIN_INFO: ChainInfoMap = {
|
||||
logoUrl: optimismLogoUrl,
|
||||
statusPage: 'https://optimism.io/status',
|
||||
helpCenterUrl: 'https://help.uniswap.org/en/collections/3137778-uniswap-on-optimistic-ethereum-oξ',
|
||||
addNetworkInfo: {
|
||||
nativeCurrency: { name: 'Optimistic Kovan Ether', symbol: 'kovOpETH', decimals: 18 },
|
||||
rpcUrl: 'https://kovan.optimism.io',
|
||||
},
|
||||
nativeCurrency: { name: 'Optimistic Kovan Ether', symbol: 'kovOpETH', decimals: 18 },
|
||||
},
|
||||
[SupportedChainId.ARBITRUM_ONE]: {
|
||||
networkType: NetworkType.L2,
|
||||
@@ -181,10 +130,7 @@ export const CHAIN_INFO: ChainInfoMap = {
|
||||
logoUrl: arbitrumLogoUrl,
|
||||
defaultListUrl: ARBITRUM_LIST,
|
||||
helpCenterUrl: 'https://help.uniswap.org/en/collections/3137787-uniswap-on-arbitrum',
|
||||
addNetworkInfo: {
|
||||
nativeCurrency: { name: 'Ether', symbol: 'ETH', decimals: 18 },
|
||||
rpcUrl: 'https://arb1.arbitrum.io/rpc',
|
||||
},
|
||||
nativeCurrency: { name: 'Ether', symbol: 'ETH', decimals: 18 },
|
||||
},
|
||||
[SupportedChainId.ARBITRUM_RINKEBY]: {
|
||||
networkType: NetworkType.L2,
|
||||
@@ -197,10 +143,7 @@ export const CHAIN_INFO: ChainInfoMap = {
|
||||
logoUrl: arbitrumLogoUrl,
|
||||
defaultListUrl: ARBITRUM_LIST,
|
||||
helpCenterUrl: 'https://help.uniswap.org/en/collections/3137787-uniswap-on-arbitrum',
|
||||
addNetworkInfo: {
|
||||
nativeCurrency: { name: 'Rinkeby Arbitrum Ether', symbol: 'rinkArbETH', decimals: 18 },
|
||||
rpcUrl: 'https://rinkeby.arbitrum.io/rpc',
|
||||
},
|
||||
nativeCurrency: { name: 'Rinkeby Arbitrum Ether', symbol: 'rinkArbETH', decimals: 18 },
|
||||
},
|
||||
[SupportedChainId.POLYGON]: {
|
||||
networkType: NetworkType.L1,
|
||||
@@ -211,10 +154,7 @@ export const CHAIN_INFO: ChainInfoMap = {
|
||||
infoLink: 'https://info.uniswap.org/#/polygon/',
|
||||
label: 'Polygon',
|
||||
logoUrl: polygonMaticLogo,
|
||||
addNetworkInfo: {
|
||||
rpcUrl: 'https://polygon-rpc.com/',
|
||||
nativeCurrency: { name: 'Polygon Matic', symbol: 'MATIC', decimals: 18 },
|
||||
},
|
||||
nativeCurrency: { name: 'Polygon Matic', symbol: 'MATIC', decimals: 18 },
|
||||
},
|
||||
[SupportedChainId.POLYGON_MUMBAI]: {
|
||||
networkType: NetworkType.L1,
|
||||
@@ -225,9 +165,6 @@ export const CHAIN_INFO: ChainInfoMap = {
|
||||
infoLink: 'https://info.uniswap.org/#/polygon/',
|
||||
label: 'Polygon Mumbai',
|
||||
logoUrl: polygonMaticLogo,
|
||||
addNetworkInfo: {
|
||||
nativeCurrency: { name: 'Polygon Mumbai Matic', symbol: 'mMATIC', decimals: 18 },
|
||||
rpcUrl: 'https://rpc-endpoints.superfluid.dev/mumbai',
|
||||
},
|
||||
nativeCurrency: { name: 'Polygon Mumbai Matic', symbol: 'mMATIC', decimals: 18 },
|
||||
},
|
||||
}
|
||||
|
||||
@@ -18,6 +18,20 @@ export enum SupportedChainId {
|
||||
POLYGON_MUMBAI = 80001,
|
||||
}
|
||||
|
||||
export const CHAIN_IDS_TO_NAMES = {
|
||||
[SupportedChainId.MAINNET]: 'mainnet',
|
||||
[SupportedChainId.ROPSTEN]: 'ropsten',
|
||||
[SupportedChainId.RINKEBY]: 'rinkeby',
|
||||
[SupportedChainId.GOERLI]: 'goerli',
|
||||
[SupportedChainId.KOVAN]: 'kovan',
|
||||
[SupportedChainId.POLYGON]: 'polygon',
|
||||
[SupportedChainId.POLYGON_MUMBAI]: 'polygon_mumbai',
|
||||
[SupportedChainId.ARBITRUM_ONE]: 'arbitrum',
|
||||
[SupportedChainId.ARBITRUM_RINKEBY]: 'arbitrum_rinkeby',
|
||||
[SupportedChainId.OPTIMISM]: 'optimism',
|
||||
[SupportedChainId.OPTIMISTIC_KOVAN]: 'optimistic_kovan',
|
||||
}
|
||||
|
||||
/**
|
||||
* Array of all the supported chain IDs
|
||||
*/
|
||||
|
||||
23
src/constants/infura.ts
Normal file
23
src/constants/infura.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { SupportedChainId } from './chains'
|
||||
|
||||
const INFURA_KEY = process.env.REACT_APP_INFURA_KEY
|
||||
if (typeof INFURA_KEY === 'undefined') {
|
||||
throw new Error(`REACT_APP_INFURA_KEY must be a defined environment variable`)
|
||||
}
|
||||
|
||||
/**
|
||||
* These are the network URLs used by the interface when there is not another available source of chain data
|
||||
*/
|
||||
export const INFURA_NETWORK_URLS: { [key in SupportedChainId]: string } = {
|
||||
[SupportedChainId.MAINNET]: `https://mainnet.infura.io/v3/${INFURA_KEY}`,
|
||||
[SupportedChainId.RINKEBY]: `https://rinkeby.infura.io/v3/${INFURA_KEY}`,
|
||||
[SupportedChainId.ROPSTEN]: `https://ropsten.infura.io/v3/${INFURA_KEY}`,
|
||||
[SupportedChainId.GOERLI]: `https://goerli.infura.io/v3/${INFURA_KEY}`,
|
||||
[SupportedChainId.KOVAN]: `https://kovan.infura.io/v3/${INFURA_KEY}`,
|
||||
[SupportedChainId.OPTIMISM]: `https://optimism-mainnet.infura.io/v3/${INFURA_KEY}`,
|
||||
[SupportedChainId.OPTIMISTIC_KOVAN]: `https://optimism-kovan.infura.io/v3/${INFURA_KEY}`,
|
||||
[SupportedChainId.ARBITRUM_ONE]: `https://arbitrum-mainnet.infura.io/v3/${INFURA_KEY}`,
|
||||
[SupportedChainId.ARBITRUM_RINKEBY]: `https://arbitrum-rinkeby.infura.io/v3/${INFURA_KEY}`,
|
||||
[SupportedChainId.POLYGON]: `https://polygon-mainnet.infura.io/v3/${INFURA_KEY}`,
|
||||
[SupportedChainId.POLYGON_MUMBAI]: `https://polygon-mumbai.infura.io/v3/${INFURA_KEY}`,
|
||||
}
|
||||
@@ -32,11 +32,11 @@ export const SUPPORTED_LOCALES = [
|
||||
'vi-VN',
|
||||
'zh-CN',
|
||||
'zh-TW',
|
||||
] as const
|
||||
]
|
||||
export type SupportedLocale = typeof SUPPORTED_LOCALES[number] | 'pseudo'
|
||||
|
||||
// eslint-disable-next-line import/first
|
||||
import * as enUS from '../locales/en-US'
|
||||
import * as enUS from 'locales/en-US'
|
||||
export const DEFAULT_LOCALE: SupportedLocale = 'en-US'
|
||||
export const DEFAULT_CATALOG = enUS
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { AbstractConnector } from '@web3-react/abstract-connector'
|
||||
import { AbstractConnector } from 'web3-react-abstract-connector'
|
||||
|
||||
import INJECTED_ICON_URL from '../assets/images/arrow-right.svg'
|
||||
import COINBASE_ICON_URL from '../assets/images/coinbaseWalletIcon.svg'
|
||||
|
||||
@@ -1,20 +1,15 @@
|
||||
import { arrayify } from '@ethersproject/bytes'
|
||||
import { parseBytes32String } from '@ethersproject/strings'
|
||||
import { Currency, Token } from '@uniswap/sdk-core'
|
||||
import { CHAIN_INFO } from 'constants/chainInfo'
|
||||
import { L2_CHAIN_IDS, SupportedChainId, SupportedL2ChainId } from 'constants/chains'
|
||||
import useActiveWeb3React from 'hooks/useActiveWeb3React'
|
||||
import { NEVER_RELOAD, useSingleCallResult } from 'lib/hooks/multicall'
|
||||
import { useCurrencyFromMap, useTokenFromMap } from 'lib/hooks/useCurrency'
|
||||
import { getTokenFilter } from 'lib/hooks/useTokenList/filtering'
|
||||
import { useMemo } from 'react'
|
||||
|
||||
import { nativeOnChain } from '../constants/tokens'
|
||||
import { useAllLists, useCombinedActiveList, useInactiveListUrls } from '../state/lists/hooks'
|
||||
import { WrappedTokenInfo } from '../state/lists/wrappedTokenInfo'
|
||||
import { useUserAddedTokens } from '../state/user/hooks'
|
||||
import { isAddress } from '../utils'
|
||||
import { TokenAddressMap, useUnsupportedTokenList } from './../state/lists/hooks'
|
||||
import { useBytes32TokenContract, useTokenContract } from './useContract'
|
||||
|
||||
// reduce token map into standard address <-> Token mapping, optionally include user added tokens
|
||||
function useTokensFromMap(tokenMap: TokenAddressMap, includeUserAdded: boolean): { [address: string]: Token } {
|
||||
@@ -159,95 +154,15 @@ export function useIsUserAddedToken(currency: Currency | undefined | null): bool
|
||||
return !!userAddedTokens.find((token) => currency.equals(token))
|
||||
}
|
||||
|
||||
// parse a name or symbol from a token response
|
||||
const BYTES32_REGEX = /^0x[a-fA-F0-9]{64}$/
|
||||
|
||||
function parseStringOrBytes32(str: string | undefined, bytes32: string | undefined, defaultValue: string): string {
|
||||
return str && str.length > 0
|
||||
? str
|
||||
: // need to check for proper bytes string and valid terminator
|
||||
bytes32 && BYTES32_REGEX.test(bytes32) && arrayify(bytes32)[31] === 0
|
||||
? parseBytes32String(bytes32)
|
||||
: defaultValue
|
||||
}
|
||||
|
||||
// undefined if invalid or does not exist
|
||||
// null if loading or null was passed
|
||||
// otherwise returns the token
|
||||
export function useToken(tokenAddress?: string | null): Token | undefined | null {
|
||||
const { chainId } = useActiveWeb3React()
|
||||
export function useToken(tokenAddress?: string | null): Token | null | undefined {
|
||||
const tokens = useAllTokens()
|
||||
|
||||
const address = isAddress(tokenAddress)
|
||||
|
||||
const tokenContract = useTokenContract(address ? address : undefined, false)
|
||||
const tokenContractBytes32 = useBytes32TokenContract(address ? address : undefined, false)
|
||||
const token: Token | undefined = address ? tokens[address] : undefined
|
||||
|
||||
const tokenName = useSingleCallResult(token ? undefined : tokenContract, 'name', undefined, NEVER_RELOAD)
|
||||
const tokenNameBytes32 = useSingleCallResult(
|
||||
token ? undefined : tokenContractBytes32,
|
||||
'name',
|
||||
undefined,
|
||||
NEVER_RELOAD
|
||||
)
|
||||
const symbol = useSingleCallResult(token ? undefined : tokenContract, 'symbol', undefined, NEVER_RELOAD)
|
||||
const symbolBytes32 = useSingleCallResult(token ? undefined : tokenContractBytes32, 'symbol', undefined, NEVER_RELOAD)
|
||||
const decimals = useSingleCallResult(token ? undefined : tokenContract, 'decimals', undefined, NEVER_RELOAD)
|
||||
|
||||
return useMemo(() => {
|
||||
if (token) return token
|
||||
if (tokenAddress === null) return null
|
||||
if (!chainId || !address) return undefined
|
||||
if (decimals.loading || symbol.loading || tokenName.loading) return null
|
||||
if (decimals.result) {
|
||||
return new Token(
|
||||
chainId,
|
||||
address,
|
||||
decimals.result[0],
|
||||
parseStringOrBytes32(symbol.result?.[0], symbolBytes32.result?.[0], 'UNKNOWN'),
|
||||
parseStringOrBytes32(tokenName.result?.[0], tokenNameBytes32.result?.[0], 'Unknown Token')
|
||||
)
|
||||
}
|
||||
return undefined
|
||||
}, [
|
||||
address,
|
||||
chainId,
|
||||
decimals.loading,
|
||||
decimals.result,
|
||||
symbol.loading,
|
||||
symbol.result,
|
||||
symbolBytes32.result,
|
||||
token,
|
||||
tokenAddress,
|
||||
tokenName.loading,
|
||||
tokenName.result,
|
||||
tokenNameBytes32.result,
|
||||
])
|
||||
return useTokenFromMap(tokens, tokenAddress)
|
||||
}
|
||||
|
||||
export function useNativeCurrency(): Currency {
|
||||
const { chainId } = useActiveWeb3React()
|
||||
return useMemo(
|
||||
() =>
|
||||
chainId
|
||||
? nativeOnChain(chainId)
|
||||
: // display mainnet when not connected
|
||||
nativeOnChain(SupportedChainId.MAINNET),
|
||||
[chainId]
|
||||
)
|
||||
}
|
||||
|
||||
export function useCurrency(currencyId: string | null | undefined): Currency | null | undefined {
|
||||
const nativeCurrency = useNativeCurrency()
|
||||
const isNative = Boolean(nativeCurrency && currencyId?.toUpperCase() === 'ETH')
|
||||
const token = useToken(isNative ? undefined : currencyId)
|
||||
|
||||
if (currencyId === null || currencyId === undefined) return currencyId
|
||||
|
||||
// this case so we use our builtin wrapped token instead of wrapped tokens on token lists
|
||||
const wrappedNative = nativeCurrency?.wrapped
|
||||
if (wrappedNative?.address?.toUpperCase() === currencyId?.toUpperCase()) return wrappedNative
|
||||
|
||||
return isNative ? nativeCurrency : token
|
||||
export function useCurrency(currencyId?: string | null): Currency | null | undefined {
|
||||
const tokens = useAllTokens()
|
||||
return useCurrencyFromMap(tokens, currencyId)
|
||||
}
|
||||
|
||||
@@ -1,19 +1,20 @@
|
||||
/* eslint-disable react-hooks/rules-of-hooks */
|
||||
import { Web3Provider } from '@ethersproject/providers'
|
||||
import { useWeb3React } from '@web3-react/core'
|
||||
import { default as useWidgetsWeb3React } from 'lib/hooks/useActiveWeb3React'
|
||||
import { useWeb3React } from 'web3-react-core'
|
||||
|
||||
import { NetworkContextName } from '../constants/misc'
|
||||
|
||||
export default function useActiveWeb3React() {
|
||||
const widgetsContext = useWidgetsWeb3React()
|
||||
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
|
||||
)
|
||||
|
||||
if (process.env.REACT_APP_IS_WIDGET) {
|
||||
return widgetsContext
|
||||
}
|
||||
if (interfaceContext.active) {
|
||||
return interfaceContext
|
||||
}
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import { Currency, Token } from '@uniswap/sdk-core'
|
||||
import useActiveWeb3React from 'hooks/useActiveWeb3React'
|
||||
import useCurrencyLogoURIs from 'lib/hooks/useCurrencyLogoURIs'
|
||||
import { useCallback, useState } from 'react'
|
||||
|
||||
import { getTokenLogoURL } from './../components/CurrencyLogo/index'
|
||||
|
||||
export default function useAddTokenToMetamask(currencyToAdd: Currency | undefined): {
|
||||
addToken: () => void
|
||||
success: boolean | undefined
|
||||
@@ -13,6 +12,7 @@ export default function useAddTokenToMetamask(currencyToAdd: Currency | undefine
|
||||
const token: Token | undefined = currencyToAdd?.wrapped
|
||||
|
||||
const [success, setSuccess] = useState<boolean | undefined>()
|
||||
const logoURL = useCurrencyLogoURIs(token)[0]
|
||||
|
||||
const addToken = useCallback(() => {
|
||||
if (library && library.provider.isMetaMask && library.provider.request && token) {
|
||||
@@ -26,7 +26,7 @@ export default function useAddTokenToMetamask(currencyToAdd: Currency | undefine
|
||||
address: token.address,
|
||||
symbol: token.symbol,
|
||||
decimals: token.decimals,
|
||||
image: getTokenLogoURL(token.address),
|
||||
image: logoURL,
|
||||
},
|
||||
},
|
||||
})
|
||||
@@ -37,7 +37,7 @@ export default function useAddTokenToMetamask(currencyToAdd: Currency | undefine
|
||||
} else {
|
||||
setSuccess(false)
|
||||
}
|
||||
}, [library, token])
|
||||
}, [library, logoURL, token])
|
||||
|
||||
return { addToken, success }
|
||||
}
|
||||
|
||||
@@ -1,69 +1,25 @@
|
||||
import { MaxUint256 } from '@ethersproject/constants'
|
||||
import { TransactionResponse } from '@ethersproject/providers'
|
||||
import { Protocol, Trade } from '@uniswap/router-sdk'
|
||||
import { Trade } from '@uniswap/router-sdk'
|
||||
import { Currency, CurrencyAmount, Percent, TradeType } from '@uniswap/sdk-core'
|
||||
import { Pair, Route as V2Route, Trade as V2Trade } from '@uniswap/v2-sdk'
|
||||
import { Pool, Route as V3Route, Trade as V3Trade } from '@uniswap/v3-sdk'
|
||||
import useActiveWeb3React from 'hooks/useActiveWeb3React'
|
||||
import { useCallback, useMemo } from 'react'
|
||||
import { getTxOptimizedSwapRouter, SwapRouterVersion } from 'utils/getTxOptimizedSwapRouter'
|
||||
import { Trade as V2Trade } from '@uniswap/v2-sdk'
|
||||
import { Trade as V3Trade } from '@uniswap/v3-sdk'
|
||||
import useSwapApproval, { useSwapApprovalOptimizedTrade } from 'lib/hooks/swap/useSwapApproval'
|
||||
import { ApprovalState, useApproval } from 'lib/hooks/useApproval'
|
||||
import { useCallback } from 'react'
|
||||
|
||||
import { SWAP_ROUTER_ADDRESSES, V2_ROUTER_ADDRESS, V3_ROUTER_ADDRESS } from '../constants/addresses'
|
||||
import { TransactionType } from '../state/transactions/actions'
|
||||
import { useHasPendingApproval, useTransactionAdder } from '../state/transactions/hooks'
|
||||
import { calculateGasMargin } from '../utils/calculateGasMargin'
|
||||
import { useTokenContract } from './useContract'
|
||||
import { useTokenAllowance } from './useTokenAllowance'
|
||||
export { ApprovalState } from 'lib/hooks/useApproval'
|
||||
|
||||
export enum ApprovalState {
|
||||
UNKNOWN = 'UNKNOWN',
|
||||
NOT_APPROVED = 'NOT_APPROVED',
|
||||
PENDING = 'PENDING',
|
||||
APPROVED = 'APPROVED',
|
||||
}
|
||||
|
||||
export function useApprovalState(amountToApprove?: CurrencyAmount<Currency>, spender?: string) {
|
||||
const { account } = useActiveWeb3React()
|
||||
const token = amountToApprove?.currency?.isToken ? amountToApprove.currency : undefined
|
||||
|
||||
const currentAllowance = useTokenAllowance(token, account ?? undefined, spender)
|
||||
const pendingApproval = useHasPendingApproval(token?.address, spender)
|
||||
|
||||
return useMemo(() => {
|
||||
if (!amountToApprove || !spender) return ApprovalState.UNKNOWN
|
||||
if (amountToApprove.currency.isNative) return ApprovalState.APPROVED
|
||||
// we might not have enough data to know whether or not we need to approve
|
||||
if (!currentAllowance) return ApprovalState.UNKNOWN
|
||||
|
||||
// amountToApprove will be defined if currentAllowance is
|
||||
return currentAllowance.lessThan(amountToApprove)
|
||||
? pendingApproval
|
||||
? ApprovalState.PENDING
|
||||
: ApprovalState.NOT_APPROVED
|
||||
: ApprovalState.APPROVED
|
||||
}, [amountToApprove, currentAllowance, pendingApproval, spender])
|
||||
}
|
||||
|
||||
/** Returns approval state for all known swap routers */
|
||||
export function useAllApprovalStates(
|
||||
trade: Trade<Currency, Currency, TradeType> | undefined,
|
||||
allowedSlippage: Percent
|
||||
) {
|
||||
const { chainId } = useActiveWeb3React()
|
||||
|
||||
const amountToApprove = useMemo(
|
||||
() => (trade && trade.inputAmount.currency.isToken ? trade.maximumAmountIn(allowedSlippage) : undefined),
|
||||
[trade, allowedSlippage]
|
||||
)
|
||||
|
||||
const v2ApprovalState = useApprovalState(amountToApprove, chainId ? V2_ROUTER_ADDRESS[chainId] : undefined)
|
||||
const v3ApprovalState = useApprovalState(amountToApprove, chainId ? V3_ROUTER_ADDRESS[chainId] : undefined)
|
||||
const v2V3ApprovalState = useApprovalState(amountToApprove, chainId ? SWAP_ROUTER_ADDRESSES[chainId] : undefined)
|
||||
|
||||
return useMemo(
|
||||
() => ({ v2: v2ApprovalState, v3: v3ApprovalState, v2V3: v2V3ApprovalState }),
|
||||
[v2ApprovalState, v2V3ApprovalState, v3ApprovalState]
|
||||
)
|
||||
function useGetAndTrackApproval(getApproval: ReturnType<typeof useApproval>[1]) {
|
||||
const addTransaction = useTransactionAdder()
|
||||
return useCallback(() => {
|
||||
return getApproval().then((pending) => {
|
||||
if (pending) {
|
||||
const { response, tokenAddress, spenderAddress: spender } = pending
|
||||
addTransaction(response, { type: TransactionType.APPROVAL, tokenAddress, spender })
|
||||
}
|
||||
})
|
||||
}, [addTransaction, getApproval])
|
||||
}
|
||||
|
||||
// returns a variable indicating the state of the approval and a function which approves if necessary or early returns
|
||||
@@ -71,69 +27,17 @@ export function useApproveCallback(
|
||||
amountToApprove?: CurrencyAmount<Currency>,
|
||||
spender?: string
|
||||
): [ApprovalState, () => Promise<void>] {
|
||||
const { chainId } = useActiveWeb3React()
|
||||
const token = amountToApprove?.currency?.isToken ? amountToApprove.currency : undefined
|
||||
|
||||
// check the current approval status
|
||||
const approvalState = useApprovalState(amountToApprove, spender)
|
||||
|
||||
const tokenContract = useTokenContract(token?.address)
|
||||
const addTransaction = useTransactionAdder()
|
||||
|
||||
const approve = useCallback(async (): Promise<void> => {
|
||||
if (approvalState !== ApprovalState.NOT_APPROVED) {
|
||||
console.error('approve was called unnecessarily')
|
||||
return
|
||||
}
|
||||
if (!chainId) {
|
||||
console.error('no chainId')
|
||||
return
|
||||
}
|
||||
|
||||
if (!token) {
|
||||
console.error('no token')
|
||||
return
|
||||
}
|
||||
|
||||
if (!tokenContract) {
|
||||
console.error('tokenContract is null')
|
||||
return
|
||||
}
|
||||
|
||||
if (!amountToApprove) {
|
||||
console.error('missing amount to approve')
|
||||
return
|
||||
}
|
||||
|
||||
if (!spender) {
|
||||
console.error('no spender')
|
||||
return
|
||||
}
|
||||
|
||||
let useExact = false
|
||||
const estimatedGas = await tokenContract.estimateGas.approve(spender, MaxUint256).catch(() => {
|
||||
// general fallback for tokens who restrict approval amounts
|
||||
useExact = true
|
||||
return tokenContract.estimateGas.approve(spender, amountToApprove.quotient.toString())
|
||||
})
|
||||
|
||||
return tokenContract
|
||||
.approve(spender, useExact ? amountToApprove.quotient.toString() : MaxUint256, {
|
||||
gasLimit: calculateGasMargin(estimatedGas),
|
||||
})
|
||||
.then((response: TransactionResponse) => {
|
||||
addTransaction(response, { type: TransactionType.APPROVAL, tokenAddress: token.address, spender })
|
||||
})
|
||||
.catch((error: Error) => {
|
||||
console.debug('Failed to approve token', error)
|
||||
throw error
|
||||
})
|
||||
}, [approvalState, token, tokenContract, amountToApprove, spender, addTransaction, chainId])
|
||||
|
||||
return [approvalState, approve]
|
||||
const [approval, getApproval] = useApproval(amountToApprove, spender, useHasPendingApproval)
|
||||
return [approval, useGetAndTrackApproval(getApproval)]
|
||||
}
|
||||
|
||||
export function useApprovalOptimizedTrade(
|
||||
trade: Trade<Currency, Currency, TradeType> | undefined,
|
||||
allowedSlippage: Percent
|
||||
) {
|
||||
return useSwapApprovalOptimizedTrade(trade, allowedSlippage, useHasPendingApproval)
|
||||
}
|
||||
|
||||
// wraps useApproveCallback in the context of a swap
|
||||
export function useApproveCallbackFromTrade(
|
||||
trade:
|
||||
| V2Trade<Currency, Currency, TradeType>
|
||||
@@ -141,85 +45,7 @@ export function useApproveCallbackFromTrade(
|
||||
| Trade<Currency, Currency, TradeType>
|
||||
| undefined,
|
||||
allowedSlippage: Percent
|
||||
) {
|
||||
const { chainId } = useActiveWeb3React()
|
||||
const amountToApprove = useMemo(
|
||||
() => (trade && trade.inputAmount.currency.isToken ? trade.maximumAmountIn(allowedSlippage) : undefined),
|
||||
[trade, allowedSlippage]
|
||||
)
|
||||
|
||||
const approveCallback = useApproveCallback(
|
||||
amountToApprove,
|
||||
chainId
|
||||
? trade instanceof V2Trade
|
||||
? V2_ROUTER_ADDRESS[chainId]
|
||||
: trade instanceof V3Trade
|
||||
? V3_ROUTER_ADDRESS[chainId]
|
||||
: SWAP_ROUTER_ADDRESSES[chainId]
|
||||
: undefined
|
||||
)
|
||||
|
||||
// TODO: remove L162-168 after testing is done. This error will help detect mistakes in the logic.
|
||||
if (
|
||||
(Trade instanceof V2Trade && approveCallback[0] !== ApprovalState.APPROVED) ||
|
||||
(trade instanceof V3Trade && approveCallback[0] !== ApprovalState.APPROVED)
|
||||
) {
|
||||
throw new Error('Trying to approve legacy router')
|
||||
}
|
||||
|
||||
return approveCallback
|
||||
}
|
||||
|
||||
export function useApprovalOptimizedTrade(
|
||||
trade: Trade<Currency, Currency, TradeType> | undefined,
|
||||
allowedSlippage: Percent
|
||||
):
|
||||
| V2Trade<Currency, Currency, TradeType>
|
||||
| V3Trade<Currency, Currency, TradeType>
|
||||
| Trade<Currency, Currency, TradeType>
|
||||
| undefined {
|
||||
const onlyV2Routes = trade?.routes.every((route) => route.protocol === Protocol.V2)
|
||||
const onlyV3Routes = trade?.routes.every((route) => route.protocol === Protocol.V3)
|
||||
const tradeHasSplits = (trade?.routes.length ?? 0) > 1
|
||||
|
||||
const approvalStates = useAllApprovalStates(trade, allowedSlippage)
|
||||
|
||||
const optimizedSwapRouter = useMemo(
|
||||
() => getTxOptimizedSwapRouter({ onlyV2Routes, onlyV3Routes, tradeHasSplits, approvalStates }),
|
||||
[approvalStates, tradeHasSplits, onlyV2Routes, onlyV3Routes]
|
||||
)
|
||||
|
||||
return useMemo(() => {
|
||||
if (!trade) return undefined
|
||||
|
||||
try {
|
||||
switch (optimizedSwapRouter) {
|
||||
case SwapRouterVersion.V2V3:
|
||||
return trade
|
||||
case SwapRouterVersion.V2:
|
||||
const pairs = trade.swaps[0].route.pools.filter((pool) => pool instanceof Pair) as Pair[]
|
||||
const v2Route = new V2Route(pairs, trade.inputAmount.currency, trade.outputAmount.currency)
|
||||
return new V2Trade(v2Route, trade.inputAmount, trade.tradeType)
|
||||
case SwapRouterVersion.V3:
|
||||
return V3Trade.createUncheckedTradeWithMultipleRoutes({
|
||||
routes: trade.swaps.map(({ route, inputAmount, outputAmount }) => ({
|
||||
route: new V3Route(
|
||||
route.pools.filter((p) => p instanceof Pool) as Pool[],
|
||||
inputAmount.currency,
|
||||
outputAmount.currency
|
||||
),
|
||||
inputAmount,
|
||||
outputAmount,
|
||||
})),
|
||||
tradeType: trade.tradeType,
|
||||
})
|
||||
default:
|
||||
return undefined
|
||||
}
|
||||
} catch (e) {
|
||||
// TODO(#2989): remove try-catch
|
||||
console.debug(e)
|
||||
return undefined
|
||||
}
|
||||
}, [trade, optimizedSwapRouter])
|
||||
): [ApprovalState, () => Promise<void>] {
|
||||
const [approval, getApproval] = useSwapApproval(trade, allowedSlippage, useHasPendingApproval)
|
||||
return [approval, useGetAndTrackApproval(getApproval)]
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import useActiveWeb3React from 'hooks/useActiveWeb3React'
|
||||
import { AUTO_ROUTER_SUPPORTED_CHAINS } from 'state/routing/clientSideSmartOrderRouter/constants'
|
||||
import { AUTO_ROUTER_SUPPORTED_CHAINS } from 'lib/hooks/routing/clientSideSmartOrderRouter'
|
||||
|
||||
export default function useAutoRouterSupported(): boolean {
|
||||
const { chainId } = useActiveWeb3React()
|
||||
|
||||
@@ -4,11 +4,10 @@ import { SUPPORTED_GAS_ESTIMATE_CHAIN_IDS } from 'constants/chains'
|
||||
import { L2_CHAIN_IDS } from 'constants/chains'
|
||||
import useActiveWeb3React from 'hooks/useActiveWeb3React'
|
||||
import JSBI from 'jsbi'
|
||||
import useNativeCurrency from 'lib/hooks/useNativeCurrency'
|
||||
import { useMemo } from 'react'
|
||||
import { InterfaceTrade } from 'state/routing/types'
|
||||
|
||||
import { useUserSlippageToleranceWithDefault } from '../state/user/hooks'
|
||||
import { useNativeCurrency } from './Tokens'
|
||||
import useGasPrice from './useGasPrice'
|
||||
import useUSDCPrice, { useUSDCValue } from './useUSDCPrice'
|
||||
|
||||
@@ -29,7 +28,10 @@ function guesstimateGas(trade: Trade<Currency, Currency, TradeType> | undefined)
|
||||
const MIN_AUTO_SLIPPAGE_TOLERANCE = new Percent(5, 1000) // 0.5%
|
||||
const MAX_AUTO_SLIPPAGE_TOLERANCE = new Percent(25, 100) // 25%
|
||||
|
||||
export default function useSwapSlippageTolerance(
|
||||
/**
|
||||
* Returns slippage tolerance based on values from current trade, gas estimates from api, and active network.
|
||||
*/
|
||||
export default function useAutoSlippageTolerance(
|
||||
trade: InterfaceTrade<Currency, Currency, TradeType> | undefined
|
||||
): Percent {
|
||||
const { chainId } = useActiveWeb3React()
|
||||
@@ -41,7 +43,7 @@ export default function useSwapSlippageTolerance(
|
||||
const nativeCurrency = useNativeCurrency()
|
||||
const nativeCurrencyPrice = useUSDCPrice(nativeCurrency ?? undefined)
|
||||
|
||||
const defaultSlippageTolerance = useMemo(() => {
|
||||
return useMemo(() => {
|
||||
if (!trade || onL2) return ONE_TENTHS_PERCENT
|
||||
|
||||
const nativeGasCost =
|
||||
@@ -73,6 +75,4 @@ export default function useSwapSlippageTolerance(
|
||||
|
||||
return V3_SWAP_DEFAULT_SLIPPAGE
|
||||
}, [trade, onL2, nativeGasPrice, gasEstimate, nativeCurrency, nativeCurrencyPrice, chainId, outputDollarValue])
|
||||
|
||||
return useUserSlippageToleranceWithDefault(defaultSlippageTolerance)
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Currency, CurrencyAmount, TradeType } from '@uniswap/sdk-core'
|
||||
import { useMemo } from 'react'
|
||||
import { InterfaceTrade, TradeState } from 'state/routing/types'
|
||||
import { useRoutingAPITrade } from 'state/routing/useRoutingAPITrade'
|
||||
|
||||
@@ -24,7 +25,10 @@ export function useBestTrade(
|
||||
const autoRouterSupported = useAutoRouterSupported()
|
||||
const isWindowVisible = useIsWindowVisible()
|
||||
|
||||
const [debouncedAmount, debouncedOtherCurrency] = useDebounce([amountSpecified, otherCurrency], 200)
|
||||
const [debouncedAmount, debouncedOtherCurrency] = useDebounce(
|
||||
useMemo(() => [amountSpecified, otherCurrency], [amountSpecified, otherCurrency]),
|
||||
200
|
||||
)
|
||||
|
||||
const routingAPITrade = useRoutingAPITrade(
|
||||
tradeType,
|
||||
@@ -56,9 +60,12 @@ export function useBestTrade(
|
||||
)
|
||||
|
||||
// only return gas estimate from api if routing api trade is used
|
||||
return {
|
||||
...(useFallback ? bestV3Trade : routingAPITrade),
|
||||
...(debouncing ? { state: TradeState.SYNCING } : {}),
|
||||
...(isLoading ? { state: TradeState.LOADING } : {}),
|
||||
}
|
||||
return useMemo(
|
||||
() => ({
|
||||
...(useFallback ? bestV3Trade : routingAPITrade),
|
||||
...(debouncing ? { state: TradeState.SYNCING } : {}),
|
||||
...(isLoading ? { state: TradeState.LOADING } : {}),
|
||||
}),
|
||||
[bestV3Trade, debouncing, isLoading, routingAPITrade, useFallback]
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,14 +1,11 @@
|
||||
import { Contract } from '@ethersproject/contracts'
|
||||
import { abi as GOVERNANCE_ABI } from '@uniswap/governance/build/GovernorAlpha.json'
|
||||
import { abi as UNI_ABI } from '@uniswap/governance/build/Uni.json'
|
||||
import { abi as STAKING_REWARDS_ABI } from '@uniswap/liquidity-staker/build/StakingRewards.json'
|
||||
import { abi as MERKLE_DISTRIBUTOR_ABI } from '@uniswap/merkle-distributor/build/MerkleDistributor.json'
|
||||
import { abi as IUniswapV2PairABI } from '@uniswap/v2-core/build/IUniswapV2Pair.json'
|
||||
import { abi as IUniswapV2Router02ABI } from '@uniswap/v2-periphery/build/IUniswapV2Router02.json'
|
||||
import { abi as QuoterABI } from '@uniswap/v3-periphery/artifacts/contracts/lens/Quoter.sol/Quoter.json'
|
||||
import { abi as MulticallABI } from '@uniswap/v3-periphery/artifacts/contracts/lens/UniswapInterfaceMulticall.sol/UniswapInterfaceMulticall.json'
|
||||
import { abi as NFTPositionManagerABI } from '@uniswap/v3-periphery/artifacts/contracts/NonfungiblePositionManager.sol/NonfungiblePositionManager.json'
|
||||
import { abi as V2MigratorABI } from '@uniswap/v3-periphery/artifacts/contracts/V3Migrator.sol/V3Migrator.json'
|
||||
import IUniswapV2PairJson from '@uniswap/v2-core/build/IUniswapV2Pair.json'
|
||||
import IUniswapV2Router02Json from '@uniswap/v2-periphery/build/IUniswapV2Router02.json'
|
||||
import QuoterJson from '@uniswap/v3-periphery/artifacts/contracts/lens/Quoter.sol/Quoter.json'
|
||||
import TickLensJson from '@uniswap/v3-periphery/artifacts/contracts/lens/TickLens.sol/TickLens.json'
|
||||
import UniswapInterfaceMulticallJson from '@uniswap/v3-periphery/artifacts/contracts/lens/UniswapInterfaceMulticall.sol/UniswapInterfaceMulticall.json'
|
||||
import NonfungiblePositionManagerJson from '@uniswap/v3-periphery/artifacts/contracts/NonfungiblePositionManager.sol/NonfungiblePositionManager.json'
|
||||
import V3MigratorJson from '@uniswap/v3-periphery/artifacts/contracts/V3Migrator.sol/V3Migrator.json'
|
||||
import ARGENT_WALLET_DETECTOR_ABI from 'abis/argent-wallet-detector.json'
|
||||
import EIP_2612 from 'abis/eip_2612.json'
|
||||
import ENS_PUBLIC_RESOLVER_ABI from 'abis/ens-public-resolver.json'
|
||||
@@ -17,30 +14,34 @@ import ERC20_ABI from 'abis/erc20.json'
|
||||
import ERC20_BYTES32_ABI from 'abis/erc20_bytes32.json'
|
||||
import ERC721_ABI from 'abis/erc721.json'
|
||||
import ERC1155_ABI from 'abis/erc1155.json'
|
||||
import GOVERNOR_BRAVO_ABI from 'abis/governor-bravo.json'
|
||||
import { ArgentWalletDetector, EnsPublicResolver, EnsRegistrar, Erc20, Erc721, Erc1155, Weth } from 'abis/types'
|
||||
import WETH_ABI from 'abis/weth.json'
|
||||
import {
|
||||
ARGENT_WALLET_DETECTOR_ADDRESS,
|
||||
ENS_REGISTRAR_ADDRESSES,
|
||||
GOVERNANCE_ALPHA_V0_ADDRESSES,
|
||||
GOVERNANCE_ALPHA_V1_ADDRESSES,
|
||||
GOVERNANCE_BRAVO_ADDRESSES,
|
||||
MERKLE_DISTRIBUTOR_ADDRESS,
|
||||
MULTICALL_ADDRESS,
|
||||
NONFUNGIBLE_POSITION_MANAGER_ADDRESSES,
|
||||
QUOTER_ADDRESSES,
|
||||
TICK_LENS_ADDRESSES,
|
||||
V2_ROUTER_ADDRESS,
|
||||
V3_MIGRATOR_ADDRESSES,
|
||||
} from 'constants/addresses'
|
||||
import { UNI, WRAPPED_NATIVE_CURRENCY } from 'constants/tokens'
|
||||
import { WRAPPED_NATIVE_CURRENCY } from 'constants/tokens'
|
||||
import useActiveWeb3React from 'hooks/useActiveWeb3React'
|
||||
import { useMemo } from 'react'
|
||||
import { NonfungiblePositionManager, Quoter, UniswapInterfaceMulticall } from 'types/v3'
|
||||
import { NonfungiblePositionManager, Quoter, TickLens, UniswapInterfaceMulticall } from 'types/v3'
|
||||
import { V3Migrator } from 'types/v3/V3Migrator'
|
||||
|
||||
import { getContract } from '../utils'
|
||||
|
||||
const { abi: IUniswapV2PairABI } = IUniswapV2PairJson
|
||||
const { abi: IUniswapV2Router02ABI } = IUniswapV2Router02Json
|
||||
const { abi: QuoterABI } = QuoterJson
|
||||
const { abi: TickLensABI } = TickLensJson
|
||||
const { abi: MulticallABI } = UniswapInterfaceMulticallJson
|
||||
const { abi: NFTPositionManagerABI } = NonfungiblePositionManagerJson
|
||||
const { abi: V2MigratorABI } = V3MigratorJson
|
||||
|
||||
// returns null on errors
|
||||
export function useContract<T extends Contract = Contract>(
|
||||
addressOrAddressMap: string | { [chainId: number]: string } | undefined,
|
||||
@@ -121,33 +122,6 @@ export function useInterfaceMulticall() {
|
||||
return useContract<UniswapInterfaceMulticall>(MULTICALL_ADDRESS, MulticallABI, false) as UniswapInterfaceMulticall
|
||||
}
|
||||
|
||||
export function useMerkleDistributorContract() {
|
||||
return useContract(MERKLE_DISTRIBUTOR_ADDRESS, MERKLE_DISTRIBUTOR_ABI, true)
|
||||
}
|
||||
|
||||
export function useGovernanceV0Contract(): Contract | null {
|
||||
return useContract(GOVERNANCE_ALPHA_V0_ADDRESSES, GOVERNANCE_ABI, false)
|
||||
}
|
||||
|
||||
export function useGovernanceV1Contract(): Contract | null {
|
||||
return useContract(GOVERNANCE_ALPHA_V1_ADDRESSES, GOVERNANCE_ABI, false)
|
||||
}
|
||||
|
||||
export function useGovernanceBravoContract(): Contract | null {
|
||||
return useContract(GOVERNANCE_BRAVO_ADDRESSES, GOVERNOR_BRAVO_ABI, true)
|
||||
}
|
||||
|
||||
export const useLatestGovernanceContract = useGovernanceBravoContract
|
||||
|
||||
export function useUniContract() {
|
||||
const { chainId } = useActiveWeb3React()
|
||||
return useContract(chainId ? UNI[chainId]?.address : undefined, UNI_ABI, true)
|
||||
}
|
||||
|
||||
export function useStakingContract(stakingAddress?: string, withSignerIfPossible?: boolean) {
|
||||
return useContract(stakingAddress, STAKING_REWARDS_ABI, withSignerIfPossible)
|
||||
}
|
||||
|
||||
export function useV3NFTPositionManagerContract(withSignerIfPossible?: boolean): NonfungiblePositionManager | null {
|
||||
return useContract<NonfungiblePositionManager>(
|
||||
NONFUNGIBLE_POSITION_MANAGER_ADDRESSES,
|
||||
@@ -159,3 +133,9 @@ export function useV3NFTPositionManagerContract(withSignerIfPossible?: boolean):
|
||||
export function useV3Quoter() {
|
||||
return useContract<Quoter>(QUOTER_ADDRESSES, QuoterABI)
|
||||
}
|
||||
|
||||
export function useTickLens(): TickLens | null {
|
||||
const { chainId } = useActiveWeb3React()
|
||||
const address = chainId ? TICK_LENS_ADDRESSES[chainId] : undefined
|
||||
return useContract(address, TickLensABI) as TickLens | null
|
||||
}
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
/**
|
||||
* Debounces updates to a value.
|
||||
* Non-primitives *must* wrap the value in useMemo, or the value will be updated due to referential inequality.
|
||||
*/
|
||||
// modified from https://usehooks.com/useDebounce/
|
||||
export default function useDebounce<T>(value: T, delay: number): T {
|
||||
const [debouncedValue, setDebouncedValue] = useState<T>(value)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { BigNumber } from '@ethersproject/bignumber'
|
||||
import { splitSignature } from '@ethersproject/bytes'
|
||||
import { Trade } from '@uniswap/router-sdk'
|
||||
import { Currency, CurrencyAmount, Percent, Token, TradeType } from '@uniswap/sdk-core'
|
||||
import { Currency, CurrencyAmount, Percent, TradeType } from '@uniswap/sdk-core'
|
||||
import { Trade as V2Trade } from '@uniswap/v2-sdk'
|
||||
import { Trade as V3Trade } from '@uniswap/v3-sdk'
|
||||
import useActiveWeb3React from 'hooks/useActiveWeb3React'
|
||||
@@ -12,9 +13,8 @@ import { SWAP_ROUTER_ADDRESSES, V3_ROUTER_ADDRESS } from '../constants/addresses
|
||||
import { DAI, UNI, USDC } from '../constants/tokens'
|
||||
import { useEIP2612Contract } from './useContract'
|
||||
import useIsArgentWallet from './useIsArgentWallet'
|
||||
import useTransactionDeadline from './useTransactionDeadline'
|
||||
|
||||
enum PermitType {
|
||||
export enum PermitType {
|
||||
AMOUNT = 1,
|
||||
ALLOWED = 2,
|
||||
}
|
||||
@@ -22,7 +22,7 @@ enum PermitType {
|
||||
// 20 minutes to submit after signing
|
||||
const PERMIT_VALIDITY_BUFFER = 20 * 60
|
||||
|
||||
interface PermitInfo {
|
||||
export interface PermitInfo {
|
||||
type: PermitType
|
||||
name: string
|
||||
// version is optional, and if omitted, will not be included in the domain
|
||||
@@ -116,9 +116,10 @@ const PERMIT_ALLOWED_TYPE = [
|
||||
{ name: 'allowed', type: 'bool' },
|
||||
]
|
||||
|
||||
function useERC20Permit(
|
||||
export function useERC20Permit(
|
||||
currencyAmount: CurrencyAmount<Currency> | null | undefined,
|
||||
spender: string | null | undefined,
|
||||
transactionDeadline: BigNumber | undefined,
|
||||
overridePermitInfo: PermitInfo | undefined | null
|
||||
): {
|
||||
signatureData: SignatureData | null
|
||||
@@ -126,7 +127,6 @@ function useERC20Permit(
|
||||
gatherPermitSignature: null | (() => Promise<void>)
|
||||
} {
|
||||
const { account, chainId, library } = useActiveWeb3React()
|
||||
const transactionDeadline = useTransactionDeadline()
|
||||
const tokenAddress = currencyAmount?.currency?.isToken ? currencyAmount.currency.address : undefined
|
||||
const eip2612Contract = useEIP2612Contract(tokenAddress)
|
||||
const isArgentWallet = useIsArgentWallet()
|
||||
@@ -259,26 +259,14 @@ function useERC20Permit(
|
||||
])
|
||||
}
|
||||
|
||||
const REMOVE_V2_LIQUIDITY_PERMIT_INFO: PermitInfo = {
|
||||
version: '1',
|
||||
name: 'Uniswap V2',
|
||||
type: PermitType.AMOUNT,
|
||||
}
|
||||
|
||||
export function useV2LiquidityTokenPermit(
|
||||
liquidityAmount: CurrencyAmount<Token> | null | undefined,
|
||||
spender: string | null | undefined
|
||||
) {
|
||||
return useERC20Permit(liquidityAmount, spender, REMOVE_V2_LIQUIDITY_PERMIT_INFO)
|
||||
}
|
||||
|
||||
export function useERC20PermitFromTrade(
|
||||
trade:
|
||||
| V2Trade<Currency, Currency, TradeType>
|
||||
| V3Trade<Currency, Currency, TradeType>
|
||||
| Trade<Currency, Currency, TradeType>
|
||||
| undefined,
|
||||
allowedSlippage: Percent
|
||||
allowedSlippage: Percent,
|
||||
transactionDeadline: BigNumber | undefined
|
||||
) {
|
||||
const { chainId } = useActiveWeb3React()
|
||||
const swapRouterAddress = chainId
|
||||
@@ -294,5 +282,5 @@ export function useERC20PermitFromTrade(
|
||||
[trade, allowedSlippage]
|
||||
)
|
||||
|
||||
return useERC20Permit(amountToApprove, swapRouterAddress, null)
|
||||
return useERC20Permit(amountToApprove, swapRouterAddress, transactionDeadline, null)
|
||||
}
|
||||
|
||||
@@ -1,30 +1,141 @@
|
||||
import { skipToken } from '@reduxjs/toolkit/query/react'
|
||||
import { Currency } from '@uniswap/sdk-core'
|
||||
import { FeeAmount, Pool, TICK_SPACINGS, tickToPrice } from '@uniswap/v3-sdk'
|
||||
import { ChainId } from '@uniswap/smart-order-router'
|
||||
import { FeeAmount, nearestUsableTick, Pool, TICK_SPACINGS, tickToPrice } from '@uniswap/v3-sdk'
|
||||
import { ZERO_ADDRESS } from 'constants/misc'
|
||||
import JSBI from 'jsbi'
|
||||
import { useSingleContractMultipleData } from 'lib/hooks/multicall'
|
||||
import ms from 'ms.macro'
|
||||
import { useMemo } from 'react'
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import { useAllV3TicksQuery } from 'state/data/enhanced'
|
||||
import { AllV3TicksQuery } from 'state/data/generated'
|
||||
import computeSurroundingTicks from 'utils/computeSurroundingTicks'
|
||||
|
||||
import { useTickLens } from './useContract'
|
||||
import { PoolState, usePool } from './usePools'
|
||||
|
||||
const PRICE_FIXED_DIGITS = 8
|
||||
const CHAIN_IDS_MISSING_SUBGRAPH_DATA = [ChainId.ARBITRUM_ONE, ChainId.ARBITRUM_RINKEBY]
|
||||
|
||||
export interface TickData {
|
||||
tick: number
|
||||
liquidityNet: JSBI
|
||||
liquidityGross: JSBI
|
||||
}
|
||||
|
||||
// Tick with fields parsed to JSBIs, and active liquidity computed.
|
||||
export interface TickProcessed {
|
||||
tickIdx: number
|
||||
tick: number
|
||||
liquidityActive: JSBI
|
||||
liquidityNet: JSBI
|
||||
price0: string
|
||||
}
|
||||
|
||||
const REFRESH_FREQUENCY = { blocksPerFetch: 2 }
|
||||
|
||||
const getActiveTick = (tickCurrent: number | undefined, feeAmount: FeeAmount | undefined) =>
|
||||
tickCurrent && feeAmount ? Math.floor(tickCurrent / TICK_SPACINGS[feeAmount]) * TICK_SPACINGS[feeAmount] : undefined
|
||||
|
||||
// Fetches all ticks for a given pool
|
||||
export function useAllV3Ticks(
|
||||
const bitmapIndex = (tick: number, tickSpacing: number) => {
|
||||
return Math.floor(tick / tickSpacing / 256)
|
||||
}
|
||||
|
||||
function useTicksFromTickLens(
|
||||
currencyA: Currency | undefined,
|
||||
currencyB: Currency | undefined,
|
||||
feeAmount: FeeAmount | undefined,
|
||||
numSurroundingTicks: number | undefined = 125
|
||||
) {
|
||||
const [tickDataLatestSynced, setTickDataLatestSynced] = useState<TickData[]>([])
|
||||
|
||||
const [poolState, pool] = usePool(currencyA, currencyB, feeAmount)
|
||||
|
||||
const tickSpacing = feeAmount && TICK_SPACINGS[feeAmount]
|
||||
|
||||
// Find nearest valid tick for pool in case tick is not initialized.
|
||||
const activeTick = pool?.tickCurrent && tickSpacing ? nearestUsableTick(pool?.tickCurrent, tickSpacing) : undefined
|
||||
|
||||
const poolAddress =
|
||||
currencyA && currencyB && feeAmount && poolState === PoolState.EXISTS
|
||||
? Pool.getAddress(currencyA?.wrapped, currencyB?.wrapped, feeAmount)
|
||||
: undefined
|
||||
|
||||
// it is also possible to grab all tick data but it is extremely slow
|
||||
// bitmapIndex(nearestUsableTick(TickMath.MIN_TICK, tickSpacing), tickSpacing)
|
||||
const minIndex = useMemo(
|
||||
() =>
|
||||
tickSpacing && activeTick ? bitmapIndex(activeTick - numSurroundingTicks * tickSpacing, tickSpacing) : undefined,
|
||||
[tickSpacing, activeTick, numSurroundingTicks]
|
||||
)
|
||||
|
||||
const maxIndex = useMemo(
|
||||
() =>
|
||||
tickSpacing && activeTick ? bitmapIndex(activeTick + numSurroundingTicks * tickSpacing, tickSpacing) : undefined,
|
||||
[tickSpacing, activeTick, numSurroundingTicks]
|
||||
)
|
||||
|
||||
const tickLensArgs: [string, number][] = useMemo(
|
||||
() =>
|
||||
maxIndex && minIndex && poolAddress && poolAddress !== ZERO_ADDRESS
|
||||
? new Array(maxIndex - minIndex + 1)
|
||||
.fill(0)
|
||||
.map((_, i) => i + minIndex)
|
||||
.map((wordIndex) => [poolAddress, wordIndex])
|
||||
: [],
|
||||
[minIndex, maxIndex, poolAddress]
|
||||
)
|
||||
|
||||
const tickLens = useTickLens()
|
||||
const callStates = useSingleContractMultipleData(
|
||||
tickLensArgs.length > 0 ? tickLens : undefined,
|
||||
'getPopulatedTicksInWord',
|
||||
tickLensArgs,
|
||||
REFRESH_FREQUENCY
|
||||
)
|
||||
|
||||
const isError = useMemo(() => callStates.some(({ error }) => error), [callStates])
|
||||
const isLoading = useMemo(() => callStates.some(({ loading }) => loading), [callStates])
|
||||
const IsSyncing = useMemo(() => callStates.some(({ syncing }) => syncing), [callStates])
|
||||
const isValid = useMemo(() => callStates.some(({ valid }) => valid), [callStates])
|
||||
|
||||
const tickData: TickData[] = useMemo(
|
||||
() =>
|
||||
callStates
|
||||
.map(({ result }) => result?.populatedTicks)
|
||||
.reduce(
|
||||
(accumulator, current) => [
|
||||
...accumulator,
|
||||
...(current?.map((tickData: TickData) => {
|
||||
return {
|
||||
tick: tickData.tick,
|
||||
liquidityNet: JSBI.BigInt(tickData.liquidityNet),
|
||||
liquidityGross: JSBI.BigInt(tickData.liquidityGross),
|
||||
}
|
||||
}) ?? []),
|
||||
],
|
||||
[]
|
||||
),
|
||||
[callStates]
|
||||
)
|
||||
|
||||
// reset on input change
|
||||
useEffect(() => {
|
||||
setTickDataLatestSynced([])
|
||||
}, [currencyA, currencyB, feeAmount])
|
||||
|
||||
// return the latest synced tickData even if we are still loading the newest data
|
||||
useEffect(() => {
|
||||
if (!IsSyncing && !isLoading && !isError && isValid) {
|
||||
setTickDataLatestSynced(tickData.sort((a, b) => a.tick - b.tick))
|
||||
}
|
||||
}, [isError, isLoading, IsSyncing, tickData, isValid])
|
||||
|
||||
return useMemo(
|
||||
() => ({ isLoading, IsSyncing, isError, isValid, tickData: tickDataLatestSynced }),
|
||||
[isLoading, IsSyncing, isError, isValid, tickDataLatestSynced]
|
||||
)
|
||||
}
|
||||
|
||||
function useTicksFromSubgraph(
|
||||
currencyA: Currency | undefined,
|
||||
currencyB: Currency | undefined,
|
||||
feeAmount: FeeAmount | undefined
|
||||
@@ -32,19 +143,34 @@ export function useAllV3Ticks(
|
||||
const poolAddress =
|
||||
currencyA && currencyB && feeAmount ? Pool.getAddress(currencyA?.wrapped, currencyB?.wrapped, feeAmount) : undefined
|
||||
|
||||
const { isLoading, isError, error, isUninitialized, data } = useAllV3TicksQuery(
|
||||
poolAddress ? { poolAddress: poolAddress?.toLowerCase(), skip: 0 } : skipToken,
|
||||
{
|
||||
pollingInterval: ms`30s`,
|
||||
}
|
||||
)
|
||||
return useAllV3TicksQuery(poolAddress ? { poolAddress: poolAddress?.toLowerCase(), skip: 0 } : skipToken, {
|
||||
pollingInterval: ms`30s`,
|
||||
})
|
||||
}
|
||||
|
||||
// Fetches all ticks for a given pool
|
||||
function useAllV3Ticks(
|
||||
currencyA: Currency | undefined,
|
||||
currencyB: Currency | undefined,
|
||||
feeAmount: FeeAmount | undefined
|
||||
): {
|
||||
isLoading: boolean
|
||||
isUninitialized: boolean
|
||||
isError: boolean
|
||||
error: unknown
|
||||
ticks: TickData[] | undefined
|
||||
} {
|
||||
const useSubgraph = currencyA ? !CHAIN_IDS_MISSING_SUBGRAPH_DATA.includes(currencyA.chainId) : true
|
||||
|
||||
const tickLensTickData = useTicksFromTickLens(!useSubgraph ? currencyA : undefined, currencyB, feeAmount)
|
||||
const subgraphTickData = useTicksFromSubgraph(useSubgraph ? currencyA : undefined, currencyB, feeAmount)
|
||||
|
||||
return {
|
||||
isLoading,
|
||||
isUninitialized,
|
||||
isError,
|
||||
error,
|
||||
ticks: data?.ticks as AllV3TicksQuery['ticks'],
|
||||
isLoading: useSubgraph ? subgraphTickData.isLoading : tickLensTickData.isLoading,
|
||||
isUninitialized: useSubgraph ? subgraphTickData.isUninitialized : false,
|
||||
isError: useSubgraph ? subgraphTickData.isError : tickLensTickData.isError,
|
||||
error: useSubgraph ? subgraphTickData.error : tickLensTickData.isError,
|
||||
ticks: useSubgraph ? subgraphTickData.data?.ticks : tickLensTickData.tickData,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -94,7 +220,7 @@ export function usePoolActiveLiquidity(
|
||||
// find where the active tick would be to partition the array
|
||||
// if the active tick is initialized, the pivot will be an element
|
||||
// if not, take the previous tick as pivot
|
||||
const pivot = ticks.findIndex(({ tickIdx }) => tickIdx > activeTick) - 1
|
||||
const pivot = ticks.findIndex(({ tick }) => tick > activeTick) - 1
|
||||
|
||||
if (pivot < 0) {
|
||||
// consider setting a local error
|
||||
@@ -111,9 +237,8 @@ export function usePoolActiveLiquidity(
|
||||
|
||||
const activeTickProcessed: TickProcessed = {
|
||||
liquidityActive: JSBI.BigInt(pool[1]?.liquidity ?? 0),
|
||||
tickIdx: activeTick,
|
||||
liquidityNet:
|
||||
Number(ticks[pivot].tickIdx) === activeTick ? JSBI.BigInt(ticks[pivot].liquidityNet) : JSBI.BigInt(0),
|
||||
tick: activeTick,
|
||||
liquidityNet: Number(ticks[pivot].tick) === activeTick ? JSBI.BigInt(ticks[pivot].liquidityNet) : JSBI.BigInt(0),
|
||||
price0: tickToPrice(token0, token1, activeTick).toFixed(PRICE_FIXED_DIGITS),
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Interface } from '@ethersproject/abi'
|
||||
import { Currency, Token } from '@uniswap/sdk-core'
|
||||
import { abi as IUniswapV3PoolStateABI } from '@uniswap/v3-core/artifacts/contracts/interfaces/pool/IUniswapV3PoolState.sol/IUniswapV3PoolState.json'
|
||||
import IUniswapV3PoolStateJson from '@uniswap/v3-core/artifacts/contracts/interfaces/pool/IUniswapV3PoolState.sol/IUniswapV3PoolState.json'
|
||||
import { computePoolAddress } from '@uniswap/v3-sdk'
|
||||
import { FeeAmount, Pool } from '@uniswap/v3-sdk'
|
||||
import useActiveWeb3React from 'hooks/useActiveWeb3React'
|
||||
@@ -10,6 +10,8 @@ import { useMemo } from 'react'
|
||||
import { V3_CORE_FACTORY_ADDRESSES } from '../constants/addresses'
|
||||
import { IUniswapV3PoolStateInterface } from '../types/v3/IUniswapV3PoolState'
|
||||
|
||||
const { abi: IUniswapV3PoolStateABI } = IUniswapV3PoolStateJson
|
||||
|
||||
const POOL_STATE_INTERFACE = new Interface(IUniswapV3PoolStateABI) as IUniswapV3PoolStateInterface
|
||||
|
||||
export enum PoolState {
|
||||
|
||||
184
src/hooks/useSwapCallArguments.tsx
Normal file
184
src/hooks/useSwapCallArguments.tsx
Normal file
@@ -0,0 +1,184 @@
|
||||
import { BigNumber } from '@ethersproject/bignumber'
|
||||
import { SwapRouter, Trade } from '@uniswap/router-sdk'
|
||||
import { Currency, Percent, TradeType } from '@uniswap/sdk-core'
|
||||
import { Router as V2SwapRouter, Trade as V2Trade } from '@uniswap/v2-sdk'
|
||||
import { FeeOptions, SwapRouter as V3SwapRouter, Trade as V3Trade } from '@uniswap/v3-sdk'
|
||||
import { SWAP_ROUTER_ADDRESSES, V3_ROUTER_ADDRESS } from 'constants/addresses'
|
||||
import useActiveWeb3React from 'hooks/useActiveWeb3React'
|
||||
import { useMemo } from 'react'
|
||||
import approveAmountCalldata from 'utils/approveAmountCalldata'
|
||||
|
||||
import { useArgentWalletContract } from './useArgentWalletContract'
|
||||
import { useV2RouterContract } from './useContract'
|
||||
import useENS from './useENS'
|
||||
import { SignatureData } from './useERC20Permit'
|
||||
|
||||
export type AnyTrade =
|
||||
| V2Trade<Currency, Currency, TradeType>
|
||||
| V3Trade<Currency, Currency, TradeType>
|
||||
| Trade<Currency, Currency, TradeType>
|
||||
|
||||
interface SwapCall {
|
||||
address: string
|
||||
calldata: string
|
||||
value: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the swap calls that can be used to make the trade
|
||||
* @param trade trade to execute
|
||||
* @param allowedSlippage user allowed slippage
|
||||
* @param recipientAddressOrName the ENS name or address of the recipient of the swap output
|
||||
* @param signatureData the signature data of the permit of the input token amount, if available
|
||||
*/
|
||||
export function useSwapCallArguments(
|
||||
trade: AnyTrade | undefined,
|
||||
allowedSlippage: Percent,
|
||||
recipientAddressOrName: string | null | undefined,
|
||||
signatureData: SignatureData | null | undefined,
|
||||
deadline: BigNumber | undefined,
|
||||
feeOptions: FeeOptions | undefined
|
||||
): SwapCall[] {
|
||||
const { account, chainId, library } = useActiveWeb3React()
|
||||
|
||||
const { address: recipientAddress } = useENS(recipientAddressOrName)
|
||||
const recipient = recipientAddressOrName === null ? account : recipientAddress
|
||||
const routerContract = useV2RouterContract()
|
||||
const argentWalletContract = useArgentWalletContract()
|
||||
|
||||
return useMemo(() => {
|
||||
if (!trade || !recipient || !library || !account || !chainId || !deadline) return []
|
||||
|
||||
if (trade instanceof V2Trade) {
|
||||
if (!routerContract) return []
|
||||
const swapMethods = []
|
||||
|
||||
swapMethods.push(
|
||||
V2SwapRouter.swapCallParameters(trade, {
|
||||
feeOnTransfer: false,
|
||||
allowedSlippage,
|
||||
recipient,
|
||||
deadline: deadline.toNumber(),
|
||||
})
|
||||
)
|
||||
|
||||
if (trade.tradeType === TradeType.EXACT_INPUT) {
|
||||
swapMethods.push(
|
||||
V2SwapRouter.swapCallParameters(trade, {
|
||||
feeOnTransfer: true,
|
||||
allowedSlippage,
|
||||
recipient,
|
||||
deadline: deadline.toNumber(),
|
||||
})
|
||||
)
|
||||
}
|
||||
return swapMethods.map(({ methodName, args, value }) => {
|
||||
if (argentWalletContract && trade.inputAmount.currency.isToken) {
|
||||
return {
|
||||
address: argentWalletContract.address,
|
||||
calldata: argentWalletContract.interface.encodeFunctionData('wc_multiCall', [
|
||||
[
|
||||
approveAmountCalldata(trade.maximumAmountIn(allowedSlippage), routerContract.address),
|
||||
{
|
||||
to: routerContract.address,
|
||||
value,
|
||||
data: routerContract.interface.encodeFunctionData(methodName, args),
|
||||
},
|
||||
],
|
||||
]),
|
||||
value: '0x0',
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
address: routerContract.address,
|
||||
calldata: routerContract.interface.encodeFunctionData(methodName, args),
|
||||
value,
|
||||
}
|
||||
}
|
||||
})
|
||||
} else {
|
||||
// swap options shared by v3 and v2+v3 swap routers
|
||||
const sharedSwapOptions = {
|
||||
fee: feeOptions,
|
||||
recipient,
|
||||
slippageTolerance: allowedSlippage,
|
||||
...(signatureData
|
||||
? {
|
||||
inputTokenPermit:
|
||||
'allowed' in signatureData
|
||||
? {
|
||||
expiry: signatureData.deadline,
|
||||
nonce: signatureData.nonce,
|
||||
s: signatureData.s,
|
||||
r: signatureData.r,
|
||||
v: signatureData.v as any,
|
||||
}
|
||||
: {
|
||||
deadline: signatureData.deadline,
|
||||
amount: signatureData.amount,
|
||||
s: signatureData.s,
|
||||
r: signatureData.r,
|
||||
v: signatureData.v as any,
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
}
|
||||
|
||||
const swapRouterAddress = chainId
|
||||
? trade instanceof V3Trade
|
||||
? V3_ROUTER_ADDRESS[chainId]
|
||||
: SWAP_ROUTER_ADDRESSES[chainId]
|
||||
: undefined
|
||||
if (!swapRouterAddress) return []
|
||||
|
||||
const { value, calldata } =
|
||||
trade instanceof V3Trade
|
||||
? V3SwapRouter.swapCallParameters(trade, {
|
||||
...sharedSwapOptions,
|
||||
deadline: deadline.toString(),
|
||||
})
|
||||
: SwapRouter.swapCallParameters(trade, {
|
||||
...sharedSwapOptions,
|
||||
deadlineOrPreviousBlockhash: deadline.toString(),
|
||||
})
|
||||
|
||||
if (argentWalletContract && trade.inputAmount.currency.isToken) {
|
||||
return [
|
||||
{
|
||||
address: argentWalletContract.address,
|
||||
calldata: argentWalletContract.interface.encodeFunctionData('wc_multiCall', [
|
||||
[
|
||||
approveAmountCalldata(trade.maximumAmountIn(allowedSlippage), swapRouterAddress),
|
||||
{
|
||||
to: swapRouterAddress,
|
||||
value,
|
||||
data: calldata,
|
||||
},
|
||||
],
|
||||
]),
|
||||
value: '0x0',
|
||||
},
|
||||
]
|
||||
}
|
||||
return [
|
||||
{
|
||||
address: swapRouterAddress,
|
||||
calldata,
|
||||
value,
|
||||
},
|
||||
]
|
||||
}
|
||||
}, [
|
||||
account,
|
||||
allowedSlippage,
|
||||
argentWalletContract,
|
||||
chainId,
|
||||
deadline,
|
||||
feeOptions,
|
||||
library,
|
||||
recipient,
|
||||
routerContract,
|
||||
signatureData,
|
||||
trade,
|
||||
])
|
||||
}
|
||||
@@ -1,289 +1,17 @@
|
||||
import { BigNumber } from '@ethersproject/bignumber'
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { t, Trans } from '@lingui/macro'
|
||||
import { SwapRouter, Trade } from '@uniswap/router-sdk'
|
||||
import { Currency, Percent, TradeType } from '@uniswap/sdk-core'
|
||||
import { Router as V2SwapRouter, Trade as V2Trade } from '@uniswap/v2-sdk'
|
||||
import { SwapRouter as V3SwapRouter, Trade as V3Trade } from '@uniswap/v3-sdk'
|
||||
import { Percent, TradeType } from '@uniswap/sdk-core'
|
||||
import useActiveWeb3React from 'hooks/useActiveWeb3React'
|
||||
import { SwapCallbackState, useSwapCallback as useLibSwapCallBack } from 'lib/hooks/swap/useSwapCallback'
|
||||
import { ReactNode, useMemo } from 'react'
|
||||
|
||||
import { SWAP_ROUTER_ADDRESSES, V3_ROUTER_ADDRESS } from '../constants/addresses'
|
||||
import { TransactionType } from '../state/transactions/actions'
|
||||
import { useTransactionAdder } from '../state/transactions/hooks'
|
||||
import approveAmountCalldata from '../utils/approveAmountCalldata'
|
||||
import { calculateGasMargin } from '../utils/calculateGasMargin'
|
||||
import { currencyId } from '../utils/currencyId'
|
||||
import isZero from '../utils/isZero'
|
||||
import { useArgentWalletContract } from './useArgentWalletContract'
|
||||
import { useV2RouterContract } from './useContract'
|
||||
import useENS from './useENS'
|
||||
import { SignatureData } from './useERC20Permit'
|
||||
import { AnyTrade } from './useSwapCallArguments'
|
||||
import useTransactionDeadline from './useTransactionDeadline'
|
||||
|
||||
type AnyTrade =
|
||||
| V2Trade<Currency, Currency, TradeType>
|
||||
| V3Trade<Currency, Currency, TradeType>
|
||||
| Trade<Currency, Currency, TradeType>
|
||||
|
||||
enum SwapCallbackState {
|
||||
INVALID,
|
||||
LOADING,
|
||||
VALID,
|
||||
}
|
||||
|
||||
interface SwapCall {
|
||||
address: string
|
||||
calldata: string
|
||||
value: string
|
||||
}
|
||||
|
||||
interface SwapCallEstimate {
|
||||
call: SwapCall
|
||||
}
|
||||
|
||||
interface SuccessfulCall extends SwapCallEstimate {
|
||||
call: SwapCall
|
||||
gasEstimate: BigNumber
|
||||
}
|
||||
|
||||
interface FailedCall extends SwapCallEstimate {
|
||||
call: SwapCall
|
||||
error: Error
|
||||
}
|
||||
/**
|
||||
* Returns the swap calls that can be used to make the trade
|
||||
* @param trade trade to execute
|
||||
* @param allowedSlippage user allowed slippage
|
||||
* @param recipientAddressOrName the ENS name or address of the recipient of the swap output
|
||||
* @param signatureData the signature data of the permit of the input token amount, if available
|
||||
*/
|
||||
function useSwapCallArguments(
|
||||
trade: AnyTrade | undefined, // trade to execute, required
|
||||
allowedSlippage: Percent, // in bips
|
||||
recipientAddressOrName: string | null, // the ENS name or address of the recipient of the trade, or null if swap should be returned to sender
|
||||
signatureData: SignatureData | null | undefined
|
||||
): SwapCall[] {
|
||||
const { account, chainId, library } = useActiveWeb3React()
|
||||
|
||||
const { address: recipientAddress } = useENS(recipientAddressOrName)
|
||||
const recipient = recipientAddressOrName === null ? account : recipientAddress
|
||||
const deadline = useTransactionDeadline()
|
||||
const routerContract = useV2RouterContract()
|
||||
const argentWalletContract = useArgentWalletContract()
|
||||
|
||||
return useMemo(() => {
|
||||
if (!trade || !recipient || !library || !account || !chainId || !deadline) return []
|
||||
|
||||
if (trade instanceof V2Trade) {
|
||||
if (!routerContract) return []
|
||||
const swapMethods = []
|
||||
|
||||
swapMethods.push(
|
||||
V2SwapRouter.swapCallParameters(trade, {
|
||||
feeOnTransfer: false,
|
||||
allowedSlippage,
|
||||
recipient,
|
||||
deadline: deadline.toNumber(),
|
||||
})
|
||||
)
|
||||
|
||||
if (trade.tradeType === TradeType.EXACT_INPUT) {
|
||||
swapMethods.push(
|
||||
V2SwapRouter.swapCallParameters(trade, {
|
||||
feeOnTransfer: true,
|
||||
allowedSlippage,
|
||||
recipient,
|
||||
deadline: deadline.toNumber(),
|
||||
})
|
||||
)
|
||||
}
|
||||
return swapMethods.map(({ methodName, args, value }) => {
|
||||
if (argentWalletContract && trade.inputAmount.currency.isToken) {
|
||||
return {
|
||||
address: argentWalletContract.address,
|
||||
calldata: argentWalletContract.interface.encodeFunctionData('wc_multiCall', [
|
||||
[
|
||||
approveAmountCalldata(trade.maximumAmountIn(allowedSlippage), routerContract.address),
|
||||
{
|
||||
to: routerContract.address,
|
||||
value,
|
||||
data: routerContract.interface.encodeFunctionData(methodName, args),
|
||||
},
|
||||
],
|
||||
]),
|
||||
value: '0x0',
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
address: routerContract.address,
|
||||
calldata: routerContract.interface.encodeFunctionData(methodName, args),
|
||||
value,
|
||||
}
|
||||
}
|
||||
})
|
||||
} else {
|
||||
// swap options shared by v3 and v2+v3 swap routers
|
||||
const sharedSwapOptions = {
|
||||
recipient,
|
||||
slippageTolerance: allowedSlippage,
|
||||
...(signatureData
|
||||
? {
|
||||
inputTokenPermit:
|
||||
'allowed' in signatureData
|
||||
? {
|
||||
expiry: signatureData.deadline,
|
||||
nonce: signatureData.nonce,
|
||||
s: signatureData.s,
|
||||
r: signatureData.r,
|
||||
v: signatureData.v as any,
|
||||
}
|
||||
: {
|
||||
deadline: signatureData.deadline,
|
||||
amount: signatureData.amount,
|
||||
s: signatureData.s,
|
||||
r: signatureData.r,
|
||||
v: signatureData.v as any,
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
}
|
||||
|
||||
const swapRouterAddress = chainId
|
||||
? trade instanceof V3Trade
|
||||
? V3_ROUTER_ADDRESS[chainId]
|
||||
: SWAP_ROUTER_ADDRESSES[chainId]
|
||||
: undefined
|
||||
if (!swapRouterAddress) return []
|
||||
|
||||
const { value, calldata } =
|
||||
trade instanceof V3Trade
|
||||
? V3SwapRouter.swapCallParameters(trade, {
|
||||
...sharedSwapOptions,
|
||||
deadline: deadline.toString(),
|
||||
})
|
||||
: SwapRouter.swapCallParameters(trade, {
|
||||
...sharedSwapOptions,
|
||||
deadlineOrPreviousBlockhash: deadline.toString(),
|
||||
})
|
||||
|
||||
if (argentWalletContract && trade.inputAmount.currency.isToken) {
|
||||
return [
|
||||
{
|
||||
address: argentWalletContract.address,
|
||||
calldata: argentWalletContract.interface.encodeFunctionData('wc_multiCall', [
|
||||
[
|
||||
approveAmountCalldata(trade.maximumAmountIn(allowedSlippage), swapRouterAddress),
|
||||
{
|
||||
to: swapRouterAddress,
|
||||
value,
|
||||
data: calldata,
|
||||
},
|
||||
],
|
||||
]),
|
||||
value: '0x0',
|
||||
},
|
||||
]
|
||||
}
|
||||
return [
|
||||
{
|
||||
address: swapRouterAddress,
|
||||
calldata,
|
||||
value,
|
||||
},
|
||||
]
|
||||
}
|
||||
}, [
|
||||
trade,
|
||||
recipient,
|
||||
library,
|
||||
account,
|
||||
chainId,
|
||||
deadline,
|
||||
routerContract,
|
||||
allowedSlippage,
|
||||
argentWalletContract,
|
||||
signatureData,
|
||||
])
|
||||
}
|
||||
|
||||
/**
|
||||
* This is hacking out the revert reason from the ethers provider thrown error however it can.
|
||||
* This object seems to be undocumented by ethers.
|
||||
* @param error an error from the ethers provider
|
||||
*/
|
||||
function swapErrorToUserReadableMessage(error: any): ReactNode {
|
||||
let reason: string | undefined
|
||||
while (Boolean(error)) {
|
||||
reason = error.reason ?? error.message ?? reason
|
||||
error = error.error ?? error.data?.originalError
|
||||
}
|
||||
|
||||
if (reason?.indexOf('execution reverted: ') === 0) reason = reason.substr('execution reverted: '.length)
|
||||
|
||||
switch (reason) {
|
||||
case 'UniswapV2Router: EXPIRED':
|
||||
return (
|
||||
<Trans>
|
||||
The transaction could not be sent because the deadline has passed. Please check that your transaction deadline
|
||||
is not too low.
|
||||
</Trans>
|
||||
)
|
||||
case 'UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT':
|
||||
case 'UniswapV2Router: EXCESSIVE_INPUT_AMOUNT':
|
||||
return (
|
||||
<Trans>
|
||||
This transaction will not succeed either due to price movement or fee on transfer. Try increasing your
|
||||
slippage tolerance.
|
||||
</Trans>
|
||||
)
|
||||
case 'TransferHelper: TRANSFER_FROM_FAILED':
|
||||
return <Trans>The input token cannot be transferred. There may be an issue with the input token.</Trans>
|
||||
case 'UniswapV2: TRANSFER_FAILED':
|
||||
return <Trans>The output token cannot be transferred. There may be an issue with the output token.</Trans>
|
||||
case 'UniswapV2: K':
|
||||
return (
|
||||
<Trans>
|
||||
The Uniswap invariant x*y=k was not satisfied by the swap. This usually means one of the tokens you are
|
||||
swapping incorporates custom behavior on transfer.
|
||||
</Trans>
|
||||
)
|
||||
case 'Too little received':
|
||||
case 'Too much requested':
|
||||
case 'STF':
|
||||
return (
|
||||
<Trans>
|
||||
This transaction will not succeed due to price movement. Try increasing your slippage tolerance. Note: fee on
|
||||
transfer and rebase tokens are incompatible with Uniswap V3.
|
||||
</Trans>
|
||||
)
|
||||
case 'TF':
|
||||
return (
|
||||
<Trans>
|
||||
The output token cannot be transferred. There may be an issue with the output token. Note: fee on transfer and
|
||||
rebase tokens are incompatible with Uniswap V3.
|
||||
</Trans>
|
||||
)
|
||||
default:
|
||||
if (reason?.indexOf('undefined is not an object') !== -1) {
|
||||
console.error(error, reason)
|
||||
return (
|
||||
<Trans>
|
||||
An error occurred when trying to execute this swap. You may need to increase your slippage tolerance. If
|
||||
that does not work, there may be an incompatibility with the token you are trading. Note: fee on transfer
|
||||
and rebase tokens are incompatible with Uniswap V3.
|
||||
</Trans>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<Trans>
|
||||
Unknown error{reason ? `: "${reason}"` : ''}. Try increasing your slippage tolerance. Note: fee on transfer
|
||||
and rebase tokens are incompatible with Uniswap V3.
|
||||
</Trans>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// returns a function that will execute a swap, if the parameters are all valid
|
||||
// and the user has approved the slippage adjusted input amount for the trade
|
||||
export function useSwapCallback(
|
||||
@@ -292,139 +20,56 @@ export function useSwapCallback(
|
||||
recipientAddressOrName: string | null, // the ENS name or address of the recipient of the trade, or null if swap should be returned to sender
|
||||
signatureData: SignatureData | undefined | null
|
||||
): { state: SwapCallbackState; callback: null | (() => Promise<string>); error: ReactNode | null } {
|
||||
const { account, chainId, library } = useActiveWeb3React()
|
||||
const { account } = useActiveWeb3React()
|
||||
|
||||
const swapCalls = useSwapCallArguments(trade, allowedSlippage, recipientAddressOrName, signatureData)
|
||||
const deadline = useTransactionDeadline()
|
||||
|
||||
const addTransaction = useTransactionAdder()
|
||||
|
||||
const { address: recipientAddress } = useENS(recipientAddressOrName)
|
||||
const recipient = recipientAddressOrName === null ? account : recipientAddress
|
||||
|
||||
return useMemo(() => {
|
||||
if (!trade || !library || !account || !chainId) {
|
||||
return { state: SwapCallbackState.INVALID, callback: null, error: <Trans>Missing dependencies</Trans> }
|
||||
const {
|
||||
state,
|
||||
callback: libCallback,
|
||||
error,
|
||||
} = useLibSwapCallBack({ trade, allowedSlippage, recipientAddressOrName: recipient, signatureData, deadline })
|
||||
|
||||
const callback = useMemo(() => {
|
||||
if (!libCallback || !trade) {
|
||||
return null
|
||||
}
|
||||
if (!recipient) {
|
||||
if (recipientAddressOrName !== null) {
|
||||
return { state: SwapCallbackState.INVALID, callback: null, error: <Trans>Invalid recipient</Trans> }
|
||||
} else {
|
||||
return { state: SwapCallbackState.LOADING, callback: null, error: null }
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
state: SwapCallbackState.VALID,
|
||||
callback: async function onSwap(): Promise<string> {
|
||||
const estimatedCalls: SwapCallEstimate[] = await Promise.all(
|
||||
swapCalls.map((call) => {
|
||||
const { address, calldata, value } = call
|
||||
|
||||
const tx =
|
||||
!value || isZero(value)
|
||||
? { from: account, to: address, data: calldata }
|
||||
: {
|
||||
from: account,
|
||||
to: address,
|
||||
data: calldata,
|
||||
value,
|
||||
}
|
||||
|
||||
return library
|
||||
.estimateGas(tx)
|
||||
.then((gasEstimate) => {
|
||||
return {
|
||||
call,
|
||||
gasEstimate,
|
||||
}
|
||||
})
|
||||
.catch((gasError) => {
|
||||
console.debug('Gas estimate failed, trying eth_call to extract error', call)
|
||||
|
||||
return library
|
||||
.call(tx)
|
||||
.then((result) => {
|
||||
console.debug('Unexpected successful call after failed estimate gas', call, gasError, result)
|
||||
return { call, error: <Trans>Unexpected issue with estimating the gas. Please try again.</Trans> }
|
||||
})
|
||||
.catch((callError) => {
|
||||
console.debug('Call threw error', call, callError)
|
||||
return { call, error: swapErrorToUserReadableMessage(callError) }
|
||||
})
|
||||
})
|
||||
})
|
||||
return () =>
|
||||
libCallback().then((response) => {
|
||||
addTransaction(
|
||||
response,
|
||||
trade.tradeType === TradeType.EXACT_INPUT
|
||||
? {
|
||||
type: TransactionType.SWAP,
|
||||
tradeType: TradeType.EXACT_INPUT,
|
||||
inputCurrencyId: currencyId(trade.inputAmount.currency),
|
||||
inputCurrencyAmountRaw: trade.inputAmount.quotient.toString(),
|
||||
expectedOutputCurrencyAmountRaw: trade.outputAmount.quotient.toString(),
|
||||
outputCurrencyId: currencyId(trade.outputAmount.currency),
|
||||
minimumOutputCurrencyAmountRaw: trade.minimumAmountOut(allowedSlippage).quotient.toString(),
|
||||
}
|
||||
: {
|
||||
type: TransactionType.SWAP,
|
||||
tradeType: TradeType.EXACT_OUTPUT,
|
||||
inputCurrencyId: currencyId(trade.inputAmount.currency),
|
||||
maximumInputCurrencyAmountRaw: trade.maximumAmountIn(allowedSlippage).quotient.toString(),
|
||||
outputCurrencyId: currencyId(trade.outputAmount.currency),
|
||||
outputCurrencyAmountRaw: trade.outputAmount.quotient.toString(),
|
||||
expectedInputCurrencyAmountRaw: trade.inputAmount.quotient.toString(),
|
||||
}
|
||||
)
|
||||
return response.hash
|
||||
})
|
||||
}, [addTransaction, allowedSlippage, libCallback, trade])
|
||||
|
||||
// a successful estimation is a bignumber gas estimate and the next call is also a bignumber gas estimate
|
||||
let bestCallOption: SuccessfulCall | SwapCallEstimate | undefined = estimatedCalls.find(
|
||||
(el, ix, list): el is SuccessfulCall =>
|
||||
'gasEstimate' in el && (ix === list.length - 1 || 'gasEstimate' in list[ix + 1])
|
||||
)
|
||||
|
||||
// check if any calls errored with a recognizable error
|
||||
if (!bestCallOption) {
|
||||
const errorCalls = estimatedCalls.filter((call): call is FailedCall => 'error' in call)
|
||||
if (errorCalls.length > 0) throw errorCalls[errorCalls.length - 1].error
|
||||
const firstNoErrorCall = estimatedCalls.find<SwapCallEstimate>(
|
||||
(call): call is SwapCallEstimate => !('error' in call)
|
||||
)
|
||||
if (!firstNoErrorCall) throw new Error(t`Unexpected error. Could not estimate gas for the swap.`)
|
||||
bestCallOption = firstNoErrorCall
|
||||
}
|
||||
|
||||
const {
|
||||
call: { address, calldata, value },
|
||||
} = bestCallOption
|
||||
|
||||
return library
|
||||
.getSigner()
|
||||
.sendTransaction({
|
||||
from: account,
|
||||
to: address,
|
||||
data: calldata,
|
||||
// let the wallet try if we can't estimate the gas
|
||||
...('gasEstimate' in bestCallOption ? { gasLimit: calculateGasMargin(bestCallOption.gasEstimate) } : {}),
|
||||
...(value && !isZero(value) ? { value } : {}),
|
||||
})
|
||||
.then((response) => {
|
||||
addTransaction(
|
||||
response,
|
||||
trade.tradeType === TradeType.EXACT_INPUT
|
||||
? {
|
||||
type: TransactionType.SWAP,
|
||||
tradeType: TradeType.EXACT_INPUT,
|
||||
inputCurrencyId: currencyId(trade.inputAmount.currency),
|
||||
inputCurrencyAmountRaw: trade.inputAmount.quotient.toString(),
|
||||
expectedOutputCurrencyAmountRaw: trade.outputAmount.quotient.toString(),
|
||||
outputCurrencyId: currencyId(trade.outputAmount.currency),
|
||||
minimumOutputCurrencyAmountRaw: trade.minimumAmountOut(allowedSlippage).quotient.toString(),
|
||||
}
|
||||
: {
|
||||
type: TransactionType.SWAP,
|
||||
tradeType: TradeType.EXACT_OUTPUT,
|
||||
inputCurrencyId: currencyId(trade.inputAmount.currency),
|
||||
maximumInputCurrencyAmountRaw: trade.maximumAmountIn(allowedSlippage).quotient.toString(),
|
||||
outputCurrencyId: currencyId(trade.outputAmount.currency),
|
||||
outputCurrencyAmountRaw: trade.outputAmount.quotient.toString(),
|
||||
expectedInputCurrencyAmountRaw: trade.inputAmount.quotient.toString(),
|
||||
}
|
||||
)
|
||||
|
||||
return response.hash
|
||||
})
|
||||
.catch((error) => {
|
||||
// if the user rejected the tx, pass this along
|
||||
if (error?.code === 4001) {
|
||||
throw new Error(t`Transaction rejected.`)
|
||||
} else {
|
||||
// otherwise, the error was unexpected and we need to convey that
|
||||
console.error(`Swap failed`, error, address, calldata, value)
|
||||
|
||||
throw new Error(t`Swap failed: ${swapErrorToUserReadableMessage(error)}`)
|
||||
}
|
||||
})
|
||||
},
|
||||
error: null,
|
||||
}
|
||||
}, [trade, library, account, chainId, recipient, recipientAddressOrName, swapCalls, addTransaction, allowedSlippage])
|
||||
return {
|
||||
state,
|
||||
callback,
|
||||
error,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Currency, CurrencyAmount, Price, Token, TradeType } from '@uniswap/sdk-core'
|
||||
import useActiveWeb3React from 'hooks/useActiveWeb3React'
|
||||
import tryParseCurrencyAmount from 'lib/utils/tryParseCurrencyAmount'
|
||||
import { useMemo } from 'react'
|
||||
import { tryParseAmount } from 'state/swap/hooks'
|
||||
|
||||
import { SupportedChainId } from '../constants/chains'
|
||||
import { DAI_OPTIMISM, USDC, USDC_ARBITRUM, USDC_POLYGON } from '../constants/tokens'
|
||||
@@ -87,7 +87,7 @@ export function useStablecoinAmountFromFiatValue(fiatValue: string | null | unde
|
||||
|
||||
try {
|
||||
// parse USD string into CurrencyAmount based on stablecoin decimals
|
||||
return tryParseAmount(parsedForDecimals, stablecoin)
|
||||
return tryParseCurrencyAmount(parsedForDecimals, stablecoin)
|
||||
} catch (error) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
18
src/hooks/useV2LiquidityTokenPermit.ts
Normal file
18
src/hooks/useV2LiquidityTokenPermit.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { CurrencyAmount, Token } from '@uniswap/sdk-core'
|
||||
|
||||
import { PermitInfo, PermitType, useERC20Permit } from './useERC20Permit'
|
||||
import useTransactionDeadline from './useTransactionDeadline'
|
||||
|
||||
const REMOVE_V2_LIQUIDITY_PERMIT_INFO: PermitInfo = {
|
||||
version: '1',
|
||||
name: 'Uniswap V2',
|
||||
type: PermitType.AMOUNT,
|
||||
}
|
||||
|
||||
export function useV2LiquidityTokenPermit(
|
||||
liquidityAmount: CurrencyAmount<Token> | null | undefined,
|
||||
spender: string | null | undefined
|
||||
) {
|
||||
const transactionDeadline = useTransactionDeadline()
|
||||
return useERC20Permit(liquidityAmount, spender, transactionDeadline, REMOVE_V2_LIQUIDITY_PERMIT_INFO)
|
||||
}
|
||||
@@ -1,12 +1,14 @@
|
||||
import { Interface } from '@ethersproject/abi'
|
||||
import { Currency, CurrencyAmount } from '@uniswap/sdk-core'
|
||||
import { abi as IUniswapV2PairABI } from '@uniswap/v2-core/build/IUniswapV2Pair.json'
|
||||
import IUniswapV2PairJson from '@uniswap/v2-core/build/IUniswapV2Pair.json'
|
||||
import { computePairAddress, Pair } from '@uniswap/v2-sdk'
|
||||
import { useMultipleContractSingleData } from 'lib/hooks/multicall'
|
||||
import { useMemo } from 'react'
|
||||
|
||||
import { V2_FACTORY_ADDRESSES } from '../constants/addresses'
|
||||
|
||||
const { abi: IUniswapV2PairABI } = IUniswapV2PairJson
|
||||
|
||||
const PAIR_INTERFACE = new Interface(IUniswapV2PairABI)
|
||||
|
||||
export enum PairState {
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import { Currency } from '@uniswap/sdk-core'
|
||||
import useActiveWeb3React from 'hooks/useActiveWeb3React'
|
||||
import useNativeCurrency from 'lib/hooks/useNativeCurrency'
|
||||
import tryParseCurrencyAmount from 'lib/utils/tryParseCurrencyAmount'
|
||||
import { useMemo } from 'react'
|
||||
|
||||
import { WRAPPED_NATIVE_CURRENCY } from '../constants/tokens'
|
||||
import { tryParseAmount } from '../state/swap/hooks'
|
||||
import { TransactionType } from '../state/transactions/actions'
|
||||
import { useTransactionAdder } from '../state/transactions/hooks'
|
||||
import { useCurrencyBalance } from '../state/wallet/hooks'
|
||||
import { useNativeCurrency } from './Tokens'
|
||||
import { useWETHContract } from './useContract'
|
||||
|
||||
export enum WrapType {
|
||||
@@ -61,7 +61,10 @@ export default function useWrapCallback(
|
||||
const wethContract = useWETHContract()
|
||||
const balance = useCurrencyBalance(account ?? undefined, inputCurrency ?? undefined)
|
||||
// we can always parse the amount typed as the input currency, since wrapping is 1:1
|
||||
const inputAmount = useMemo(() => tryParseAmount(typedValue, inputCurrency ?? undefined), [inputCurrency, typedValue])
|
||||
const inputAmount = useMemo(
|
||||
() => tryParseCurrencyAmount(typedValue, inputCurrency ?? undefined),
|
||||
[inputCurrency, typedValue]
|
||||
)
|
||||
const addTransaction = useTransactionAdder()
|
||||
|
||||
return useMemo(() => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useWeb3React } from '@web3-react/core'
|
||||
import type { EthereumProvider } from 'lib/ethereum'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useWeb3React } from 'web3-react-core'
|
||||
|
||||
import { gnosisSafe, injected } from '../connectors'
|
||||
import { IS_IN_IFRAME } from '../constants/misc'
|
||||
|
||||
@@ -3,13 +3,13 @@ import 'inter-ui'
|
||||
import 'polyfills'
|
||||
import 'components/analytics'
|
||||
|
||||
import { createWeb3ReactRoot, Web3ReactProvider } from '@web3-react/core'
|
||||
import { BlockUpdater } from 'lib/hooks/useBlockNumber'
|
||||
import { MulticallUpdater } from 'lib/state/multicall'
|
||||
import { StrictMode } from 'react'
|
||||
import ReactDOM from 'react-dom'
|
||||
import { Provider } from 'react-redux'
|
||||
import { HashRouter } from 'react-router-dom'
|
||||
import { createWeb3ReactRoot, Web3ReactProvider } from 'web3-react-core'
|
||||
|
||||
import Blocklist from './components/Blocklist'
|
||||
import { NetworkContextName } from './constants/misc'
|
||||
@@ -73,4 +73,4 @@ ReactDOM.render(
|
||||
if (process.env.REACT_APP_SERVICE_WORKER !== 'false') {
|
||||
serviceWorkerRegistration.register()
|
||||
}
|
||||
export { INFURA_NETWORK_URLS } from 'constants/chainInfo'
|
||||
export { INFURA_NETWORK_URLS } from 'constants/infura'
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
import * as React from 'react'
|
||||
|
||||
function SvgCheck(props: React.SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg" {...props}>
|
||||
<circle cx={10} cy={10} r={10} />
|
||||
<path d="M14 7l-5.5 5.5L6 10" stroke="#fff" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export default SvgCheck
|
||||
@@ -1,19 +0,0 @@
|
||||
import * as React from 'react'
|
||||
|
||||
function SvgExpando(props: React.SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
strokeWidth={2}
|
||||
strokeLinecap="round"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
{...props}
|
||||
>
|
||||
<path className="expando_svg__left" d="M18 15l-6-6" />
|
||||
<path className="expando_svg__right" d="M12 9l-6 6" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export default SvgExpando
|
||||
@@ -1,18 +0,0 @@
|
||||
import * as React from 'react'
|
||||
|
||||
function SvgLogo(props: React.SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg viewBox="0 0 14 15" xmlns="http://www.w3.org/2000/svg" {...props}>
|
||||
<path d="M4.152 1.551c-.188-.029-.196-.032-.107-.045.17-.026.57.009.846.074.644.152 1.23.542 1.856 1.235l.166.184.238-.038c1.002-.16 2.02-.033 2.873.358.235.108.605.322.65.377.016.018.043.13.06.251.064.418.033.737-.096.976-.07.13-.074.171-.027.283a.274.274 0 00.246.154c.212 0 .44-.34.545-.814l.042-.189.083.094c.457.514.815 1.214.876 1.712l.016.13-.076-.118a1.462 1.462 0 00-.435-.453c-.306-.201-.63-.27-1.486-.315-.774-.04-1.212-.106-1.646-.247-.739-.24-1.111-.558-1.989-1.702-.39-.509-.63-.79-.87-1.016-.545-.515-1.08-.785-1.765-.89z" />
|
||||
<path d="M10.85 2.686c.019-.34.065-.565.159-.77a.825.825 0 01.077-.148c.005 0-.011.06-.036.133-.068.2-.08.472-.032.789.06.402.093.46.52.894.201.204.434.46.519.571l.154.2-.154-.143c-.188-.175-.62-.517-.716-.566-.064-.032-.074-.032-.113.007-.037.036-.044.09-.05.346-.007.399-.062.655-.194.91-.071.14-.082.11-.018-.047.048-.116.053-.168.053-.554 0-.775-.094-.962-.637-1.28a5.971 5.971 0 00-.504-.26 1.912 1.912 0 01-.246-.12c.015-.015.545.139.758.22.318.122.37.137.409.123.025-.01.038-.085.05-.305zM4.517 4.013c-.381-.522-.618-1.323-.566-1.922l.015-.185.087.015c.164.03.445.134.577.214.361.218.518.505.677 1.243.047.216.108.46.136.544.045.133.217.444.356.646.1.146.034.215-.188.195-.339-.03-.798-.345-1.094-.75zM10.386 7.9c-1.784-.713-2.412-1.333-2.412-2.378 0-.154.005-.28.012-.28.006 0 .075.05.153.113.362.288.767.411 1.889.574.66.096 1.03.173 1.373.286 1.09.359 1.763 1.087 1.924 2.08.046.288.02.828-.057 1.113-.06.225-.242.63-.29.646-.014.005-.027-.046-.03-.116-.018-.372-.208-.735-.526-1.007-.362-.309-.848-.555-2.036-1.03zM9.134 8.197a3.133 3.133 0 00-.086-.375l-.046-.135.085.095c.117.13.21.297.288.52.06.17.066.22.066.496 0 .271-.008.328-.064.48a1.518 1.518 0 01-.376.596c-.326.33-.745.512-1.35.588-.105.013-.411.035-.68.049-.679.035-1.126.108-1.527.248a.324.324 0 01-.115.027c-.016-.016.258-.178.483-.286.318-.153.635-.236 1.345-.353.35-.058.713-.129.805-.157.868-.264 1.315-.947 1.172-1.793z" />
|
||||
<path d="M9.952 9.641c-.237-.506-.292-.995-.162-1.451.014-.05.036-.089.05-.089.013 0 .07.03.124.067.11.073.328.196.912.512.728.395 1.144.7 1.426 1.05.247.305.4.654.474 1.078.042.24.017.82-.045 1.062-.196.764-.65 1.364-1.3 1.714-.095.051-.18.093-.19.093-.009 0 .026-.087.077-.194.219-.454.244-.895.079-1.386-.102-.301-.308-.668-.724-1.289-.484-.72-.602-.913-.721-1.167zM3.25 12.374c.663-.556 1.486-.95 2.237-1.072a3.51 3.51 0 011.161.045c.48.122.91.396 1.133.721.218.319.312.596.41 1.214.038.243.08.488.092.543.073.32.216.576.392.704.28.204.764.217 1.239.033a.618.618 0 01.155-.048c.017.017-.222.176-.39.26a1.334 1.334 0 01-.648.156c-.435 0-.796-.22-1.098-.668a5.3 5.3 0 01-.296-.588c-.318-.721-.475-.94-.844-1.181-.322-.21-.737-.247-1.049-.095-.41.2-.524.72-.23 1.05a.911.911 0 00.512.266.545.545 0 00.619-.544c0-.217-.084-.34-.295-.436-.289-.129-.598.022-.597.291 0 .115.051.187.167.24.074.033.076.035.015.023-.264-.055-.326-.372-.114-.582.256-.252.784-.141.965.204.076.145.085.433.019.607-.15.39-.582.595-1.022.483-.3-.076-.421-.158-.782-.527-.627-.642-.87-.767-1.774-.907l-.174-.027.197-.165z" />
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M.308.884C2.402 3.41 3.845 4.452 4.005 4.672c.132.182.082.346-.144.474a1.381 1.381 0 01-.515.143c-.147 0-.198-.056-.198-.056-.085-.08-.133-.066-.57-.837A132.96 132.96 0 001.45 2.67c-.032-.03-.031-.03 1.067 1.923.177.407.035.556.035.614 0 .118-.033.18-.179.343-.244.27-.353.574-.432 1.203-.088.705-.336 1.203-1.024 2.056-.402.499-.468.59-.57.792-.128.253-.163.395-.177.714-.015.339.014.557.118.88.09.284.186.47.429.844.21.323.33.563.33.657 0 .074.014.074.34.001.776-.174 1.407-.48 1.762-.857.22-.233.271-.361.273-.68.001-.208-.006-.252-.063-.372-.092-.195-.26-.358-.63-.61-.486-.33-.694-.595-.75-.96-.048-.3.007-.511.275-1.07.278-.58.347-.827.394-1.41.03-.377.071-.526.18-.646.114-.124.216-.166.498-.204.459-.063.75-.18.99-.4a.853.853 0 00.31-.652l.01-.21-.117-.134C4.098 4.004.026.5 0 .5-.005.5.133.673.308.884zm.976 9.815a.37.37 0 00-.115-.489c-.15-.1-.385-.052-.385.077 0 .04.022.069.072.094.084.043.09.091.024.19-.067.099-.061.186.015.246.123.095.297.043.389-.118zM4.925 5.999c-.215.065-.424.292-.49.53-.039.145-.016.4.043.478.096.127.188.16.439.159.49-.003.916-.212.966-.474.04-.214-.147-.51-.405-.641a.965.965 0 00-.553-.052zm.574.445c.075-.107.042-.222-.087-.3-.244-.149-.615-.026-.615.204 0 .115.193.24.37.24.118 0 .28-.07.332-.144z"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export default SvgLogo
|
||||
@@ -1,17 +0,0 @@
|
||||
import * as React from 'react'
|
||||
|
||||
function SvgSpinner(props: React.SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" {...props}>
|
||||
<mask id="spinner_svg__a">
|
||||
<path fill="#fff" strokeWidth={0} d="M0 0h24v24H0z" />
|
||||
<path fill="#000" strokeWidth={0} d="M0 0h12v12H0z" />
|
||||
<circle cx={2} cy={12} r={1} fill="#fff" strokeWidth={0} />
|
||||
<circle cx={12} cy={2} r={1} fill="#fff" strokeWidth={0} />
|
||||
</mask>
|
||||
<circle cx={12} cy={12} r={10} mask="url(#spinner_svg__a)" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export default SvgSpinner
|
||||
5
src/lib/assets/svg/wallet.svg
Normal file
5
src/lib/assets/svg/wallet.svg
Normal file
@@ -0,0 +1,5 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="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>
|
||||
|
After Width: | Height: | Size: 743 B |
@@ -1,6 +1,6 @@
|
||||
import { AlertTriangle, LargeIcon } from 'lib/icons'
|
||||
import { AlertTriangle, Icon, LargeIcon } from 'lib/icons'
|
||||
import styled, { Color, css, keyframes, ThemedText } from 'lib/theme'
|
||||
import { ReactNode } from 'react'
|
||||
import { ReactNode, useMemo } from 'react'
|
||||
|
||||
import Button from './Button'
|
||||
import Row from './Row'
|
||||
@@ -9,6 +9,10 @@ const StyledButton = styled(Button)`
|
||||
border-radius: ${({ theme }) => theme.borderRadius}em;
|
||||
flex-grow: 1;
|
||||
transition: background-color 0.25s ease-out, flex-grow 0.25s ease-out, padding 0.25s ease-out;
|
||||
|
||||
:disabled {
|
||||
margin: -1px;
|
||||
}
|
||||
`
|
||||
|
||||
const UpdateRow = styled(Row)``
|
||||
@@ -24,7 +28,7 @@ const grow = keyframes`
|
||||
}
|
||||
`
|
||||
|
||||
const updatedCss = css`
|
||||
const updateCss = css`
|
||||
border: 1px solid ${({ theme }) => theme.outline};
|
||||
padding: calc(0.25em - 1px);
|
||||
padding-left: calc(0.75em - 1px);
|
||||
@@ -41,19 +45,19 @@ const updatedCss = css`
|
||||
}
|
||||
`
|
||||
|
||||
export const Overlay = styled(Row)<{ updated?: boolean }>`
|
||||
export const Overlay = styled(Row)<{ update?: boolean }>`
|
||||
border-radius: ${({ theme }) => theme.borderRadius}em;
|
||||
flex-direction: row-reverse;
|
||||
min-height: 3.5em;
|
||||
transition: padding 0.25s ease-out;
|
||||
|
||||
${({ updated }) => updated && updatedCss}
|
||||
${({ update }) => update && updateCss}
|
||||
`
|
||||
|
||||
export interface ActionButtonProps {
|
||||
color?: Color
|
||||
disabled?: boolean
|
||||
updated?: { message: ReactNode; action: ReactNode }
|
||||
update?: { message: ReactNode; action: ReactNode; icon?: Icon }
|
||||
onClick: () => void
|
||||
onUpdate?: () => void
|
||||
children: ReactNode
|
||||
@@ -62,22 +66,23 @@ export interface ActionButtonProps {
|
||||
export default function ActionButton({
|
||||
color = 'accent',
|
||||
disabled,
|
||||
updated,
|
||||
update,
|
||||
onClick,
|
||||
onUpdate,
|
||||
children,
|
||||
}: ActionButtonProps) {
|
||||
const textColor = useMemo(() => (color === 'accent' && !disabled ? 'onAccent' : 'currentColor'), [color, disabled])
|
||||
return (
|
||||
<Overlay updated={Boolean(updated)} flex align="stretch">
|
||||
<StyledButton color={color} disabled={disabled} onClick={updated ? onUpdate : onClick}>
|
||||
<ThemedText.TransitionButton buttonSize={updated ? 'medium' : 'large'} color="currentColor">
|
||||
{updated ? updated.action : children}
|
||||
<Overlay update={Boolean(update)} flex align="stretch">
|
||||
<StyledButton color={color} disabled={disabled} onClick={update ? onUpdate : onClick}>
|
||||
<ThemedText.TransitionButton buttonSize={update ? 'medium' : 'large'} color={textColor}>
|
||||
{update ? update.action : children}
|
||||
</ThemedText.TransitionButton>
|
||||
</StyledButton>
|
||||
{updated && (
|
||||
{update && (
|
||||
<UpdateRow gap={0.5}>
|
||||
<LargeIcon icon={AlertTriangle} />
|
||||
<ThemedText.Subhead2>{updated?.message}</ThemedText.Subhead2>
|
||||
<LargeIcon color="currentColor" icon={update.icon || AlertTriangle} />
|
||||
<ThemedText.Subhead2>{update?.message}</ThemedText.Subhead2>
|
||||
</UpdateRow>
|
||||
)}
|
||||
</Overlay>
|
||||
|
||||
40
src/lib/components/BrandedFooter.tsx
Normal file
40
src/lib/components/BrandedFooter.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import Row from 'lib/components/Row'
|
||||
import { Logo } from 'lib/icons'
|
||||
import styled, { brand, ThemedText } from 'lib/theme'
|
||||
|
||||
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 function BrandedFooter() {
|
||||
return (
|
||||
<Row justify="center">
|
||||
<UniswapA href={`https://app.uniswap.org/`}>
|
||||
<Row gap={0.25}>
|
||||
<Logo />
|
||||
<ThemedText.Caption>
|
||||
<Trans>Powered by the Uniswap protocol</Trans>
|
||||
</ThemedText.Caption>
|
||||
</Row>
|
||||
</UniswapA>
|
||||
</Row>
|
||||
)
|
||||
}
|
||||
@@ -12,7 +12,6 @@ const Column = styled.div<{
|
||||
css?: ReturnType<typeof css>
|
||||
}>`
|
||||
align-items: ${({ align }) => align ?? 'center'};
|
||||
background-color: inherit;
|
||||
color: ${({ color, theme }) => color && theme[color]};
|
||||
display: ${({ flex }) => (flex ? 'flex' : 'grid')};
|
||||
flex-direction: column;
|
||||
|
||||
@@ -106,7 +106,7 @@ export default function Dialog({ color, children, onClose = () => void 0 }: Dial
|
||||
context.element &&
|
||||
createPortal(
|
||||
<ThemeProvider>
|
||||
<Modal className="dialog" color={color} ref={dialog}>
|
||||
<Modal color={color} ref={dialog}>
|
||||
<OnCloseContext.Provider value={onClose}>{children}</OnCloseContext.Provider>
|
||||
</Modal>
|
||||
</ThemeProvider>,
|
||||
|
||||
@@ -34,7 +34,7 @@ export default class ErrorBoundary extends React.Component<ErrorBoundaryProps, E
|
||||
<Dialog color="dialog">
|
||||
<ErrorDialog
|
||||
error={this.state.error}
|
||||
header={<Trans>Reload the page to try again</Trans>}
|
||||
header={<Trans>Something went wrong.</Trans>}
|
||||
action={<Trans>Reload the page</Trans>}
|
||||
onAction={() => window.location.reload()}
|
||||
/>
|
||||
|
||||
@@ -117,7 +117,10 @@ export default function ErrorDialog({ header, error, action, onAction }: ErrorDi
|
||||
<Rule />
|
||||
<ErrorColumn>
|
||||
<Column gap={0.5} ref={setDetails} css={scrollbar}>
|
||||
<ThemedText.Code>{error.message}</ThemedText.Code>
|
||||
<ThemedText.Code>
|
||||
{error.name}
|
||||
{error.message ? `: ${error.message}` : ''}
|
||||
</ThemedText.Code>
|
||||
</Column>
|
||||
</ErrorColumn>
|
||||
<ActionButton onClick={onAction}>{action}</ActionButton>
|
||||
|
||||
30
src/lib/components/Error/WidgetsPropsValidator.tsx
Normal file
30
src/lib/components/Error/WidgetsPropsValidator.tsx
Normal file
@@ -0,0 +1,30 @@
|
||||
import { SUPPORTED_LOCALES } from 'constants/locales'
|
||||
import { WidgetProps } from 'lib/components/Widget'
|
||||
import { IntegrationError } from 'lib/errors'
|
||||
import { PropsWithChildren, useEffect } from 'react'
|
||||
|
||||
export default function WidgetsPropsValidator(props: PropsWithChildren<WidgetProps>) {
|
||||
const { jsonRpcEndpoint, provider } = props
|
||||
|
||||
useEffect(() => {
|
||||
if (!provider && !jsonRpcEndpoint) {
|
||||
throw new IntegrationError('This widget requires a provider or jsonRpcEndpoint.')
|
||||
}
|
||||
}, [provider, jsonRpcEndpoint])
|
||||
|
||||
const { width } = props
|
||||
useEffect(() => {
|
||||
if (width && width < 300) {
|
||||
throw new IntegrationError(`Set widget width to at least 300px. (You set it to ${width}.)`)
|
||||
}
|
||||
}, [width])
|
||||
|
||||
const { locale } = props
|
||||
useEffect(() => {
|
||||
if (locale && locale !== 'pseudo' && !SUPPORTED_LOCALES.includes(locale)) {
|
||||
console.warn('Unsupported locale: ', locale)
|
||||
}
|
||||
}, [locale])
|
||||
|
||||
return <>{props.children}</>
|
||||
}
|
||||
17
src/lib/components/ExternalLink.tsx
Normal file
17
src/lib/components/ExternalLink.tsx
Normal file
@@ -0,0 +1,17 @@
|
||||
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 +1,9 @@
|
||||
import { largeIconCss, Logo } from 'lib/icons'
|
||||
import { largeIconCss } from 'lib/icons'
|
||||
import styled, { ThemedText } from 'lib/theme'
|
||||
import { ReactElement, ReactNode } from 'react'
|
||||
|
||||
import Row from './Row'
|
||||
|
||||
const UniswapA = styled.a`
|
||||
cursor: pointer;
|
||||
|
||||
${Logo} {
|
||||
fill: ${({ theme }) => theme.secondary};
|
||||
height: 1.5em;
|
||||
transition: transform 0.25s ease;
|
||||
width: 1.5em;
|
||||
will-change: transform;
|
||||
|
||||
:hover {
|
||||
fill: ${({ theme }) => theme.onHover(theme.secondary)};
|
||||
transform: rotate(-5deg);
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const HeaderRow = styled(Row)`
|
||||
height: 1.75em;
|
||||
margin: 0 0.75em 0.75em;
|
||||
@@ -30,21 +13,13 @@ const HeaderRow = styled(Row)`
|
||||
|
||||
export interface HeaderProps {
|
||||
title?: ReactElement
|
||||
logo?: boolean
|
||||
children: ReactNode
|
||||
}
|
||||
|
||||
export default function Header({ title, logo, children }: HeaderProps) {
|
||||
export default function Header({ title, children }: HeaderProps) {
|
||||
return (
|
||||
<HeaderRow iconSize={1.2}>
|
||||
<Row gap={0.5}>
|
||||
{logo && (
|
||||
<UniswapA href={`https://app.uniswap.org/`}>
|
||||
<Logo />
|
||||
</UniswapA>
|
||||
)}
|
||||
{title && <ThemedText.Subhead1>{title}</ThemedText.Subhead1>}
|
||||
</Row>
|
||||
<Row gap={0.5}>{title && <ThemedText.Subhead1>{title}</ThemedText.Subhead1>}</Row>
|
||||
<Row gap={1}>{children}</Row>
|
||||
</HeaderRow>
|
||||
)
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import JSBI from 'jsbi'
|
||||
import styled, { css } from 'lib/theme'
|
||||
import { forwardRef, HTMLProps, useCallback, useEffect, useState } from 'react'
|
||||
|
||||
@@ -67,23 +68,31 @@ export const StringInput = forwardRef<HTMLInputElement, StringInputProps>(functi
|
||||
})
|
||||
|
||||
interface NumericInputProps extends Omit<HTMLProps<HTMLInputElement>, 'onChange' | 'as' | 'value'> {
|
||||
value: number | undefined
|
||||
onChange: (input: number | undefined) => void
|
||||
value: string
|
||||
onChange: (input: string) => void
|
||||
}
|
||||
|
||||
interface EnforcedNumericInputProps extends NumericInputProps {
|
||||
// Validates nextUserInput; returns stringified value or undefined if valid, or null if invalid
|
||||
enforcer: (nextUserInput: string) => string | undefined | null
|
||||
// Validates nextUserInput; returns stringified value, or null if invalid
|
||||
enforcer: (nextUserInput: string) => string | null
|
||||
}
|
||||
|
||||
function isNumericallyEqual(a: string, b: string) {
|
||||
const [aInteger, aDecimal] = a.split('.')
|
||||
const [bInteger, bDecimal] = b.split('.')
|
||||
return (
|
||||
JSBI.equal(JSBI.BigInt(aInteger ?? 0), JSBI.BigInt(bInteger ?? 0)) &&
|
||||
JSBI.equal(JSBI.BigInt(aDecimal ?? 0), JSBI.BigInt(bDecimal ?? 0))
|
||||
)
|
||||
}
|
||||
|
||||
const NumericInput = forwardRef<HTMLInputElement, EnforcedNumericInputProps>(function NumericInput(
|
||||
{ value, onChange, enforcer, pattern, ...props }: EnforcedNumericInputProps,
|
||||
ref
|
||||
) {
|
||||
// Allow value/onChange to use number by preventing a trailing decimal separator from triggering onChange
|
||||
const [state, setState] = useState(value ?? '')
|
||||
useEffect(() => {
|
||||
if (+state !== value) {
|
||||
if (!isNumericallyEqual(state, value)) {
|
||||
setState(value ?? '')
|
||||
}
|
||||
}, [value, state, setState])
|
||||
@@ -93,8 +102,8 @@ const NumericInput = forwardRef<HTMLInputElement, EnforcedNumericInputProps>(fun
|
||||
const nextInput = enforcer(event.target.value.replace(/,/g, '.'))
|
||||
if (nextInput !== null) {
|
||||
setState(nextInput ?? '')
|
||||
if (nextInput === undefined || +nextInput !== value) {
|
||||
onChange(nextInput === undefined ? undefined : +nextInput)
|
||||
if (!isNumericallyEqual(nextInput, value)) {
|
||||
onChange(nextInput)
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -114,6 +123,7 @@ const NumericInput = forwardRef<HTMLInputElement, EnforcedNumericInputProps>(fun
|
||||
pattern={pattern}
|
||||
placeholder={props.placeholder || '0'}
|
||||
minLength={1}
|
||||
maxLength={79}
|
||||
spellCheck="false"
|
||||
ref={ref as any}
|
||||
{...props}
|
||||
@@ -125,7 +135,7 @@ const integerRegexp = /^\d*$/
|
||||
const integerEnforcer = (nextUserInput: string) => {
|
||||
if (nextUserInput === '' || integerRegexp.test(nextUserInput)) {
|
||||
const nextInput = parseInt(nextUserInput)
|
||||
return isNaN(nextInput) ? undefined : nextInput.toString()
|
||||
return isNaN(nextInput) ? '' : nextInput.toString()
|
||||
}
|
||||
return null
|
||||
}
|
||||
@@ -136,7 +146,7 @@ export const IntegerInput = forwardRef(function IntegerInput(props: NumericInput
|
||||
const decimalRegexp = /^\d*(?:[.])?\d*$/
|
||||
const decimalEnforcer = (nextUserInput: string) => {
|
||||
if (nextUserInput === '') {
|
||||
return undefined
|
||||
return ''
|
||||
} else if (nextUserInput === '.') {
|
||||
return '0.'
|
||||
} else if (decimalRegexp.test(nextUserInput)) {
|
||||
@@ -153,7 +163,7 @@ export const inputCss = css`
|
||||
border: 1px solid ${({ theme }) => theme.container};
|
||||
border-radius: ${({ theme }) => theme.borderRadius}em;
|
||||
cursor: text;
|
||||
padding: calc(0.75em - 1px);
|
||||
padding: calc(0.5em - 1px);
|
||||
|
||||
:hover:not(:focus-within) {
|
||||
background-color: ${({ theme }) => theme.onHover(theme.container)};
|
||||
|
||||
@@ -11,6 +11,7 @@ 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: 8px;
|
||||
@@ -32,7 +33,6 @@ const Arrow = styled.div`
|
||||
background: ${({ theme }) => theme.dialog};
|
||||
border: 1px solid ${({ theme }) => theme.outline};
|
||||
content: '';
|
||||
|
||||
height: 8px;
|
||||
position: absolute;
|
||||
transform: rotate(45deg);
|
||||
@@ -40,32 +40,36 @@ const Arrow = styled.div`
|
||||
}
|
||||
|
||||
&.arrow-top {
|
||||
bottom: -5px;
|
||||
bottom: -4px;
|
||||
::before {
|
||||
border-radius: 1px;
|
||||
border-left: none;
|
||||
border-top: none;
|
||||
}
|
||||
}
|
||||
|
||||
&.arrow-bottom {
|
||||
top: -5px;
|
||||
top: -5px; // includes -1px from border
|
||||
::before {
|
||||
border-bottom: none;
|
||||
border-right: none;
|
||||
border-radius: 1px;
|
||||
}
|
||||
}
|
||||
|
||||
&.arrow-left {
|
||||
right: -5px;
|
||||
right: -4px;
|
||||
::before {
|
||||
border-bottom: none;
|
||||
border-left: none;
|
||||
border-radius: 1px;
|
||||
}
|
||||
}
|
||||
|
||||
&.arrow-right {
|
||||
left: -5px;
|
||||
left: -5px; // includes -1px from border
|
||||
::before {
|
||||
border-radius: 1px;
|
||||
border-right: none;
|
||||
border-top: none;
|
||||
}
|
||||
@@ -77,10 +81,11 @@ export interface PopoverProps {
|
||||
show: boolean
|
||||
children: React.ReactNode
|
||||
placement: Placement
|
||||
offset?: number
|
||||
contained?: true
|
||||
}
|
||||
|
||||
export default function Popover({ content, show, children, placement, contained }: PopoverProps) {
|
||||
export default function Popover({ content, show, children, placement, offset, contained }: PopoverProps) {
|
||||
const boundary = useContext(BoundaryContext)
|
||||
const reference = useRef<HTMLDivElement>(null)
|
||||
|
||||
@@ -90,8 +95,8 @@ export default function Popover({ content, show, children, placement, contained
|
||||
|
||||
const options = useMemo((): Options => {
|
||||
const modifiers: Options['modifiers'] = [
|
||||
{ name: 'offset', options: { offset: [5, 5] } },
|
||||
{ name: 'arrow', options: { element: arrow, padding: 6 } },
|
||||
{ name: 'offset', options: { offset: [4, offset || 4] } },
|
||||
{ name: 'arrow', options: { element: arrow, padding: 4 } },
|
||||
]
|
||||
if (contained) {
|
||||
modifiers.push(
|
||||
@@ -118,7 +123,7 @@ export default function Popover({ content, show, children, placement, contained
|
||||
strategy: 'absolute',
|
||||
modifiers,
|
||||
}
|
||||
}, [arrow, boundary, placement, contained])
|
||||
}, [offset, arrow, contained, placement, boundary])
|
||||
|
||||
const { styles, attributes } = usePopper(reference.current, popover, options)
|
||||
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import { Currency } from '@uniswap/sdk-core'
|
||||
import { AlertTriangle, ArrowRight, CheckCircle, Spinner, Trash2 } from 'lib/icons'
|
||||
import { DAI, ETH, UNI, USDC } from 'lib/mocks'
|
||||
import styled, { ThemedText } from 'lib/theme'
|
||||
import { Token } from 'lib/types'
|
||||
import { useMemo, useState } from 'react'
|
||||
|
||||
import Button from './Button'
|
||||
@@ -13,7 +12,7 @@ import TokenImg from './TokenImg'
|
||||
|
||||
interface ITokenAmount {
|
||||
value: number
|
||||
token: Token
|
||||
token: Currency
|
||||
}
|
||||
|
||||
export enum TransactionStatus {
|
||||
@@ -28,25 +27,6 @@ interface ITransaction {
|
||||
status: TransactionStatus
|
||||
}
|
||||
|
||||
// TODO: integrate with web3-react context
|
||||
export const mockTxs: ITransaction[] = [
|
||||
{
|
||||
input: { value: 4170.15, token: USDC },
|
||||
output: { value: 4167.44, token: DAI },
|
||||
status: TransactionStatus.SUCCESS,
|
||||
},
|
||||
{
|
||||
input: { value: 1.23, token: ETH },
|
||||
output: { value: 4125.02, token: DAI },
|
||||
status: TransactionStatus.PENDING,
|
||||
},
|
||||
{
|
||||
input: { value: 10, token: UNI },
|
||||
output: { value: 2125.02, token: ETH },
|
||||
status: TransactionStatus.ERROR,
|
||||
},
|
||||
]
|
||||
|
||||
const TransactionRow = styled(Row)`
|
||||
padding: 0.5em 1em;
|
||||
|
||||
@@ -94,7 +74,7 @@ function Transaction({ tx }: { tx: ITransaction }) {
|
||||
}
|
||||
|
||||
export default function RecentTransactionsDialog() {
|
||||
const [txs, setTxs] = useState(mockTxs)
|
||||
const [txs, setTxs] = useState([])
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -8,7 +8,7 @@ const Row = styled.div<{
|
||||
pad?: number
|
||||
gap?: number
|
||||
flex?: true
|
||||
grow?: true
|
||||
grow?: true | 'first' | 'last'
|
||||
children?: ReactNode
|
||||
theme: Theme
|
||||
}>`
|
||||
@@ -19,7 +19,12 @@ const Row = styled.div<{
|
||||
flex-grow: ${({ grow }) => grow && 1};
|
||||
gap: ${({ gap }) => gap && `${gap}em`};
|
||||
grid-auto-flow: column;
|
||||
grid-template-columns: ${({ grow, children }) => (grow ? `repeat(${Children.count(children)}, 1fr)` : '')};
|
||||
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`};
|
||||
`
|
||||
|
||||
@@ -1,16 +1,24 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import { CurrencyAmount, Token } from '@uniswap/sdk-core'
|
||||
import { useLingui } from '@lingui/react'
|
||||
import { useUSDCValue } from 'hooks/useUSDCPrice'
|
||||
import { useAtomValue } from 'jotai/utils'
|
||||
import { inputAtom, useUpdateInputToken, useUpdateInputValue } from 'lib/state/swap'
|
||||
import { loadingOpacityCss } from 'lib/css/loading'
|
||||
import { useSwapAmount, useSwapCurrency, useSwapInfo } from 'lib/hooks/swap'
|
||||
import { usePrefetchCurrencyColor } from 'lib/hooks/useCurrencyColor'
|
||||
import { Field, independentFieldAtom } 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 Column from '../Column'
|
||||
import Row from '../Row'
|
||||
import TokenImg from '../TokenImg'
|
||||
import TokenInput from './TokenInput'
|
||||
|
||||
const mockToken = new Token(1, '0x8b3192f5eebd8579568a2ed41e6feb402f93f73f', 9, 'STM', 'Saitama')
|
||||
const mockCurrencyAmount = CurrencyAmount.fromRawAmount(mockToken, '134108514895957704114061')
|
||||
const LoadingSpan = styled.span<{ $loading: boolean }>`
|
||||
${loadingOpacityCss};
|
||||
`
|
||||
|
||||
const InputColumn = styled(Column)<{ approved?: boolean }>`
|
||||
margin: 0.75em;
|
||||
@@ -27,31 +35,59 @@ interface InputProps {
|
||||
}
|
||||
|
||||
export default function Input({ disabled }: InputProps) {
|
||||
const input = useAtomValue(inputAtom)
|
||||
const setValue = useUpdateInputValue(inputAtom)
|
||||
const setToken = useUpdateInputToken(inputAtom)
|
||||
const balance = mockCurrencyAmount
|
||||
const { i18n } = useLingui()
|
||||
const {
|
||||
trade: { state: tradeState },
|
||||
currencyBalances: { [Field.INPUT]: balance },
|
||||
currencyAmounts: { [Field.INPUT]: inputCurrencyAmount },
|
||||
} = useSwapInfo()
|
||||
const inputUSDC = useUSDCValue(inputCurrencyAmount)
|
||||
|
||||
const [swapInputAmount, updateSwapInputAmount] = useSwapAmount(Field.INPUT)
|
||||
const [swapInputCurrency, updateSwapInputCurrency] = useSwapCurrency(Field.INPUT)
|
||||
|
||||
// extract eagerly in case of reversal
|
||||
usePrefetchCurrencyColor(swapInputCurrency)
|
||||
|
||||
const isTradeLoading = useMemo(
|
||||
() => TradeState.LOADING === tradeState || TradeState.SYNCING === tradeState,
|
||||
[tradeState]
|
||||
)
|
||||
const isDependentField = useAtomValue(independentFieldAtom) !== Field.INPUT
|
||||
const isLoading = isDependentField && isTradeLoading
|
||||
|
||||
//TODO(ianlapham): mimic logic from app swap page
|
||||
const mockApproved = true
|
||||
|
||||
const onMax = useMemo(() => {
|
||||
if (balance?.greaterThan(0)) {
|
||||
return () => updateSwapInputAmount(balance.toExact())
|
||||
}
|
||||
return
|
||||
}, [balance, updateSwapInputAmount])
|
||||
|
||||
return (
|
||||
<InputColumn gap={0.5} approved={input.approved !== false}>
|
||||
<InputColumn gap={0.5} approved={mockApproved}>
|
||||
<Row>
|
||||
<ThemedText.Subhead2 color="secondary">
|
||||
<Trans>Trading</Trans>
|
||||
</ThemedText.Subhead2>
|
||||
</Row>
|
||||
<TokenInput
|
||||
input={input}
|
||||
currency={swapInputCurrency}
|
||||
amount={(swapInputAmount !== undefined ? swapInputAmount : inputCurrencyAmount?.toSignificant(6)) ?? ''}
|
||||
disabled={disabled}
|
||||
onMax={balance ? () => setValue(1234) : undefined}
|
||||
onChangeInput={setValue}
|
||||
onChangeToken={setToken}
|
||||
onMax={onMax}
|
||||
onChangeInput={updateSwapInputAmount}
|
||||
onChangeCurrency={updateSwapInputCurrency}
|
||||
loading={isLoading}
|
||||
>
|
||||
<ThemedText.Body2 color="secondary">
|
||||
<Row>
|
||||
{input.usdc ? `~ $${input.usdc.toLocaleString('en')}` : '-'}
|
||||
<LoadingSpan $loading={isLoading}>{inputUSDC ? `$${inputUSDC.toFixed(2)}` : '-'}</LoadingSpan>
|
||||
{balance && (
|
||||
<ThemedText.Body2 color={input.value && balance.lessThan(input.value) ? 'error' : undefined}>
|
||||
Balance: <span style={{ userSelect: 'text' }}>{balance.toExact()}</span>
|
||||
<ThemedText.Body2 color={inputCurrencyAmount?.greaterThan(balance) ? 'error' : undefined}>
|
||||
Balance: <span style={{ userSelect: 'text' }}>{formatCurrencyAmount(balance, 4, i18n.locale)}</span>
|
||||
</ThemedText.Body2>
|
||||
)}
|
||||
</Row>
|
||||
|
||||
@@ -1,10 +1,18 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import { useLingui } from '@lingui/react'
|
||||
import { useUSDCValue } from 'hooks/useUSDCPrice'
|
||||
import { atom } from 'jotai'
|
||||
import { useAtomValue } from 'jotai/utils'
|
||||
import useColor, { usePrefetchColor } from 'lib/hooks/useColor'
|
||||
import { inputAtom, outputAtom, useUpdateInputToken, useUpdateInputValue } from 'lib/state/swap'
|
||||
import BrandedFooter from 'lib/components/BrandedFooter'
|
||||
import { loadingOpacityCss } from 'lib/css/loading'
|
||||
import { useSwapAmount, useSwapCurrency, useSwapInfo } from 'lib/hooks/swap'
|
||||
import useCurrencyColor from 'lib/hooks/useCurrencyColor'
|
||||
import { Field, independentFieldAtom } from 'lib/state/swap'
|
||||
import styled, { DynamicThemeProvider, ThemedText } from 'lib/theme'
|
||||
import { ReactNode, useMemo } from 'react'
|
||||
import { TradeState } from 'state/routing/types'
|
||||
import { computeFiatValuePriceImpact } from 'utils/computeFiatValuePriceImpact'
|
||||
import { formatCurrencyAmount } from 'utils/formatCurrencyAmount'
|
||||
|
||||
import Column from '../Column'
|
||||
import Row from '../Row'
|
||||
@@ -12,16 +20,21 @@ import TokenInput from './TokenInput'
|
||||
|
||||
export const colorAtom = atom<string | undefined>(undefined)
|
||||
|
||||
const LoadingSpan = styled.span<{ $loading: boolean }>`
|
||||
${loadingOpacityCss};
|
||||
`
|
||||
|
||||
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)};
|
||||
}
|
||||
@@ -33,32 +46,47 @@ interface OutputProps {
|
||||
}
|
||||
|
||||
export default function Output({ disabled, children }: OutputProps) {
|
||||
const input = useAtomValue(inputAtom)
|
||||
const output = useAtomValue(outputAtom)
|
||||
const setValue = useUpdateInputValue(outputAtom)
|
||||
const setToken = useUpdateInputToken(outputAtom)
|
||||
const balance = 123.45
|
||||
const { i18n } = useLingui()
|
||||
|
||||
const {
|
||||
trade: { state: tradeState },
|
||||
currencyBalances: { [Field.OUTPUT]: balance },
|
||||
currencyAmounts: { [Field.INPUT]: inputCurrencyAmount, [Field.OUTPUT]: outputCurrencyAmount },
|
||||
} = useSwapInfo()
|
||||
|
||||
const [swapOutputAmount, updateSwapOutputAmount] = useSwapAmount(Field.OUTPUT)
|
||||
const [swapOutputCurrency, updateSwapOutputCurrency] = useSwapCurrency(Field.OUTPUT)
|
||||
|
||||
//loading status of the trade
|
||||
const isTradeLoading = useMemo(
|
||||
() => TradeState.LOADING === tradeState || TradeState.SYNCING === tradeState,
|
||||
[tradeState]
|
||||
)
|
||||
|
||||
const isDependentField = useAtomValue(independentFieldAtom) !== Field.OUTPUT
|
||||
const isLoading = isDependentField && isTradeLoading
|
||||
|
||||
const overrideColor = useAtomValue(colorAtom)
|
||||
const dynamicColor = useColor(output.token)
|
||||
usePrefetchColor(input.token) // extract eagerly in case of reversal
|
||||
const dynamicColor = useCurrencyColor(swapOutputCurrency)
|
||||
const color = overrideColor || dynamicColor
|
||||
const hasColor = output.token ? Boolean(color) || null : false
|
||||
|
||||
const change = useMemo(() => {
|
||||
if (input.usdc && output.usdc) {
|
||||
const change = output.usdc / input.usdc - 1
|
||||
const percent = (change * 100).toPrecision(3)
|
||||
return change > 0 ? ` (+${percent}%)` : `(${percent}%)`
|
||||
// different state true/null/false allow smoother color transition
|
||||
const hasColor = swapOutputCurrency ? Boolean(color) || null : false
|
||||
|
||||
const inputUSDC = useUSDCValue(inputCurrencyAmount)
|
||||
const outputUSDC = useUSDCValue(outputCurrencyAmount)
|
||||
|
||||
const priceImpact = useMemo(() => {
|
||||
const computedChange = computeFiatValuePriceImpact(inputUSDC, outputUSDC)
|
||||
return computedChange ? parseFloat(computedChange.multiply(-1)?.toSignificant(3)) : undefined
|
||||
}, [inputUSDC, outputUSDC])
|
||||
|
||||
const usdc = useMemo(() => {
|
||||
if (outputUSDC) {
|
||||
return `$${outputUSDC.toFixed(2)} (${priceImpact && priceImpact > 0 ? '+' : ''}${priceImpact}%)`
|
||||
}
|
||||
return ''
|
||||
}, [input, output])
|
||||
const usdc = useMemo(() => {
|
||||
if (output.usdc) {
|
||||
return `~ $${output.usdc.toLocaleString('en')}${change}`
|
||||
}
|
||||
return '-'
|
||||
}, [change, output])
|
||||
}, [priceImpact, outputUSDC])
|
||||
|
||||
return (
|
||||
<DynamicThemeProvider color={color}>
|
||||
@@ -68,19 +96,27 @@ export default function Output({ disabled, children }: OutputProps) {
|
||||
<Trans>For</Trans>
|
||||
</ThemedText.Subhead2>
|
||||
</Row>
|
||||
<TokenInput input={output} disabled={disabled} onChangeInput={setValue} onChangeToken={setToken}>
|
||||
<TokenInput
|
||||
currency={swapOutputCurrency}
|
||||
amount={(swapOutputAmount !== undefined ? swapOutputAmount : outputCurrencyAmount?.toSignificant(6)) ?? ''}
|
||||
disabled={disabled}
|
||||
onChangeInput={updateSwapOutputAmount}
|
||||
onChangeCurrency={updateSwapOutputCurrency}
|
||||
loading={isLoading}
|
||||
>
|
||||
<ThemedText.Body2 color="secondary">
|
||||
<Row>
|
||||
{usdc}
|
||||
<LoadingSpan $loading={isLoading}>{usdc}</LoadingSpan>
|
||||
{balance && (
|
||||
<span>
|
||||
Balance: <span style={{ userSelect: 'text' }}>{balance}</span>
|
||||
Balance: <span style={{ userSelect: 'text' }}>{formatCurrencyAmount(balance, 4, i18n.locale)}</span>
|
||||
</span>
|
||||
)}
|
||||
</Row>
|
||||
</ThemedText.Body2>
|
||||
</TokenInput>
|
||||
{children}
|
||||
<BrandedFooter />
|
||||
</OutputColumn>
|
||||
</DynamicThemeProvider>
|
||||
)
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { useAtom } from 'jotai'
|
||||
import { useSwitchSwapCurrencies } from 'lib/hooks/swap'
|
||||
import { ArrowDown as ArrowDownIcon, ArrowUp as ArrowUpIcon } from 'lib/icons'
|
||||
import { stateAtom } from 'lib/state/swap'
|
||||
import styled, { Layer } from 'lib/theme'
|
||||
import { useCallback, useState } from 'react'
|
||||
|
||||
@@ -47,16 +46,12 @@ const StyledReverseButton = styled(Button)<{ turns: number }>`
|
||||
`
|
||||
|
||||
export default function ReverseButton({ disabled }: { disabled?: boolean }) {
|
||||
const [state, setState] = useAtom(stateAtom)
|
||||
const [turns, setTurns] = useState(0)
|
||||
const switchCurrencies = useSwitchSwapCurrencies()
|
||||
const onClick = useCallback(() => {
|
||||
const { input, output } = state
|
||||
setState((state) => {
|
||||
state.input = output
|
||||
state.output = input
|
||||
})
|
||||
switchCurrencies()
|
||||
setTurns((turns) => ++turns)
|
||||
}, [state, setState])
|
||||
}, [switchCurrencies])
|
||||
|
||||
return (
|
||||
<ReverseRow justify="center">
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import { t, Trans } from '@lingui/macro'
|
||||
import { Trans } from '@lingui/macro'
|
||||
import { Percent } from '@uniswap/sdk-core'
|
||||
import { useAtom } from 'jotai'
|
||||
import { Check, LargeIcon } from 'lib/icons'
|
||||
import { MaxSlippage, maxSlippageAtom } from 'lib/state/swap'
|
||||
import styled, { ThemedText } from 'lib/theme'
|
||||
import { ReactNode, useCallback, useRef } from 'react'
|
||||
import Popover from 'lib/components/Popover'
|
||||
import { TooltipHandlers, useTooltip } from 'lib/components/Tooltip'
|
||||
import { AlertTriangle, Check, Icon, LargeIcon, XOctagon } from 'lib/icons'
|
||||
import { autoSlippageAtom, MAX_VALID_SLIPPAGE, maxSlippageAtom, MIN_HIGH_SLIPPAGE } from 'lib/state/settings'
|
||||
import styled, { Color, ThemedText } from 'lib/theme'
|
||||
import { memo, PropsWithChildren, ReactNode, useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
|
||||
import { BaseButton, TextButton } from '../../Button'
|
||||
import Column from '../../Column'
|
||||
@@ -14,77 +17,156 @@ 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 StyledOption = styled(TextButton)<{ selected: boolean }>`
|
||||
const placeholder = '0.10'
|
||||
|
||||
const Button = styled(TextButton)<{ selected: boolean }>`
|
||||
${({ selected }) => optionCss(selected)}
|
||||
`
|
||||
|
||||
const StyledInputOption = styled(BaseButton)<{ selected: boolean }>`
|
||||
const Custom = styled(BaseButton)<{ selected: boolean }>`
|
||||
${({ selected }) => optionCss(selected)}
|
||||
${inputCss}
|
||||
border-color: ${({ selected, theme }) => (selected ? theme.active : 'transparent')} !important;
|
||||
padding: calc(0.5em - 1px) 0.625em;
|
||||
padding: calc(0.75em - 3px) 0.625em;
|
||||
`
|
||||
|
||||
interface OptionProps<T> {
|
||||
value: T
|
||||
interface OptionProps extends Partial<TooltipHandlers> {
|
||||
wrapper: typeof Button | typeof Custom
|
||||
selected: boolean
|
||||
onSelect: (value: T) => void
|
||||
onSelect: () => void
|
||||
icon?: ReactNode
|
||||
}
|
||||
|
||||
function Option<T>({ value, selected, onSelect }: OptionProps<T>) {
|
||||
function Option({
|
||||
wrapper: Wrapper,
|
||||
children,
|
||||
selected,
|
||||
onSelect,
|
||||
icon,
|
||||
...tooltipHandlers
|
||||
}: PropsWithChildren<OptionProps>) {
|
||||
return (
|
||||
<StyledOption selected={selected} onClick={() => onSelect(value)}>
|
||||
<Row>
|
||||
<ThemedText.Subhead2>{value}%</ThemedText.Subhead2>
|
||||
{selected && <LargeIcon icon={Check} />}
|
||||
<Wrapper selected={selected} onClick={onSelect} {...tooltipHandlers}>
|
||||
<Row gap={0.5}>
|
||||
{children}
|
||||
{icon ? icon : <LargeIcon icon={selected ? Check : undefined} size={1.25} />}
|
||||
</Row>
|
||||
</StyledOption>
|
||||
</Wrapper>
|
||||
)
|
||||
}
|
||||
|
||||
function InputOption<T>({ value, children, selected, onSelect }: OptionProps<T> & { children: ReactNode }) {
|
||||
return (
|
||||
<StyledInputOption color="container" selected={selected} onClick={() => onSelect(value)}>
|
||||
<ThemedText.Subhead2>
|
||||
<Row>{children}</Row>
|
||||
</ThemedText.Subhead2>
|
||||
</StyledInputOption>
|
||||
)
|
||||
enum WarningState {
|
||||
NONE,
|
||||
HIGH_SLIPPAGE,
|
||||
INVALID_SLIPPAGE,
|
||||
}
|
||||
|
||||
const Warning = memo(function Warning({ state, showTooltip }: { state: WarningState; showTooltip: boolean }) {
|
||||
let icon: Icon
|
||||
let color: Color
|
||||
let content: ReactNode
|
||||
let show = showTooltip
|
||||
switch (state) {
|
||||
case WarningState.INVALID_SLIPPAGE:
|
||||
icon = XOctagon
|
||||
color = 'error'
|
||||
content = invalidSlippage
|
||||
show = true
|
||||
break
|
||||
case WarningState.HIGH_SLIPPAGE:
|
||||
icon = AlertTriangle
|
||||
color = 'warning'
|
||||
content = highSlippage
|
||||
break
|
||||
case WarningState.NONE:
|
||||
return null
|
||||
}
|
||||
return (
|
||||
<Popover
|
||||
key={state}
|
||||
content={<ThemedText.Caption>{content}</ThemedText.Caption>}
|
||||
show={show}
|
||||
placement="top"
|
||||
offset={16}
|
||||
contained
|
||||
>
|
||||
<LargeIcon icon={icon} color={color} size={1.25} />
|
||||
</Popover>
|
||||
)
|
||||
})
|
||||
|
||||
export default function MaxSlippageSelect() {
|
||||
const { P01, P05, CUSTOM } = MaxSlippage
|
||||
const [{ value: maxSlippage, custom }, setMaxSlippage] = useAtom(maxSlippageAtom)
|
||||
|
||||
const input = useRef<HTMLInputElement>(null)
|
||||
const focus = useCallback(() => input.current?.focus(), [input])
|
||||
const onInputSelect = useCallback(
|
||||
(custom) => {
|
||||
focus()
|
||||
if (custom !== undefined) {
|
||||
setMaxSlippage({ value: CUSTOM, custom })
|
||||
|
||||
const [autoSlippage, setAutoSlippage] = useAtom(autoSlippageAtom)
|
||||
const [maxSlippage, setMaxSlippage] = useAtom(maxSlippageAtom)
|
||||
const maxSlippageInput = useMemo(() => maxSlippage?.toString() || '', [maxSlippage])
|
||||
const [warning, setWarning] = useState(WarningState.NONE)
|
||||
const [showTooltip, setShowTooltip, tooltipProps] = useTooltip()
|
||||
|
||||
const processInput = useCallback(
|
||||
(input: number | undefined) => {
|
||||
const numerator = input && Math.floor(input * 100)
|
||||
if (numerator) {
|
||||
const percent = new Percent(numerator, 10_000)
|
||||
if (percent.greaterThan(MAX_VALID_SLIPPAGE)) {
|
||||
setWarning(WarningState.INVALID_SLIPPAGE)
|
||||
setAutoSlippage(true)
|
||||
setMaxSlippage(input)
|
||||
} else if (percent.greaterThan(MIN_HIGH_SLIPPAGE)) {
|
||||
setWarning(WarningState.HIGH_SLIPPAGE)
|
||||
setAutoSlippage(false)
|
||||
setMaxSlippage(input)
|
||||
} else {
|
||||
setWarning(WarningState.NONE)
|
||||
setAutoSlippage(false)
|
||||
setMaxSlippage(input)
|
||||
}
|
||||
} else {
|
||||
setAutoSlippage(true)
|
||||
setMaxSlippage(undefined)
|
||||
}
|
||||
},
|
||||
[CUSTOM, focus, setMaxSlippage]
|
||||
[setAutoSlippage, setMaxSlippage]
|
||||
)
|
||||
const onInputSelect = useCallback(() => {
|
||||
focus()
|
||||
processInput(maxSlippage)
|
||||
}, [focus, maxSlippage, processInput])
|
||||
|
||||
useEffect(() => processInput(maxSlippage), [maxSlippage, processInput]) // processes any warnings on mount
|
||||
useEffect(() => setShowTooltip(true), [warning, setShowTooltip]) // enables the tooltip if a warning is set
|
||||
|
||||
return (
|
||||
<Column gap={0.75}>
|
||||
<Label name={<Trans>Max slippage</Trans>} tooltip={tooltip} />
|
||||
<Row gap={0.5} grow>
|
||||
<Option value={P01} onSelect={setMaxSlippage} selected={maxSlippage === P01} />
|
||||
<Option value={P05} onSelect={setMaxSlippage} selected={maxSlippage === P05} />
|
||||
<InputOption value={custom} onSelect={onInputSelect} selected={maxSlippage === CUSTOM}>
|
||||
<DecimalInput
|
||||
size={custom === undefined ? undefined : 5}
|
||||
value={custom}
|
||||
onChange={(custom) => setMaxSlippage({ value: CUSTOM, custom })}
|
||||
placeholder={t`Custom`}
|
||||
ref={input}
|
||||
/>
|
||||
%
|
||||
</InputOption>
|
||||
<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 state={warning} showTooltip={showTooltip} />}
|
||||
{...tooltipProps}
|
||||
>
|
||||
<Row color={warning === WarningState.INVALID_SLIPPAGE ? 'error' : undefined}>
|
||||
<DecimalInput
|
||||
size={Math.max(maxSlippageInput.length, 3)}
|
||||
value={maxSlippageInput}
|
||||
onChange={(input) => processInput(+input)}
|
||||
placeholder={placeholder}
|
||||
ref={input}
|
||||
/>
|
||||
%
|
||||
</Row>
|
||||
</Option>
|
||||
</Row>
|
||||
</Column>
|
||||
)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import { useAtom } from 'jotai'
|
||||
import { mockTogglableAtom } from 'lib/state/swap'
|
||||
import { mockTogglableAtom } from 'lib/state/settings'
|
||||
|
||||
import Row from '../../Row'
|
||||
import Toggle from '../../Toggle'
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import { useAtom } from 'jotai'
|
||||
import { TRANSACTION_TTL_DEFAULT, transactionTtlAtom } from 'lib/state/swap'
|
||||
import { useDefaultTransactionTtl, useTransactionTtl } from 'lib/hooks/useTransactionDeadline'
|
||||
import styled, { ThemedText } from 'lib/theme'
|
||||
import { useRef } from 'react'
|
||||
|
||||
@@ -16,17 +15,20 @@ const Input = styled(Row)`
|
||||
`
|
||||
|
||||
export default function TransactionTtlInput() {
|
||||
const [transactionTtl, setTransactionTtl] = useAtom(transactionTtlAtom)
|
||||
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 onClick={() => input.current?.focus()}>
|
||||
<Input justify="start" onClick={() => input.current?.focus()}>
|
||||
<IntegerInput
|
||||
placeholder={TRANSACTION_TTL_DEFAULT.toString()}
|
||||
value={transactionTtl}
|
||||
onChange={(value) => setTransactionTtl(value ?? 0)}
|
||||
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>
|
||||
|
||||
@@ -12,11 +12,15 @@ export const optionCss = (selected: boolean) => css`
|
||||
color: ${({ theme }) => theme.primary} !important;
|
||||
display: grid;
|
||||
grid-gap: 0.25em;
|
||||
padding: 0.5em 0.625em;
|
||||
padding: calc(0.75em - 1px) 0.625em;
|
||||
|
||||
: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) {
|
||||
|
||||
@@ -2,7 +2,7 @@ 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/swap'
|
||||
import { settingsAtom } from 'lib/state/settings'
|
||||
import styled, { ThemedText } from 'lib/theme'
|
||||
import React, { useState } from 'react'
|
||||
|
||||
|
||||
@@ -1,56 +1,8 @@
|
||||
import { useUpdateAtom } from 'jotai/utils'
|
||||
import { DAI, ETH } from 'lib/mocks'
|
||||
import { transactionAtom } from 'lib/state/swap'
|
||||
import { useEffect } from 'react'
|
||||
import { useSelect } from 'react-cosmos/fixture'
|
||||
import invariant from 'tiny-invariant'
|
||||
|
||||
import { Modal } from '../Dialog'
|
||||
import { StatusDialog } from './Status'
|
||||
|
||||
function Fixture() {
|
||||
const setTransaction = useUpdateAtom(transactionAtom)
|
||||
|
||||
const [state] = useSelect('state', {
|
||||
options: ['PENDING', 'ERROR', 'SUCCESS'],
|
||||
})
|
||||
useEffect(() => {
|
||||
setTransaction({
|
||||
input: { token: ETH, value: 1 },
|
||||
output: { token: DAI, value: 4200 },
|
||||
receipt: '',
|
||||
timestamp: Date.now(),
|
||||
})
|
||||
}, [setTransaction])
|
||||
useEffect(() => {
|
||||
switch (state) {
|
||||
case 'PENDING':
|
||||
setTransaction({
|
||||
input: { token: ETH, value: 1 },
|
||||
output: { token: DAI, value: 4200 },
|
||||
receipt: '',
|
||||
timestamp: Date.now(),
|
||||
})
|
||||
break
|
||||
case 'ERROR':
|
||||
setTransaction((tx) => {
|
||||
invariant(tx)
|
||||
tx.status = new Error(
|
||||
'Swap failed: Unknown error: "Error: Lorem ipsum dolor sit amet, consectetur adipiscing elit. Praesent pulvinar, risus eu pretium condimentum, tellus dui fermentum turpis, id gravida metus justo ac lorem. Etiam vitae dapibus eros, nec elementum ipsum. Duis condimentum, felis vel tempor ultricies, eros diam tempus odio, at tempor urna odio id massa. Aliquam laoreet turpis justo, auctor accumsan est pellentesque at. Integer et dolor feugiat, sodales tortor non, cursus augue. Phasellus id suscipit justo, in ultricies tortor. Aenean libero nibh, egestas sit amet vehicula sit amet, tempor ac ligula. Cras at tempor lectus. Mauris sollicitudin est velit, nec consectetur lorem dapibus ut. Praesent magna ex, faucibus ac fermentum malesuada, molestie at ex. Phasellus bibendum lorem nec dolor dignissim eleifend. Nam dignissim varius velit, at volutpat justo pretium id."'
|
||||
)
|
||||
tx.elapsedMs = Date.now() - tx.timestamp
|
||||
})
|
||||
break
|
||||
case 'SUCCESS':
|
||||
setTransaction((tx) => {
|
||||
invariant(tx)
|
||||
tx.status = true
|
||||
tx.elapsedMs = Date.now() - tx.timestamp
|
||||
})
|
||||
break
|
||||
}
|
||||
}, [setTransaction, state])
|
||||
return <StatusDialog onClose={() => void 0} />
|
||||
return null
|
||||
// TODO(zzmp): Mock <StatusDialog tx={} onClose={() => void 0} />
|
||||
}
|
||||
|
||||
export default (
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import { useAtomValue } from 'jotai/utils'
|
||||
import ErrorDialog, { StatusHeader } from 'lib/components/Error/ErrorDialog'
|
||||
import useInterval from 'lib/hooks/useInterval'
|
||||
import { CheckCircle, Clock, Spinner } from 'lib/icons'
|
||||
import { Transaction, transactionAtom } from 'lib/state/swap'
|
||||
import { SwapTransactionInfo, Transaction } from 'lib/state/transactions'
|
||||
import styled, { ThemedText } from 'lib/theme'
|
||||
import { useCallback, useMemo, useState } from 'react'
|
||||
|
||||
@@ -24,17 +23,17 @@ const TransactionRow = styled(Row)`
|
||||
flex-direction: row-reverse;
|
||||
`
|
||||
|
||||
function ElapsedTime({ tx }: { tx: Transaction | null }) {
|
||||
function ElapsedTime({ tx }: { tx: Transaction<SwapTransactionInfo> }) {
|
||||
const [elapsedMs, setElapsedMs] = useState(0)
|
||||
useInterval(
|
||||
() => {
|
||||
if (tx?.elapsedMs) {
|
||||
setElapsedMs(tx.elapsedMs)
|
||||
} else if (tx?.timestamp) {
|
||||
setElapsedMs(Date.now() - tx.timestamp)
|
||||
if (tx.info.response.timestamp) {
|
||||
setElapsedMs(tx.info.response.timestamp - tx.addedTime)
|
||||
} else {
|
||||
setElapsedMs(Date.now() - tx.addedTime)
|
||||
}
|
||||
},
|
||||
elapsedMs === tx?.elapsedMs ? null : 1000
|
||||
elapsedMs === tx.info.response.timestamp ? null : 1000
|
||||
)
|
||||
const toElapsedTime = useCallback((ms: number) => {
|
||||
let sec = Math.floor(ms / 1000)
|
||||
@@ -63,22 +62,23 @@ const EtherscanA = styled.a`
|
||||
text-decoration: none;
|
||||
`
|
||||
|
||||
interface TransactionStatusProps extends StatusProps {
|
||||
tx: Transaction | null
|
||||
interface TransactionStatusProps {
|
||||
tx: Transaction<SwapTransactionInfo>
|
||||
onClose: () => void
|
||||
}
|
||||
|
||||
function TransactionStatus({ tx, onClose }: TransactionStatusProps) {
|
||||
const Icon = useMemo(() => {
|
||||
return tx?.status ? CheckCircle : Spinner
|
||||
}, [tx?.status])
|
||||
return tx.receipt?.status ? CheckCircle : Spinner
|
||||
}, [tx.receipt?.status])
|
||||
const heading = useMemo(() => {
|
||||
return tx?.status ? <Trans>Transaction submitted</Trans> : <Trans>Transaction pending</Trans>
|
||||
}, [tx?.status])
|
||||
return tx.receipt?.status ? <Trans>Transaction submitted</Trans> : <Trans>Transaction pending</Trans>
|
||||
}, [tx.receipt?.status])
|
||||
return (
|
||||
<Column flex padded gap={0.75} align="stretch" style={{ height: '100%' }}>
|
||||
<StatusHeader icon={Icon} iconColor={tx?.status && 'success'}>
|
||||
<StatusHeader icon={Icon} iconColor={tx.receipt?.status ? 'success' : undefined}>
|
||||
<ThemedText.Subhead1>{heading}</ThemedText.Subhead1>
|
||||
{tx ? <Summary input={tx.input} output={tx.output} /> : <div style={{ height: '1.25em' }} />}
|
||||
<Summary input={tx.info.inputCurrencyAmount} output={tx.info.outputCurrencyAmount} />
|
||||
</StatusHeader>
|
||||
<TransactionRow flex>
|
||||
<ThemedText.ButtonSmall>
|
||||
@@ -95,15 +95,14 @@ function TransactionStatus({ tx, onClose }: TransactionStatusProps) {
|
||||
)
|
||||
}
|
||||
|
||||
interface StatusProps {
|
||||
onClose: () => void
|
||||
}
|
||||
|
||||
export default function TransactionStatusDialog({ onClose }: StatusProps) {
|
||||
const tx = useAtomValue(transactionAtom)
|
||||
|
||||
return tx?.status instanceof Error ? (
|
||||
<ErrorDialog header={errorMessage} error={tx.status} action={<Trans>Dismiss</Trans>} onAction={onClose} />
|
||||
export default function TransactionStatusDialog({ tx, onClose }: TransactionStatusProps) {
|
||||
return tx.receipt?.status === 0 ? (
|
||||
<ErrorDialog
|
||||
header={errorMessage}
|
||||
error={new Error('TODO(zzmp)')}
|
||||
action={<Trans>Dismiss</Trans>}
|
||||
onAction={onClose}
|
||||
/>
|
||||
) : (
|
||||
<TransactionStatus tx={tx} onClose={onClose} />
|
||||
)
|
||||
|
||||
@@ -1,43 +1,50 @@
|
||||
import { tokens } from '@uniswap/default-token-list'
|
||||
import { SupportedChainId } from 'constants/chains'
|
||||
import { nativeOnChain } from 'constants/tokens'
|
||||
import { useUpdateAtom } from 'jotai/utils'
|
||||
import { DAI, ETH } from 'lib/mocks'
|
||||
import { Field, outputAtom, stateAtom } from 'lib/state/swap'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useValue } from 'react-cosmos/fixture'
|
||||
import { useSwapInfo } from 'lib/hooks/swap'
|
||||
import { SwapInfoUpdater } 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(stateAtom)
|
||||
const [, setInitialized] = useState(false)
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
const setState = useUpdateAtom(swapAtom)
|
||||
const {
|
||||
allowedSlippage,
|
||||
trade: { trade },
|
||||
} = useSwapInfo()
|
||||
|
||||
useEffect(() => {
|
||||
setState({
|
||||
activeInput: Field.INPUT,
|
||||
input: { token: ETH, value: 1, usdc: 4195 },
|
||||
output: { token: DAI, value: 4200, usdc: 4200 },
|
||||
swap: {
|
||||
lpFee: 0.0005,
|
||||
integratorFee: 0.00025,
|
||||
priceImpact: 0.01,
|
||||
slippageTolerance: 0.5,
|
||||
minimumReceived: 4190,
|
||||
},
|
||||
independentField: Field.INPUT,
|
||||
amount: '1',
|
||||
[Field.INPUT]: ETH,
|
||||
[Field.OUTPUT]: UNI,
|
||||
})
|
||||
setInitialized(true)
|
||||
})
|
||||
}, [setState])
|
||||
|
||||
const setOutput = useUpdateAtom(outputAtom)
|
||||
const [price] = useValue('output value', { defaultValue: 4200 })
|
||||
useEffect(() => {
|
||||
setState((state) => ({ ...state, output: { token: DAI, value: price, usdc: price } }))
|
||||
}, [price, setOutput, setState])
|
||||
|
||||
return (
|
||||
return trade ? (
|
||||
<Modal color="dialog">
|
||||
<SummaryDialog onConfirm={() => void 0} />
|
||||
<SummaryDialog onConfirm={() => void 0} trade={trade} allowedSlippage={allowedSlippage} />
|
||||
</Modal>
|
||||
)
|
||||
) : null
|
||||
}
|
||||
|
||||
export default <Fixture />
|
||||
export default (
|
||||
<>
|
||||
<SwapInfoUpdater />
|
||||
<Fixture />
|
||||
</>
|
||||
)
|
||||
|
||||
@@ -1,57 +1,119 @@
|
||||
import { t } from '@lingui/macro'
|
||||
import { State } from 'lib/state/swap'
|
||||
import { ThemedText } from 'lib/theme'
|
||||
import { Token } from 'lib/types'
|
||||
import { useLingui } from '@lingui/react'
|
||||
import { Trade } from '@uniswap/router-sdk'
|
||||
import { Currency, Percent, TradeType } from '@uniswap/sdk-core'
|
||||
import { ALLOWED_PRICE_IMPACT_HIGH, ALLOWED_PRICE_IMPACT_MEDIUM } from 'constants/misc'
|
||||
import { useAtomValue } from 'jotai/utils'
|
||||
import { MIN_HIGH_SLIPPAGE } from 'lib/state/settings'
|
||||
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, computeRealizedPriceImpact } from 'utils/prices'
|
||||
|
||||
import Row from '../../Row'
|
||||
|
||||
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 }: DetailProps) {
|
||||
function Detail({ label, value, color }: DetailProps) {
|
||||
return (
|
||||
<ThemedText.Caption>
|
||||
<Row gap={2}>
|
||||
<span>{label}</span>
|
||||
<span style={{ whiteSpace: 'nowrap' }}>{value}</span>
|
||||
<Value color={color}>{value}</Value>
|
||||
</Row>
|
||||
</ThemedText.Caption>
|
||||
)
|
||||
}
|
||||
|
||||
interface DetailsProps {
|
||||
swap: Required<State>['swap']
|
||||
input: Token
|
||||
output: Token
|
||||
trade: Trade<Currency, Currency, TradeType>
|
||||
allowedSlippage: Percent
|
||||
}
|
||||
|
||||
export default function Details({
|
||||
input: { symbol: inputSymbol },
|
||||
output: { symbol: outputSymbol },
|
||||
swap,
|
||||
}: DetailsProps) {
|
||||
const integrator = window.location.hostname
|
||||
const details = useMemo((): [string, string][] => {
|
||||
return [
|
||||
[t`Liquidity provider fee`, `${swap.lpFee} ${inputSymbol}`],
|
||||
[t`${integrator} fee`, swap.integratorFee && `${swap.integratorFee} ${inputSymbol}`],
|
||||
[t`Price impact`, `${swap.priceImpact}%`],
|
||||
[t`Maximum sent`, swap.maximumSent && `${swap.maximumSent} ${inputSymbol}`],
|
||||
[t`Minimum received`, swap.minimumReceived && `${swap.minimumReceived} ${outputSymbol}`],
|
||||
[t`Slippage tolerance`, `${swap.slippageTolerance}%`],
|
||||
].filter(isDetail)
|
||||
export default function Details({ trade, allowedSlippage }: DetailsProps) {
|
||||
const { inputAmount, outputAmount } = trade
|
||||
const inputCurrency = inputAmount.currency
|
||||
const outputCurrency = outputAmount.currency
|
||||
const priceImpact = useMemo(() => computeRealizedPriceImpact(trade), [trade])
|
||||
|
||||
function isDetail(detail: unknown[]): detail is [string, string] {
|
||||
return Boolean(detail[1])
|
||||
const lpFeeAmount = useMemo(() => computeRealizedLPFeeAmount(trade), [trade])
|
||||
|
||||
const integrator = window.location.hostname
|
||||
const feeOptions = useAtomValue(feeOptionsAtom)
|
||||
|
||||
const { i18n } = useLingui()
|
||||
const details = useMemo(() => {
|
||||
const rows = []
|
||||
// @TODO(ianlapham): Check that provider fee is even a valid list item
|
||||
|
||||
if (feeOptions) {
|
||||
const parsedConvenienceFee = formatCurrencyAmount(outputAmount.multiply(feeOptions.fee), 6, i18n.locale)
|
||||
rows.push([
|
||||
t`${integrator} fee`,
|
||||
`${parsedConvenienceFee} ${outputCurrency.symbol || currencyId(outputCurrency)}`,
|
||||
])
|
||||
}
|
||||
}, [inputSymbol, outputSymbol, swap, integrator])
|
||||
|
||||
const priceImpactRow = [t`Price impact`, `${priceImpact.toFixed(2)}%`]
|
||||
if (priceImpact.greaterThan(ALLOWED_PRICE_IMPACT_HIGH)) {
|
||||
priceImpactRow.push('error')
|
||||
} else if (priceImpact.greaterThan(ALLOWED_PRICE_IMPACT_MEDIUM)) {
|
||||
priceImpactRow.push('warning')
|
||||
}
|
||||
rows.push(priceImpactRow)
|
||||
|
||||
if (lpFeeAmount) {
|
||||
const localizedFeeAmount = formatCurrencyAmount(lpFeeAmount, 6, i18n.locale)
|
||||
rows.push([
|
||||
t`Liquidity provider fee`,
|
||||
`${localizedFeeAmount} ${inputCurrency.symbol || currencyId(inputCurrency)}`,
|
||||
])
|
||||
}
|
||||
|
||||
if (trade.tradeType === TradeType.EXACT_OUTPUT) {
|
||||
const localizedMaxSent = formatCurrencyAmount(trade.maximumAmountIn(allowedSlippage), 6, i18n.locale)
|
||||
rows.push([t`Maximum sent`, `${localizedMaxSent} ${inputCurrency.symbol}`])
|
||||
}
|
||||
|
||||
if (trade.tradeType === TradeType.EXACT_INPUT) {
|
||||
const localizedMaxSent = formatCurrencyAmount(trade.minimumAmountOut(allowedSlippage), 6, i18n.locale)
|
||||
rows.push([t`Minimum received`, `${localizedMaxSent} ${outputCurrency.symbol}`])
|
||||
}
|
||||
|
||||
const slippageToleranceRow = [t`Slippage tolerance`, `${allowedSlippage.toFixed(2)}%`]
|
||||
if (allowedSlippage.greaterThan(MIN_HIGH_SLIPPAGE)) {
|
||||
slippageToleranceRow.push('warning')
|
||||
}
|
||||
rows.push(slippageToleranceRow)
|
||||
|
||||
return rows
|
||||
}, [
|
||||
feeOptions,
|
||||
priceImpact,
|
||||
lpFeeAmount,
|
||||
trade,
|
||||
allowedSlippage,
|
||||
outputAmount,
|
||||
i18n.locale,
|
||||
integrator,
|
||||
outputCurrency,
|
||||
inputCurrency,
|
||||
])
|
||||
return (
|
||||
<>
|
||||
{details.map(([label, detail]) => (
|
||||
<Detail key={label} label={label} value={detail} />
|
||||
{details.map(([label, detail, color]) => (
|
||||
<Detail key={label} label={label} value={detail} color={color as Color} />
|
||||
))}
|
||||
</>
|
||||
)
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
import { useLingui } from '@lingui/react'
|
||||
import { Currency, CurrencyAmount } from '@uniswap/sdk-core'
|
||||
import { useUSDCValue } from 'hooks/useUSDCPrice'
|
||||
import { ArrowRight } from 'lib/icons'
|
||||
import { Input } from 'lib/state/swap'
|
||||
import styled from 'lib/theme'
|
||||
import { ThemedText } from 'lib/theme'
|
||||
import { useMemo } from 'react'
|
||||
import { computeFiatValuePriceImpact } from 'utils/computeFiatValuePriceImpact'
|
||||
import { formatCurrencyAmount } from 'utils/formatCurrencyAmount'
|
||||
|
||||
import Column from '../../Column'
|
||||
import Row from '../../Row'
|
||||
@@ -13,31 +17,35 @@ const Percent = styled.span<{ gain: boolean }>`
|
||||
`
|
||||
|
||||
interface TokenValueProps {
|
||||
input: Required<Pick<Input, 'token' | 'value'>> & Input
|
||||
input: CurrencyAmount<Currency>
|
||||
usdc?: boolean
|
||||
change?: number
|
||||
}
|
||||
|
||||
function TokenValue({ input, usdc, change }: TokenValueProps) {
|
||||
const { i18n } = useLingui()
|
||||
const percent = useMemo(() => {
|
||||
if (change) {
|
||||
const percent = (change * 100).toPrecision(3)
|
||||
const percent = change.toPrecision(3)
|
||||
return change > 0 ? `(+${percent}%)` : `(${percent}%)`
|
||||
}
|
||||
return undefined
|
||||
}, [change])
|
||||
|
||||
const usdcAmount = useUSDCValue(input)
|
||||
|
||||
return (
|
||||
<Column justify="flex-start">
|
||||
<Row gap={0.375} justify="flex-start">
|
||||
<TokenImg token={input.token} />
|
||||
<TokenImg token={input.currency} />
|
||||
<ThemedText.Body2>
|
||||
{input.value} {input.token.symbol}
|
||||
{formatCurrencyAmount(input, 6, i18n.locale)} {input.currency.symbol}
|
||||
</ThemedText.Body2>
|
||||
</Row>
|
||||
{usdc && input.usdc && (
|
||||
{usdc && usdcAmount && (
|
||||
<Row justify="flex-start">
|
||||
<ThemedText.Caption color="secondary">
|
||||
~ ${input.usdc.toLocaleString('en')}
|
||||
${formatCurrencyAmount(usdcAmount, 2, i18n.locale)}
|
||||
{change && <Percent gain={change > 0}> {percent}</Percent>}
|
||||
</ThemedText.Caption>
|
||||
</Row>
|
||||
@@ -47,23 +55,25 @@ function TokenValue({ input, usdc, change }: TokenValueProps) {
|
||||
}
|
||||
|
||||
interface SummaryProps {
|
||||
input: Required<Pick<Input, 'token' | 'value'>> & Input
|
||||
output: Required<Pick<Input, 'token' | 'value'>> & Input
|
||||
input: CurrencyAmount<Currency>
|
||||
output: CurrencyAmount<Currency>
|
||||
usdc?: boolean
|
||||
}
|
||||
|
||||
export default function Summary({ input, output, usdc }: SummaryProps) {
|
||||
const change = useMemo(() => {
|
||||
if (usdc && input.usdc && output.usdc) {
|
||||
return output.usdc / input.usdc - 1
|
||||
}
|
||||
return undefined
|
||||
}, [usdc, input.usdc, output.usdc])
|
||||
const inputUSDCValue = useUSDCValue(input)
|
||||
const outputUSDCValue = useUSDCValue(output)
|
||||
|
||||
const priceImpact = useMemo(() => {
|
||||
const computedChange = computeFiatValuePriceImpact(inputUSDCValue, outputUSDCValue)
|
||||
return computedChange ? parseFloat(computedChange.multiply(-1)?.toSignificant(3)) : undefined
|
||||
}, [inputUSDCValue, outputUSDCValue])
|
||||
|
||||
return (
|
||||
<Row gap={usdc ? 1 : 0.25}>
|
||||
<TokenValue input={input} usdc={usdc} />
|
||||
<ArrowRight />
|
||||
<TokenValue input={output} usdc={usdc} change={change} />
|
||||
<TokenValue input={output} usdc={usdc} change={priceImpact} />
|
||||
</Row>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,11 +1,20 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import { useLingui } from '@lingui/react'
|
||||
import { Trade } from '@uniswap/router-sdk'
|
||||
import { Currency, Percent, TradeType } from '@uniswap/sdk-core'
|
||||
import { ALLOWED_PRICE_IMPACT_HIGH, ALLOWED_PRICE_IMPACT_MEDIUM } from 'constants/misc'
|
||||
import { useAtomValue } from 'jotai/utils'
|
||||
import { IconButton } from 'lib/components/Button'
|
||||
import useScrollbar from 'lib/hooks/useScrollbar'
|
||||
import { Expando, Info } from 'lib/icons'
|
||||
import { Input, inputAtom, outputAtom, swapAtom } from 'lib/state/swap'
|
||||
import { AlertTriangle, Expando, Info } from 'lib/icons'
|
||||
import { MIN_HIGH_SLIPPAGE } from 'lib/state/settings'
|
||||
import { Field, independentFieldAtom } from 'lib/state/swap'
|
||||
import styled, { ThemedText } from 'lib/theme'
|
||||
import formatLocaleNumber from 'lib/utils/formatLocaleNumber'
|
||||
import { useMemo, useState } from 'react'
|
||||
import { formatCurrencyAmount, formatPrice } from 'utils/formatCurrencyAmount'
|
||||
import { computeRealizedPriceImpact } from 'utils/prices'
|
||||
import { tradeMeaningfullyDiffers } from 'utils/tradeMeaningFullyDiffer'
|
||||
|
||||
import ActionButton from '../../ActionButton'
|
||||
import Column from '../../Column'
|
||||
@@ -17,12 +26,6 @@ import Summary from './Summary'
|
||||
|
||||
export default Summary
|
||||
|
||||
function asInput(input: Input): (Required<Pick<Input, 'token' | 'value'>> & Input) | undefined {
|
||||
return input.token && input.value ? (input as Required<Pick<Input, 'token' | 'value'>>) : undefined
|
||||
}
|
||||
|
||||
const updated = { message: <Trans>Price updated</Trans>, action: <Trans>Accept</Trans> }
|
||||
|
||||
const SummaryColumn = styled(Column)``
|
||||
const ExpandoColumn = styled(Column)``
|
||||
const DetailsColumn = styled(Column)``
|
||||
@@ -47,6 +50,7 @@ const Body = styled(Column)<{ open: boolean }>`
|
||||
|
||||
${Column} {
|
||||
height: 100%;
|
||||
grid-template-rows: repeat(auto-fill, 1em);
|
||||
padding: ${({ open }) => (open ? '0.5em 0' : 0)};
|
||||
transition: padding 0.25s;
|
||||
|
||||
@@ -64,6 +68,7 @@ const Body = styled(Column)<{ open: boolean }>`
|
||||
|
||||
${Estimate} {
|
||||
max-height: ${({ open }) => (open ? 0 : 56 / 12)}em; // 2 * line-height + padding
|
||||
min-height: 0;
|
||||
overflow-y: hidden;
|
||||
padding: ${({ open }) => (open ? 0 : '1em 0')};
|
||||
transition: ${({ open }) =>
|
||||
@@ -74,29 +79,43 @@ const Body = styled(Column)<{ open: boolean }>`
|
||||
}
|
||||
`
|
||||
|
||||
const priceUpdate = { message: <Trans>Price updated</Trans>, action: <Trans>Accept</Trans> }
|
||||
|
||||
interface SummaryDialogProps {
|
||||
trade: Trade<Currency, Currency, TradeType>
|
||||
allowedSlippage: Percent
|
||||
onConfirm: () => void
|
||||
}
|
||||
|
||||
export function SummaryDialog({ onConfirm }: SummaryDialogProps) {
|
||||
const swap = useAtomValue(swapAtom)
|
||||
const partialInput = useAtomValue(inputAtom)
|
||||
const partialOutput = useAtomValue(outputAtom)
|
||||
const input = asInput(partialInput)
|
||||
const output = asInput(partialOutput)
|
||||
export function SummaryDialog({ trade, allowedSlippage, onConfirm }: SummaryDialogProps) {
|
||||
const { inputAmount, outputAmount, executionPrice } = trade
|
||||
const inputCurrency = inputAmount.currency
|
||||
const outputCurrency = outputAmount.currency
|
||||
const priceImpact = useMemo(() => computeRealizedPriceImpact(trade), [trade])
|
||||
|
||||
const price = useMemo(() => {
|
||||
return input && output ? output.value / input.value : undefined
|
||||
}, [input, output])
|
||||
const [confirmedPrice, confirmPrice] = useState(price)
|
||||
const independentField = useAtomValue(independentFieldAtom)
|
||||
|
||||
const [open, setOpen] = useState(true)
|
||||
const warning = useMemo(() => {
|
||||
if (priceImpact.greaterThan(ALLOWED_PRICE_IMPACT_HIGH)) return 'error'
|
||||
if (priceImpact.greaterThan(ALLOWED_PRICE_IMPACT_MEDIUM)) return 'warning'
|
||||
if (allowedSlippage.greaterThan(MIN_HIGH_SLIPPAGE)) return 'warning'
|
||||
return
|
||||
}, [allowedSlippage, priceImpact])
|
||||
|
||||
const [confirmedTrade, setConfirmedTrade] = useState(trade)
|
||||
const doesTradeDiffer = useMemo(
|
||||
() => Boolean(trade && confirmedTrade && tradeMeaningfullyDiffers(trade, confirmedTrade)),
|
||||
[confirmedTrade, trade]
|
||||
)
|
||||
const [open, setOpen] = useState(false)
|
||||
|
||||
const [details, setDetails] = useState<HTMLDivElement | null>(null)
|
||||
|
||||
const scrollbar = useScrollbar(details)
|
||||
|
||||
if (!(input && output && swap)) {
|
||||
const { i18n } = useLingui()
|
||||
|
||||
if (!(inputAmount && outputAmount && inputCurrency && outputCurrency)) {
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -105,15 +124,16 @@ export function SummaryDialog({ onConfirm }: SummaryDialogProps) {
|
||||
<Header title={<Trans>Swap summary</Trans>} ruled />
|
||||
<Body flex align="stretch" gap={0.75} padded open={open}>
|
||||
<SummaryColumn gap={0.75} flex justify="center">
|
||||
<Summary input={input} output={output} usdc={true} />
|
||||
<Summary input={inputAmount} output={outputAmount} usdc={true} />
|
||||
<ThemedText.Caption>
|
||||
1 {input.token.symbol} = {price} {output.token.symbol}
|
||||
{formatLocaleNumber({ number: 1, sigFigs: 1, locale: i18n.locale })} {inputCurrency.symbol} ={' '}
|
||||
{formatPrice(executionPrice, 6, i18n.locale)} {outputCurrency.symbol}
|
||||
</ThemedText.Caption>
|
||||
</SummaryColumn>
|
||||
<Rule />
|
||||
<Row>
|
||||
<Row gap={0.5}>
|
||||
<Info color="secondary" />
|
||||
{warning ? <AlertTriangle color={warning} /> : <Info color="secondary" />}
|
||||
<ThemedText.Subhead2 color="secondary">
|
||||
<Trans>Swap details</Trans>
|
||||
</ThemedText.Subhead2>
|
||||
@@ -124,26 +144,29 @@ export function SummaryDialog({ onConfirm }: SummaryDialogProps) {
|
||||
<Rule />
|
||||
<DetailsColumn>
|
||||
<Column gap={0.5} ref={setDetails} css={scrollbar}>
|
||||
<Details input={input.token} output={output.token} swap={swap} />
|
||||
<Details trade={trade} allowedSlippage={allowedSlippage} />
|
||||
</Column>
|
||||
</DetailsColumn>
|
||||
<Estimate color="secondary">
|
||||
<Trans>Output is estimated.</Trans>{' '}
|
||||
{swap?.minimumReceived && (
|
||||
<Trans>Output is estimated.</Trans>
|
||||
{independentField === Field.INPUT && (
|
||||
<Trans>
|
||||
You will receive at least {swap.minimumReceived} {output.token.symbol} or the transaction will revert.
|
||||
You will receive at least{' '}
|
||||
{formatCurrencyAmount(trade.minimumAmountOut(allowedSlippage), 6, i18n.locale)} {outputCurrency.symbol}{' '}
|
||||
or the transaction will revert.
|
||||
</Trans>
|
||||
)}
|
||||
{swap?.maximumSent && (
|
||||
{independentField === Field.OUTPUT && (
|
||||
<Trans>
|
||||
You will send at most {swap.maximumSent} {input.token.symbol} or the transaction will revert.
|
||||
You will send at most {formatCurrencyAmount(trade.maximumAmountIn(allowedSlippage), 6, i18n.locale)}{' '}
|
||||
{inputCurrency.symbol} or the transaction will revert.
|
||||
</Trans>
|
||||
)}
|
||||
</Estimate>
|
||||
<ActionButton
|
||||
onClick={onConfirm}
|
||||
onUpdate={() => confirmPrice(price)}
|
||||
updated={price === confirmedPrice ? undefined : updated}
|
||||
onUpdate={() => setConfirmedTrade(trade)}
|
||||
update={doesTradeDiffer ? priceUpdate : undefined}
|
||||
>
|
||||
<Trans>Confirm swap</Trans>
|
||||
</ActionButton>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { useAtom } from 'jotai'
|
||||
import { tokens } from '@uniswap/default-token-list'
|
||||
import { DAI, USDC } from 'constants/tokens'
|
||||
import { useUpdateAtom } from 'jotai/utils'
|
||||
import { inputAtom, outputAtom, swapAtom } from 'lib/state/swap'
|
||||
import { useEffect } from 'react'
|
||||
import { useValue } from 'react-cosmos/fixture'
|
||||
import { useSelect, useValue } from 'react-cosmos/fixture'
|
||||
|
||||
import Swap from '.'
|
||||
import { colorAtom } from './Output'
|
||||
@@ -17,42 +17,6 @@ const validateColor = (() => {
|
||||
})()
|
||||
|
||||
function Fixture() {
|
||||
const [input, setInput] = useAtom(inputAtom)
|
||||
const [output, setOutput] = useAtom(outputAtom)
|
||||
const [swap, setSwap] = useAtom(swapAtom)
|
||||
const [priceFetched] = useValue('price fetched', { defaultValue: false })
|
||||
useEffect(() => {
|
||||
if (priceFetched && input.token && output.token) {
|
||||
const inputValue = input.value || 1
|
||||
const inputUsdc = input.usdc || inputValue
|
||||
const outputValue = output.value || 1
|
||||
const outputUsdc = output.usdc || outputValue
|
||||
if (!(inputValue === input.value && inputUsdc === input.usdc)) {
|
||||
setInput({ ...input, value: inputValue, usdc: inputUsdc })
|
||||
}
|
||||
if (!(outputValue === output.value && outputUsdc === output.usdc)) {
|
||||
setOutput({ ...output, value: outputValue, usdc: outputUsdc })
|
||||
}
|
||||
if (!swap || swap.minimumReceived !== outputValue * 0.995) {
|
||||
setSwap({
|
||||
lpFee: 0.0005,
|
||||
priceImpact: 0.01,
|
||||
slippageTolerance: 0.5,
|
||||
minimumReceived: outputValue * 0.995,
|
||||
})
|
||||
}
|
||||
} else if (swap) {
|
||||
setSwap(undefined)
|
||||
}
|
||||
}, [input, output, priceFetched, setInput, setOutput, setSwap, swap])
|
||||
|
||||
const [tokenApproved] = useValue('token approved', { defaultValue: true })
|
||||
useEffect(() => {
|
||||
if (tokenApproved !== input.approved) {
|
||||
setInput({ ...input, approved: tokenApproved })
|
||||
}
|
||||
}, [input, setInput, tokenApproved])
|
||||
|
||||
const setColor = useUpdateAtom(colorAtom)
|
||||
const [color] = useValue('token color', { defaultValue: '' })
|
||||
useEffect(() => {
|
||||
@@ -61,7 +25,54 @@ function Fixture() {
|
||||
}
|
||||
}, [color, setColor])
|
||||
|
||||
return <Swap />
|
||||
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> = {
|
||||
none: '',
|
||||
Native: 'NATIVE',
|
||||
DAI: DAI.address,
|
||||
USDC: USDC.address,
|
||||
}
|
||||
const addressOptions = Object.keys(optionsToAddressMap)
|
||||
const [defaultInput] = useSelect('defaultInputAddress', {
|
||||
options: addressOptions,
|
||||
defaultValue: addressOptions[2],
|
||||
})
|
||||
const inputOptions = ['', '0', '100', '-1']
|
||||
const [defaultInputAmount] = useSelect('defaultInputAmount', {
|
||||
options: inputOptions,
|
||||
defaultValue: inputOptions[2],
|
||||
})
|
||||
const [defaultOutput] = useSelect('defaultOutputAddress', {
|
||||
options: addressOptions,
|
||||
defaultValue: addressOptions[1],
|
||||
})
|
||||
const [defaultOutputAmount] = useSelect('defaultOutputAmount', {
|
||||
options: inputOptions,
|
||||
defaultValue: inputOptions[0],
|
||||
})
|
||||
|
||||
return (
|
||||
<Swap
|
||||
convenienceFee={convenienceFee}
|
||||
convenienceFeeRecipient={convenienceFeeRecipient}
|
||||
defaultInputAddress={optionsToAddressMap[defaultInput]}
|
||||
defaultInputAmount={defaultInputAmount}
|
||||
defaultOutputAddress={optionsToAddressMap[defaultOutput]}
|
||||
defaultOutputAmount={defaultOutputAmount}
|
||||
tokenList={tokens}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default <Fixture />
|
||||
|
||||
@@ -1,56 +1,173 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import { Token, TradeType } from '@uniswap/sdk-core'
|
||||
import { CHAIN_INFO } from 'constants/chainInfo'
|
||||
import { useERC20PermitFromTrade } from 'hooks/useERC20Permit'
|
||||
import { useUpdateAtom } from 'jotai/utils'
|
||||
import { useAtomValue } from 'jotai/utils'
|
||||
import { inputAtom, outputAtom, swapAtom } from 'lib/state/swap'
|
||||
import { useCallback, useMemo, useState } from 'react'
|
||||
import { useSwapInfo } from 'lib/hooks/swap'
|
||||
import useSwapApproval, {
|
||||
ApprovalState,
|
||||
useSwapApprovalOptimizedTrade,
|
||||
useSwapRouterAddress,
|
||||
} from 'lib/hooks/swap/useSwapApproval'
|
||||
import { useSwapCallback } from 'lib/hooks/swap/useSwapCallback'
|
||||
import { useAddTransaction } from 'lib/hooks/transactions'
|
||||
import { usePendingApproval } from 'lib/hooks/transactions'
|
||||
import useActiveWeb3React from 'lib/hooks/useActiveWeb3React'
|
||||
import useTransactionDeadline from 'lib/hooks/useTransactionDeadline'
|
||||
import { Link, Spinner } from 'lib/icons'
|
||||
import { displayTxHashAtom, Field, independentFieldAtom } from 'lib/state/swap'
|
||||
import { TransactionType } from 'lib/state/transactions'
|
||||
import styled, { useTheme } from 'lib/theme'
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import invariant from 'tiny-invariant'
|
||||
|
||||
import ActionButton from '../ActionButton'
|
||||
import Dialog from '../Dialog'
|
||||
import { StatusDialog } from './Status'
|
||||
import Row from '../Row'
|
||||
import { SummaryDialog } from './Summary'
|
||||
|
||||
const mockBalance = 123.45
|
||||
|
||||
enum Mode {
|
||||
NONE,
|
||||
SUMMARY,
|
||||
STATUS,
|
||||
interface SwapButtonProps {
|
||||
disabled?: boolean
|
||||
}
|
||||
|
||||
export default function SwapButton() {
|
||||
const swap = useAtomValue(swapAtom)
|
||||
const input = useAtomValue(inputAtom)
|
||||
const output = useAtomValue(outputAtom)
|
||||
const balance = mockBalance
|
||||
const [mode, setMode] = useState(Mode.NONE)
|
||||
const EtherscanA = styled.a`
|
||||
color: currentColor;
|
||||
text-decoration: none;
|
||||
`
|
||||
|
||||
function useIsPendingApproval(token?: Token, spender?: string): boolean {
|
||||
return Boolean(usePendingApproval(token, spender))
|
||||
}
|
||||
|
||||
export default function SwapButton({ disabled }: SwapButtonProps) {
|
||||
const { account, chainId } = useActiveWeb3React()
|
||||
|
||||
const { tokenColorExtraction } = useTheme()
|
||||
|
||||
const {
|
||||
trade,
|
||||
allowedSlippage,
|
||||
currencies: { [Field.INPUT]: inputCurrency },
|
||||
currencyBalances: { [Field.INPUT]: inputCurrencyBalance },
|
||||
currencyAmounts: { [Field.INPUT]: inputCurrencyAmount, [Field.OUTPUT]: outputCurrencyAmount },
|
||||
feeOptions,
|
||||
} = useSwapInfo()
|
||||
|
||||
const independentField = useAtomValue(independentFieldAtom)
|
||||
|
||||
const [activeTrade, setActiveTrade] = useState<typeof trade.trade | undefined>()
|
||||
useEffect(() => {
|
||||
setActiveTrade((activeTrade) => activeTrade && trade.trade)
|
||||
}, [trade])
|
||||
|
||||
// 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, allowedSlippage, useIsPendingApproval) || trade.trade
|
||||
const [approval, getApproval] = useSwapApproval(optimizedTrade, allowedSlippage, useIsPendingApproval)
|
||||
const approvalHash = usePendingApproval(
|
||||
inputCurrency?.isToken ? inputCurrency : undefined,
|
||||
useSwapRouterAddress(optimizedTrade)
|
||||
)
|
||||
|
||||
const addTransaction = useAddTransaction()
|
||||
const addApprovalTransaction = useCallback(() => {
|
||||
getApproval().then((transaction) => {
|
||||
if (transaction) {
|
||||
addTransaction({ type: TransactionType.APPROVAL, ...transaction })
|
||||
}
|
||||
})
|
||||
}, [addTransaction, getApproval])
|
||||
|
||||
const actionProps = useMemo(() => {
|
||||
if (swap && input.token && input.value && output.token && output.value && input.value <= balance) {
|
||||
if (input.approved) {
|
||||
return {}
|
||||
} else {
|
||||
if (disabled) return { disabled: true }
|
||||
|
||||
if (chainId && inputCurrencyAmount) {
|
||||
if (!inputCurrencyBalance || inputCurrencyBalance.lessThan(inputCurrencyAmount)) {
|
||||
return { disabled: true }
|
||||
} else if (approval === ApprovalState.PENDING) {
|
||||
return {
|
||||
updated: { message: <Trans>Approve {input.token.symbol} first</Trans>, action: <Trans>Approve</Trans> },
|
||||
disabled: true,
|
||||
update: {
|
||||
message: (
|
||||
<EtherscanA href={approvalHash && `${CHAIN_INFO[chainId].explorer}tx/${approvalHash}`} target="_blank">
|
||||
<Row gap={0.25}>
|
||||
<Trans>
|
||||
Approval pending <Link />
|
||||
</Trans>
|
||||
</Row>
|
||||
</EtherscanA>
|
||||
),
|
||||
action: <Trans>Approve</Trans>,
|
||||
icon: Spinner,
|
||||
},
|
||||
}
|
||||
} else if (approval === ApprovalState.NOT_APPROVED) {
|
||||
return {
|
||||
update: {
|
||||
message: <Trans>Approve {inputCurrencyAmount.currency.symbol} first</Trans>,
|
||||
action: <Trans>Approve</Trans>,
|
||||
},
|
||||
}
|
||||
}
|
||||
return {}
|
||||
}
|
||||
|
||||
return { disabled: true }
|
||||
}, [balance, input.approved, input.token, input.value, output.token, output.value, swap])
|
||||
}, [approval, approvalHash, chainId, disabled, inputCurrencyAmount, inputCurrencyBalance])
|
||||
|
||||
const deadline = useTransactionDeadline()
|
||||
const { signatureData } = useERC20PermitFromTrade(optimizedTrade, allowedSlippage, deadline)
|
||||
|
||||
// the callback to execute the swap
|
||||
const { callback: swapCallback } = useSwapCallback({
|
||||
trade: optimizedTrade,
|
||||
allowedSlippage,
|
||||
recipientAddressOrName: account ?? null,
|
||||
signatureData,
|
||||
deadline,
|
||||
feeOptions,
|
||||
})
|
||||
|
||||
//@TODO(ianlapham): add a loading state, process errors
|
||||
const setDisplayTxHash = useUpdateAtom(displayTxHashAtom)
|
||||
|
||||
const onConfirm = useCallback(() => {
|
||||
// TODO: Send the tx to the connected wallet.
|
||||
setMode(Mode.STATUS)
|
||||
}, [])
|
||||
swapCallback?.()
|
||||
.then((response) => {
|
||||
setDisplayTxHash(response.hash)
|
||||
invariant(inputCurrencyAmount && outputCurrencyAmount)
|
||||
addTransaction({
|
||||
response,
|
||||
type: TransactionType.SWAP,
|
||||
tradeType: independentField === Field.INPUT ? TradeType.EXACT_INPUT : TradeType.EXACT_OUTPUT,
|
||||
inputCurrencyAmount,
|
||||
outputCurrencyAmount,
|
||||
})
|
||||
})
|
||||
.catch((error) => {
|
||||
//@TODO(ianlapham): add error handling
|
||||
console.log(error)
|
||||
})
|
||||
.finally(() => {
|
||||
setActiveTrade(undefined)
|
||||
})
|
||||
}, [addTransaction, independentField, inputCurrencyAmount, outputCurrencyAmount, setDisplayTxHash, swapCallback])
|
||||
|
||||
return (
|
||||
<>
|
||||
<ActionButton color="interactive" onClick={() => setMode(Mode.SUMMARY)} onUpdate={() => void 0} {...actionProps}>
|
||||
<ActionButton
|
||||
color={tokenColorExtraction ? 'interactive' : 'accent'}
|
||||
onClick={() => setActiveTrade(trade.trade)}
|
||||
onUpdate={addApprovalTransaction}
|
||||
{...actionProps}
|
||||
>
|
||||
<Trans>Review swap</Trans>
|
||||
</ActionButton>
|
||||
{mode >= Mode.SUMMARY && (
|
||||
<Dialog color="dialog" onClose={() => setMode(Mode.NONE)}>
|
||||
<SummaryDialog onConfirm={onConfirm} />
|
||||
</Dialog>
|
||||
)}
|
||||
{mode >= Mode.STATUS && (
|
||||
<Dialog color="dialog">
|
||||
<StatusDialog onClose={() => setMode(Mode.NONE)} />
|
||||
{activeTrade && (
|
||||
<Dialog color="dialog" onClose={() => setActiveTrade(undefined)}>
|
||||
<SummaryDialog trade={activeTrade} allowedSlippage={allowedSlippage} onConfirm={onConfirm} />
|
||||
</Dialog>
|
||||
)}
|
||||
</>
|
||||
|
||||
76
src/lib/components/Swap/SwapPropValidator.tsx
Normal file
76
src/lib/components/Swap/SwapPropValidator.tsx
Normal file
@@ -0,0 +1,76 @@
|
||||
import { BigNumber } from '@ethersproject/bignumber'
|
||||
import { DefaultAddress, SwapProps } from 'lib/components/Swap'
|
||||
import { IntegrationError } from 'lib/errors'
|
||||
import { PropsWithChildren, useEffect } from 'react'
|
||||
|
||||
import { isAddress } from '../../../utils'
|
||||
|
||||
function isAddressOrAddressMap(addressOrMap: DefaultAddress): boolean {
|
||||
if (typeof addressOrMap === 'object') {
|
||||
return Object.values(addressOrMap).every((address) => isAddress(address))
|
||||
}
|
||||
if (typeof addressOrMap === 'string') {
|
||||
return typeof isAddress(addressOrMap) === 'string'
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
type ValidatorProps = PropsWithChildren<SwapProps>
|
||||
|
||||
export default function SwapPropValidator(props: ValidatorProps) {
|
||||
const { convenienceFee, convenienceFeeRecipient } = props
|
||||
useEffect(() => {
|
||||
if (convenienceFee) {
|
||||
if (convenienceFee > 100 || convenienceFee < 0) {
|
||||
throw new IntegrationError(`convenienceFee must be between 0 and 100. (You set it to ${convenienceFee})`)
|
||||
}
|
||||
if (!convenienceFeeRecipient) {
|
||||
throw new IntegrationError('convenienceFeeRecipient is required when convenienceFee is set.')
|
||||
}
|
||||
|
||||
if (typeof convenienceFeeRecipient === 'string') {
|
||||
if (!isAddress(convenienceFeeRecipient)) {
|
||||
throw new IntegrationError(
|
||||
`convenienceFeeRecipient must be a valid address. (You set it to ${convenienceFeeRecipient}.)`
|
||||
)
|
||||
}
|
||||
} else if (typeof convenienceFeeRecipient === 'object') {
|
||||
Object.values(convenienceFeeRecipient).forEach((recipient) => {
|
||||
if (!isAddress(recipient)) {
|
||||
const values = Object.values(convenienceFeeRecipient).join(', ')
|
||||
throw new IntegrationError(
|
||||
`All values in convenienceFeeRecipient object must be valid addresses. (You used ${values}.)`
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}, [convenienceFee, convenienceFeeRecipient])
|
||||
|
||||
const { defaultInputAddress, defaultInputAmount, defaultOutputAddress, defaultOutputAmount } = props
|
||||
useEffect(() => {
|
||||
if (defaultOutputAmount && defaultInputAmount) {
|
||||
throw new IntegrationError('defaultInputAmount and defaultOutputAmount may not both be defined.')
|
||||
}
|
||||
if (defaultInputAmount && BigNumber.from(defaultInputAmount).lt(0)) {
|
||||
throw new IntegrationError(`defaultInputAmount must be a positive number. (You set it to ${defaultInputAmount})`)
|
||||
}
|
||||
if (defaultOutputAmount && BigNumber.from(defaultOutputAmount).lt(0)) {
|
||||
throw new IntegrationError(
|
||||
`defaultOutputAmount must be a positive number. (You set it to ${defaultOutputAmount})`
|
||||
)
|
||||
}
|
||||
if (defaultInputAddress && !isAddressOrAddressMap(defaultInputAddress) && defaultInputAddress !== 'NATIVE') {
|
||||
throw new IntegrationError(
|
||||
`defaultInputAddress(es) must be a valid address or "NATIVE". (You set it to ${defaultInputAddress}`
|
||||
)
|
||||
}
|
||||
if (defaultOutputAddress && !isAddressOrAddressMap(defaultOutputAddress) && defaultOutputAddress !== 'NATIVE') {
|
||||
throw new IntegrationError(
|
||||
`defaultOutputAddress(es) must be a valid address or "NATIVE". (You set it to ${defaultOutputAddress}`
|
||||
)
|
||||
}
|
||||
}, [defaultInputAddress, defaultInputAmount, defaultOutputAddress, defaultOutputAmount])
|
||||
|
||||
return <>{props.children}</>
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import { Input } from 'lib/state/swap'
|
||||
import { Currency } from '@uniswap/sdk-core'
|
||||
import { loadingOpacityCss } from 'lib/css/loading'
|
||||
import styled, { keyframes, ThemedText } from 'lib/theme'
|
||||
import { Token } from 'lib/types'
|
||||
import { FocusEvent, ReactNode, useCallback, useRef, useState } from 'react'
|
||||
|
||||
import Button from '../Button'
|
||||
@@ -14,7 +14,7 @@ const TokenInputRow = styled(Row)`
|
||||
grid-template-columns: 1fr;
|
||||
`
|
||||
|
||||
const ValueInput = styled(DecimalInput)`
|
||||
const ValueInput = styled(DecimalInput)<{ $loading: boolean }>`
|
||||
color: ${({ theme }) => theme.primary};
|
||||
|
||||
:hover:not(:focus-within) {
|
||||
@@ -24,6 +24,8 @@ const ValueInput = styled(DecimalInput)`
|
||||
:hover:not(:focus-within)::placeholder {
|
||||
color: ${({ theme }) => theme.onHover(theme.secondary)};
|
||||
}
|
||||
|
||||
${loadingOpacityCss}
|
||||
`
|
||||
|
||||
const delayedFadeIn = keyframes`
|
||||
@@ -45,20 +47,24 @@ const MaxButton = styled(Button)`
|
||||
`
|
||||
|
||||
interface TokenInputProps {
|
||||
input: Input
|
||||
currency?: Currency
|
||||
amount: string
|
||||
disabled?: boolean
|
||||
onMax?: () => void
|
||||
onChangeInput: (input: number | undefined) => void
|
||||
onChangeToken: (token: Token) => void
|
||||
onChangeInput: (input: string) => void
|
||||
onChangeCurrency: (currency: Currency) => void
|
||||
loading?: boolean
|
||||
children: ReactNode
|
||||
}
|
||||
|
||||
export default function TokenInput({
|
||||
input: { value, token },
|
||||
currency,
|
||||
amount,
|
||||
disabled,
|
||||
onMax,
|
||||
onChangeInput,
|
||||
onChangeToken,
|
||||
onChangeCurrency,
|
||||
loading,
|
||||
children,
|
||||
}: TokenInputProps) {
|
||||
const max = useRef<HTMLButtonElement>(null)
|
||||
@@ -69,15 +75,17 @@ export default function TokenInput({
|
||||
setShowMax(false)
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<Column gap={0.25}>
|
||||
<TokenInputRow gap={0.5} onBlur={onBlur}>
|
||||
<ThemedText.H2>
|
||||
<ValueInput
|
||||
value={value}
|
||||
value={amount}
|
||||
onFocus={onFocus}
|
||||
onChange={onChangeInput}
|
||||
disabled={disabled || !token}
|
||||
disabled={disabled || !currency}
|
||||
$loading={Boolean(loading)}
|
||||
></ValueInput>
|
||||
</ThemedText.H2>
|
||||
{showMax && (
|
||||
@@ -87,7 +95,7 @@ export default function TokenInput({
|
||||
</ThemedText.ButtonMedium>
|
||||
</MaxButton>
|
||||
)}
|
||||
<TokenSelect value={token} collapsed={showMax} disabled={disabled} onSelect={onChangeToken} />
|
||||
<TokenSelect value={currency} collapsed={showMax} disabled={disabled} onSelect={onChangeCurrency} />
|
||||
</TokenInputRow>
|
||||
{children}
|
||||
</Column>
|
||||
|
||||
@@ -1,124 +0,0 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import { useAtomValue } from 'jotai/utils'
|
||||
import { AlertTriangle, Info, largeIconCss, Spinner } from 'lib/icons'
|
||||
import { Field, Input, inputAtom, outputAtom, stateAtom, swapAtom } from 'lib/state/swap'
|
||||
import styled, { ThemedText, ThemeProvider } from 'lib/theme'
|
||||
import { useMemo, useState } from 'react'
|
||||
|
||||
import { TextButton } from '../Button'
|
||||
import Row from '../Row'
|
||||
import Rule from '../Rule'
|
||||
import Tooltip from '../Tooltip'
|
||||
|
||||
const mockBalance = 123.45
|
||||
|
||||
function RoutingTooltip() {
|
||||
return (
|
||||
<Tooltip icon={Info} placement="bottom">
|
||||
<ThemeProvider>
|
||||
<ThemedText.Subhead2>TODO: Routing Tooltip</ThemedText.Subhead2>
|
||||
</ThemeProvider>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
|
||||
type FilledInput = Input & Required<Pick<Input, 'token' | 'value'>>
|
||||
|
||||
function asFilledInput(input: Input): FilledInput | undefined {
|
||||
return input.token && input.value ? (input as FilledInput) : undefined
|
||||
}
|
||||
|
||||
interface LoadedStateProps {
|
||||
input: FilledInput
|
||||
output: FilledInput
|
||||
}
|
||||
|
||||
function LoadedState({ input, output }: LoadedStateProps) {
|
||||
const [flip, setFlip] = useState(true)
|
||||
const ratio = useMemo(() => {
|
||||
const [a, b] = flip ? [output, input] : [input, output]
|
||||
const ratio = `1 ${a.token.symbol} = ${b.value / a.value} ${b.token.symbol}`
|
||||
const usdc = a.usdc && ` ($${(a.usdc / a.value).toLocaleString('en')})`
|
||||
return (
|
||||
<Row gap={0.25} style={{ userSelect: 'text' }}>
|
||||
{ratio}
|
||||
{usdc && <ThemedText.Caption color="secondary">{usdc}</ThemedText.Caption>}
|
||||
</Row>
|
||||
)
|
||||
}, [flip, input, output])
|
||||
|
||||
return (
|
||||
<TextButton color="primary" onClick={() => setFlip(!flip)}>
|
||||
{ratio}
|
||||
</TextButton>
|
||||
)
|
||||
}
|
||||
|
||||
const ToolbarRow = styled(Row)`
|
||||
padding: 0.5em 0;
|
||||
${largeIconCss}
|
||||
`
|
||||
|
||||
export default function Toolbar({ disabled }: { disabled?: boolean }) {
|
||||
const { activeInput } = useAtomValue(stateAtom)
|
||||
const swap = useAtomValue(swapAtom)
|
||||
const input = useAtomValue(inputAtom)
|
||||
const output = useAtomValue(outputAtom)
|
||||
const balance = mockBalance
|
||||
|
||||
const caption = useMemo(() => {
|
||||
const filledInput = asFilledInput(input)
|
||||
const filledOutput = asFilledInput(output)
|
||||
if (disabled) {
|
||||
return (
|
||||
<>
|
||||
<AlertTriangle color="secondary" />
|
||||
<Trans>Connect wallet to swap</Trans>
|
||||
</>
|
||||
)
|
||||
}
|
||||
if (activeInput === Field.INPUT ? filledInput && output.token : filledOutput && input.token) {
|
||||
if (!swap) {
|
||||
return (
|
||||
<>
|
||||
<Spinner color="secondary" />
|
||||
<Trans>Fetching best price…</Trans>
|
||||
</>
|
||||
)
|
||||
}
|
||||
if (filledInput && filledInput.value > balance) {
|
||||
return (
|
||||
<>
|
||||
<AlertTriangle color="secondary" />
|
||||
<Trans>Insufficient {filledInput.token.symbol}</Trans>
|
||||
</>
|
||||
)
|
||||
}
|
||||
if (filledInput && filledOutput) {
|
||||
return (
|
||||
<>
|
||||
<RoutingTooltip />
|
||||
<LoadedState input={filledInput} output={filledOutput} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<Info color="secondary" />
|
||||
<Trans>Enter an amount</Trans>
|
||||
</>
|
||||
)
|
||||
}, [activeInput, balance, disabled, input, output, swap])
|
||||
|
||||
return (
|
||||
<>
|
||||
<Rule />
|
||||
<ThemedText.Caption>
|
||||
<ToolbarRow justify="flex-start" gap={0.5} iconSize={4 / 3}>
|
||||
{caption}
|
||||
</ToolbarRow>
|
||||
</ThemedText.Caption>
|
||||
</>
|
||||
)
|
||||
}
|
||||
81
src/lib/components/Swap/Toolbar/Caption.tsx
Normal file
81
src/lib/components/Swap/Toolbar/Caption.tsx
Normal file
@@ -0,0 +1,81 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import { Currency, TradeType } from '@uniswap/sdk-core'
|
||||
import useUSDCPrice from 'hooks/useUSDCPrice'
|
||||
import { AlertTriangle, Icon, Info, Spinner } from 'lib/icons'
|
||||
import { ThemedText } from 'lib/theme'
|
||||
import { ReactNode, useMemo, useState } from 'react'
|
||||
import { InterfaceTrade } from 'state/routing/types'
|
||||
|
||||
import { TextButton } from '../../Button'
|
||||
import Row from '../../Row'
|
||||
import RoutingTooltip from './RoutingTooltip'
|
||||
|
||||
interface CaptionProps {
|
||||
icon?: Icon
|
||||
caption: ReactNode
|
||||
}
|
||||
|
||||
function Caption({ icon: Icon = AlertTriangle, caption }: CaptionProps) {
|
||||
return (
|
||||
<>
|
||||
<Icon color="secondary" />
|
||||
{caption}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export function ConnectWallet() {
|
||||
return <Caption caption={<Trans>Connect wallet to swap</Trans>} />
|
||||
}
|
||||
export function UnsupportedNetwork() {
|
||||
return <Caption caption={<Trans>Unsupported network - switch to another to trade.</Trans>} />
|
||||
}
|
||||
export function InsufficientBalance({ currency }: { currency: Currency }) {
|
||||
return <Caption caption={<Trans>Insufficient {currency?.symbol} balance</Trans>} />
|
||||
}
|
||||
export function InsufficientLiquidity() {
|
||||
return <Caption caption={<Trans>Insufficient liquidity in the pool for your trade</Trans>} />
|
||||
}
|
||||
export function Empty() {
|
||||
return <Caption icon={Info} caption={<Trans>Enter an amount</Trans>} />
|
||||
}
|
||||
export function LoadingTrade() {
|
||||
return <Caption icon={Spinner} caption={<Trans>Fetching best price…</Trans>} />
|
||||
}
|
||||
|
||||
export function Trade({ trade }: { trade: InterfaceTrade<Currency, Currency, TradeType> }) {
|
||||
const [flip, setFlip] = useState(true)
|
||||
const { inputAmount, outputAmount, executionPrice } = trade
|
||||
const fiatValueInput = useUSDCPrice(inputAmount.currency)
|
||||
const fiatValueOutput = useUSDCPrice(outputAmount.currency)
|
||||
|
||||
const ratio = useMemo(() => {
|
||||
const [a, b] = flip ? [outputAmount, inputAmount] : [inputAmount, outputAmount]
|
||||
const priceString = (!flip ? executionPrice : executionPrice?.invert())?.toSignificant(6)
|
||||
|
||||
const ratio = `1 ${a.currency.symbol} = ${priceString} ${b.currency.symbol}`
|
||||
const usdc = !flip
|
||||
? fiatValueInput
|
||||
? ` ($${fiatValueInput.toSignificant(6)})`
|
||||
: null
|
||||
: fiatValueOutput
|
||||
? ` ($${fiatValueOutput.toSignificant(6)})`
|
||||
: null
|
||||
|
||||
return (
|
||||
<Row gap={0.25} style={{ userSelect: 'text' }}>
|
||||
{ratio}
|
||||
{usdc && <ThemedText.Caption color="secondary">{usdc}</ThemedText.Caption>}
|
||||
</Row>
|
||||
)
|
||||
}, [executionPrice, fiatValueInput, fiatValueOutput, flip, inputAmount, outputAmount])
|
||||
|
||||
return (
|
||||
<>
|
||||
<RoutingTooltip />
|
||||
<TextButton color="primary" onClick={() => setFlip(!flip)}>
|
||||
{ratio}
|
||||
</TextButton>
|
||||
</>
|
||||
)
|
||||
}
|
||||
14
src/lib/components/Swap/Toolbar/RoutingTooltip.tsx
Normal file
14
src/lib/components/Swap/Toolbar/RoutingTooltip.tsx
Normal file
@@ -0,0 +1,14 @@
|
||||
import { Info } from 'lib/icons'
|
||||
|
||||
export default function RoutingTooltip() {
|
||||
return <Info color="secondary" />
|
||||
/* TODO(zzmp): Implement post-beta launch.
|
||||
return (
|
||||
<Tooltip icon={Info} placement="bottom">
|
||||
<ThemeProvider>
|
||||
<ThemedText.Subhead2>TODO: Routing Tooltip</ThemedText.Subhead2>
|
||||
</ThemeProvider>
|
||||
</Tooltip>
|
||||
)
|
||||
*/
|
||||
}
|
||||
72
src/lib/components/Swap/Toolbar/index.tsx
Normal file
72
src/lib/components/Swap/Toolbar/index.tsx
Normal file
@@ -0,0 +1,72 @@
|
||||
import { ALL_SUPPORTED_CHAIN_IDS } from 'constants/chains'
|
||||
import { useIsAmountPopulated, useSwapInfo } from 'lib/hooks/swap'
|
||||
import useActiveWeb3React from 'lib/hooks/useActiveWeb3React'
|
||||
import { largeIconCss } from 'lib/icons'
|
||||
import { Field } from 'lib/state/swap'
|
||||
import styled, { ThemedText } from 'lib/theme'
|
||||
import { useMemo } from 'react'
|
||||
import { TradeState } from 'state/routing/types'
|
||||
|
||||
import Row from '../../Row'
|
||||
import Rule from '../../Rule'
|
||||
import * as Caption from './Caption'
|
||||
|
||||
const ToolbarRow = styled(Row)`
|
||||
padding: 0.5em 0;
|
||||
${largeIconCss}
|
||||
`
|
||||
|
||||
export default function Toolbar({ disabled }: { disabled?: boolean }) {
|
||||
const { chainId } = useActiveWeb3React()
|
||||
const {
|
||||
trade: { trade, state },
|
||||
currencies: { [Field.INPUT]: inputCurrency, [Field.OUTPUT]: outputCurrency },
|
||||
currencyBalances: { [Field.INPUT]: balance },
|
||||
} = useSwapInfo()
|
||||
|
||||
const [routeFound, routeIsLoading] = useMemo(
|
||||
() => [Boolean(trade?.swaps), TradeState.LOADING === state || TradeState.SYNCING === state],
|
||||
[state, trade?.swaps]
|
||||
)
|
||||
|
||||
const isAmountPopulated = useIsAmountPopulated()
|
||||
|
||||
const caption = useMemo(() => {
|
||||
if (disabled) {
|
||||
return <Caption.ConnectWallet />
|
||||
}
|
||||
|
||||
if (chainId && !ALL_SUPPORTED_CHAIN_IDS.includes(chainId)) {
|
||||
return <Caption.UnsupportedNetwork />
|
||||
}
|
||||
|
||||
if (balance && trade?.inputAmount.greaterThan(balance)) {
|
||||
return <Caption.InsufficientBalance currency={trade.inputAmount.currency} />
|
||||
}
|
||||
|
||||
if (inputCurrency && outputCurrency && isAmountPopulated) {
|
||||
if (!trade || routeIsLoading) {
|
||||
return <Caption.LoadingTrade />
|
||||
}
|
||||
if (!routeFound) {
|
||||
return <Caption.InsufficientLiquidity />
|
||||
}
|
||||
if (trade.inputAmount && trade.outputAmount) {
|
||||
return <Caption.Trade trade={trade} />
|
||||
}
|
||||
}
|
||||
|
||||
return <Caption.Empty />
|
||||
}, [balance, chainId, disabled, inputCurrency, isAmountPopulated, outputCurrency, routeFound, routeIsLoading, trade])
|
||||
|
||||
return (
|
||||
<>
|
||||
<Rule />
|
||||
<ThemedText.Caption>
|
||||
<ToolbarRow justify="flex-start" gap={0.5} iconSize={4 / 3}>
|
||||
{caption}
|
||||
</ToolbarRow>
|
||||
</ThemedText.Caption>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -1,9 +1,18 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import { TokenInfo } from '@uniswap/token-lists'
|
||||
import { ALL_SUPPORTED_CHAIN_IDS } from 'constants/chains'
|
||||
import { useAtom } from 'jotai'
|
||||
import { SwapInfoUpdater } from 'lib/hooks/swap/useSwapInfo'
|
||||
import useSyncConvenienceFee from 'lib/hooks/swap/useSyncConvenienceFee'
|
||||
import useSyncSwapDefaults from 'lib/hooks/swap/useSyncSwapDefaults'
|
||||
import { usePendingTransactions } from 'lib/hooks/transactions'
|
||||
import useActiveWeb3React from 'lib/hooks/useActiveWeb3React'
|
||||
import useTokenList, { DEFAULT_TOKEN_LIST } from 'lib/hooks/useTokenList'
|
||||
import useTokenList from 'lib/hooks/useTokenList'
|
||||
import { displayTxHashAtom } from 'lib/state/swap'
|
||||
import { SwapTransactionInfo, Transaction, TransactionType } from 'lib/state/transactions'
|
||||
import { useMemo, useState } from 'react'
|
||||
|
||||
import Dialog from '../Dialog'
|
||||
import Header from '../Header'
|
||||
import { BoundaryProvider } from '../Popover'
|
||||
import Wallet from '../Wallet'
|
||||
@@ -11,47 +20,56 @@ import Input from './Input'
|
||||
import Output from './Output'
|
||||
import ReverseButton from './ReverseButton'
|
||||
import Settings from './Settings'
|
||||
import { StatusDialog } from './Status'
|
||||
import SwapButton from './SwapButton'
|
||||
import SwapPropValidator from './SwapPropValidator'
|
||||
import Toolbar from './Toolbar'
|
||||
|
||||
interface DefaultTokenAmount {
|
||||
address?: string | { [chainId: number]: string }
|
||||
amount?: number
|
||||
}
|
||||
export type DefaultAddress = string | { [chainId: number]: string | 'NATIVE' } | 'NATIVE'
|
||||
|
||||
interface SwapDefaults {
|
||||
tokenList: string | TokenInfo[]
|
||||
input: DefaultTokenAmount
|
||||
output: DefaultTokenAmount
|
||||
}
|
||||
|
||||
const DEFAULT_INPUT: DefaultTokenAmount = { address: 'ETH' }
|
||||
const DEFAULT_OUTPUT: DefaultTokenAmount = {}
|
||||
|
||||
function useSwapDefaults(defaults: Partial<SwapDefaults> = {}): SwapDefaults {
|
||||
const tokenList = defaults.tokenList || DEFAULT_TOKEN_LIST
|
||||
const input: DefaultTokenAmount = defaults.input || DEFAULT_INPUT
|
||||
const output: DefaultTokenAmount = defaults.output || DEFAULT_OUTPUT
|
||||
input.amount = input.amount || 0
|
||||
output.amount = output.amount || 0
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
return useMemo(() => ({ tokenList, input, output }), [])
|
||||
function getSwapTx(txs: { [hash: string]: Transaction }, hash?: string): Transaction<SwapTransactionInfo> | undefined {
|
||||
if (hash) {
|
||||
const tx = txs[hash]
|
||||
if (tx?.info?.type === TransactionType.SWAP) {
|
||||
return tx as Transaction<SwapTransactionInfo>
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
export interface SwapProps {
|
||||
defaults?: Partial<SwapDefaults>
|
||||
tokenList?: string | TokenInfo[]
|
||||
defaultInputAddress?: DefaultAddress
|
||||
defaultInputAmount?: string
|
||||
defaultOutputAddress?: DefaultAddress
|
||||
defaultOutputAmount?: string
|
||||
convenienceFee?: number
|
||||
convenienceFeeRecipient?: string | { [chainId: number]: string }
|
||||
onConnectWallet?: () => void
|
||||
}
|
||||
|
||||
export default function Swap({ defaults }: SwapProps) {
|
||||
const { tokenList } = useSwapDefaults(defaults)
|
||||
useTokenList(tokenList)
|
||||
export default function Swap(props: SwapProps) {
|
||||
const list = useTokenList(props.tokenList)
|
||||
useSyncSwapDefaults(props)
|
||||
useSyncConvenienceFee(props)
|
||||
|
||||
const { active, account, chainId } = useActiveWeb3React()
|
||||
const [boundary, setBoundary] = useState<HTMLDivElement | null>(null)
|
||||
const { active, account } = useActiveWeb3React()
|
||||
|
||||
const [displayTxHash, setDisplayTxHash] = useAtom(displayTxHashAtom)
|
||||
const pendingTxs = usePendingTransactions()
|
||||
const displayTx = getSwapTx(pendingTxs, displayTxHash)
|
||||
|
||||
const onSupportedChain = useMemo(
|
||||
() => chainId && ALL_SUPPORTED_CHAIN_IDS.includes(chainId) && list.some((token) => token.chainId === chainId),
|
||||
[chainId, list]
|
||||
)
|
||||
|
||||
return (
|
||||
<>
|
||||
<Header logo title={<Trans>Swap</Trans>}>
|
||||
{active && <Wallet disabled={!account} />}
|
||||
<SwapPropValidator {...props}>
|
||||
{onSupportedChain && <SwapInfoUpdater />}
|
||||
<Header title={<Trans>Swap</Trans>}>
|
||||
{active && <Wallet disabled={!account} onClick={props.onConnectWallet} />}
|
||||
<Settings disabled={!active} />
|
||||
</Header>
|
||||
<div ref={setBoundary}>
|
||||
@@ -60,10 +78,15 @@ export default function Swap({ defaults }: SwapProps) {
|
||||
<ReverseButton disabled={!active} />
|
||||
<Output disabled={!active}>
|
||||
<Toolbar disabled={!active} />
|
||||
<SwapButton />
|
||||
<SwapButton disabled={!account} />
|
||||
</Output>
|
||||
</BoundaryProvider>
|
||||
</div>
|
||||
</>
|
||||
{displayTx && (
|
||||
<Dialog color="dialog">
|
||||
<StatusDialog tx={displayTx} onClose={() => setDisplayTxHash()} />
|
||||
</Dialog>
|
||||
)}
|
||||
</SwapPropValidator>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,29 +1,31 @@
|
||||
import useNativeEvent from 'lib/hooks/useNativeEvent'
|
||||
import { Currency } from '@uniswap/sdk-core'
|
||||
import useCurrencyLogoURIs from 'lib/hooks/useCurrencyLogoURIs'
|
||||
import { Slash } from 'lib/icons'
|
||||
import styled from 'lib/theme'
|
||||
import uriToHttp from 'lib/utils/uriToHttp'
|
||||
import { useState } from 'react'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
|
||||
const badSrcs = new Set<string>()
|
||||
|
||||
interface TokenImgProps {
|
||||
className?: string
|
||||
token: {
|
||||
name?: string
|
||||
symbol: string
|
||||
logoURI?: string
|
||||
}
|
||||
token: Currency
|
||||
}
|
||||
const TRANSPARENT_SRC = 'data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=='
|
||||
|
||||
function TokenImg({ className, token }: TokenImgProps) {
|
||||
const [img, setImg] = useState<HTMLImageElement | null>(null)
|
||||
const src = token.logoURI ? uriToHttp(token.logoURI)[0] : TRANSPARENT_SRC
|
||||
useNativeEvent(img, 'error', () => {
|
||||
if (img) {
|
||||
// Use a local transparent gif to avoid the browser-dependent broken img icon.
|
||||
// The icon may still flash, but using a native event further reduces the duration.
|
||||
img.src = TRANSPARENT_SRC
|
||||
}
|
||||
})
|
||||
return <img className={className} src={src} alt={token.name || token.symbol} ref={setImg} />
|
||||
const srcs = useCurrencyLogoURIs(token)
|
||||
const [src, setSrc] = useState<string | undefined>()
|
||||
useEffect(() => {
|
||||
setSrc(srcs.find((src) => !badSrcs.has(src)))
|
||||
}, [srcs])
|
||||
const onError = useCallback(() => {
|
||||
if (src) badSrcs.add(src)
|
||||
setSrc(srcs.find((src) => !badSrcs.has(src)))
|
||||
}, [src, srcs])
|
||||
|
||||
if (src) {
|
||||
return <img className={className} src={src} alt={token.name || token.symbol} onError={onError} />
|
||||
}
|
||||
return <Slash className={className} color="secondary" />
|
||||
}
|
||||
|
||||
export default styled(TokenImg)<{ size?: number }>`
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Currency } from '@uniswap/sdk-core'
|
||||
import styled, { ThemedText } from 'lib/theme'
|
||||
import { Token } from 'lib/types'
|
||||
|
||||
import Button from '../Button'
|
||||
import Row from '../Row'
|
||||
@@ -11,8 +11,8 @@ const TokenButton = styled(Button)`
|
||||
`
|
||||
|
||||
interface TokenBaseProps {
|
||||
value: Token
|
||||
onClick: (value: Token) => void
|
||||
value: Currency
|
||||
onClick: (value: Currency) => void
|
||||
}
|
||||
|
||||
export default function TokenBase({ value, onClick }: TokenBaseProps) {
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import { Currency } from '@uniswap/sdk-core'
|
||||
import { ChevronDown } from 'lib/icons'
|
||||
import styled, { ThemedText } from 'lib/theme'
|
||||
import { Token } from 'lib/types'
|
||||
import { useMemo } from 'react'
|
||||
|
||||
import Button from '../Button'
|
||||
import Row from '../Row'
|
||||
@@ -27,16 +28,18 @@ const TokenButtonRow = styled(Row)<{ collapsed: boolean }>`
|
||||
`
|
||||
|
||||
interface TokenButtonProps {
|
||||
value?: Token
|
||||
value?: Currency
|
||||
collapsed: boolean
|
||||
disabled?: boolean
|
||||
onClick: () => void
|
||||
}
|
||||
|
||||
export default function TokenButton({ value, collapsed, disabled, onClick }: TokenButtonProps) {
|
||||
const buttonBackgroundColor = useMemo(() => (value ? 'interactive' : 'accent'), [value])
|
||||
const contentColor = useMemo(() => (value || disabled ? 'onInteractive' : 'onAccent'), [value, disabled])
|
||||
return (
|
||||
<StyledTokenButton onClick={onClick} empty={!value} color={value ? 'interactive' : 'accent'} disabled={disabled}>
|
||||
<ThemedText.ButtonLarge color="onInteractive">
|
||||
<StyledTokenButton onClick={onClick} empty={!value} color={buttonBackgroundColor} disabled={disabled}>
|
||||
<ThemedText.ButtonLarge color={contentColor}>
|
||||
<TokenButtonRow gap={0.4} collapsed={Boolean(value) && collapsed}>
|
||||
{value ? (
|
||||
<>
|
||||
@@ -46,7 +49,7 @@ export default function TokenButton({ value, collapsed, disabled, onClick }: Tok
|
||||
) : (
|
||||
<Trans>Select a token</Trans>
|
||||
)}
|
||||
<ChevronDown color="onInteractive" strokeWidth={3} />
|
||||
<ChevronDown color={contentColor} strokeWidth={3} />
|
||||
</TokenButtonRow>
|
||||
</ThemedText.ButtonLarge>
|
||||
</StyledTokenButton>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user