Compare commits
120 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9470c49d11 | ||
|
|
e1abd81a1d | ||
|
|
7d9657867d | ||
|
|
7cc52abb96 | ||
|
|
5ac41417b0 | ||
|
|
2c74c5f2df | ||
|
|
cbc2ff668e | ||
|
|
a73f59b4ff | ||
|
|
7a75626c31 | ||
|
|
a0e14bef10 | ||
|
|
9b5a53b2e8 | ||
|
|
50fdb36b6f | ||
|
|
b993902c73 | ||
|
|
828bf540ba | ||
|
|
7c88a5a008 | ||
|
|
360c5e2c96 | ||
|
|
72678ee667 | ||
|
|
4dd74f2144 | ||
|
|
e45c104135 | ||
|
|
98fcaacd9b | ||
|
|
93551579e4 | ||
|
|
8a9388ed81 | ||
|
|
884bf41da7 | ||
|
|
5b686aea97 | ||
|
|
c4a456a085 | ||
|
|
4b9098a7bf | ||
|
|
71a246f25c | ||
|
|
8de048bc84 | ||
|
|
163e2d5560 | ||
|
|
0edb0fe5e2 | ||
|
|
496408b3db | ||
|
|
78b6f5c72d | ||
|
|
f9fb71a803 | ||
|
|
59d0046411 | ||
|
|
b4e0234d07 | ||
|
|
4a8dbda0b8 | ||
|
|
0cbb24c614 | ||
|
|
a9dba258ff | ||
|
|
fa163cb938 | ||
|
|
b52273932a | ||
|
|
9ad8f80e4e | ||
|
|
69bc598dea | ||
|
|
7feba045fc | ||
|
|
04cee0a07d | ||
|
|
ea73260e56 | ||
|
|
b4bd2973a9 | ||
|
|
a071b8adb0 | ||
|
|
610acb0191 | ||
|
|
63bad8f890 | ||
|
|
32f955693f | ||
|
|
96c66a5846 | ||
|
|
8c269a6d39 | ||
|
|
36f111fa6f | ||
|
|
e569dc2152 | ||
|
|
1aa042c5ef | ||
|
|
1450315b98 | ||
|
|
aefbb3d812 | ||
|
|
c3f12398cd | ||
|
|
2272f2a01a | ||
|
|
fb71078ea2 | ||
|
|
1c7c93191e | ||
|
|
0713f730b3 | ||
|
|
5f7a18b411 | ||
|
|
020c8d181a | ||
|
|
ab3f024031 | ||
|
|
d989c61de5 | ||
|
|
5dd8059734 | ||
|
|
b50e5511ea | ||
|
|
1efe5e9cd5 | ||
|
|
2944dc4d0b | ||
|
|
29ae755f2a | ||
|
|
27b831b301 | ||
|
|
6d9d38819e | ||
|
|
2de29129ed | ||
|
|
52af0e506b | ||
|
|
4d69c946bf | ||
|
|
542bf0bf66 | ||
|
|
a4fbfae4ba | ||
|
|
b2288258f2 | ||
|
|
8703013b2d | ||
|
|
4f6173675d | ||
|
|
2469eb58b9 | ||
|
|
e0a8ac2408 | ||
|
|
0a736b5e62 | ||
|
|
b44eb8877c | ||
|
|
92e61fa34b | ||
|
|
ef62fd33b2 | ||
|
|
96a42f66d4 | ||
|
|
c446f20d2f | ||
|
|
5a1ef8fb7d | ||
|
|
2863971640 | ||
|
|
dcaf10ec29 | ||
|
|
bca5113569 | ||
|
|
6779c1a024 | ||
|
|
f79ef12494 | ||
|
|
7bcda46934 | ||
|
|
f4ba24cfd5 | ||
|
|
59c6ab16dd | ||
|
|
db17dcbf2c | ||
|
|
1835de7f5f | ||
|
|
00f158209c | ||
|
|
2108ceedd5 | ||
|
|
ad080470da | ||
|
|
fc34912b53 | ||
|
|
c25d2b894c | ||
|
|
83c99b8c04 | ||
|
|
ccdf1e7575 | ||
|
|
c9faafee5e | ||
|
|
26a44fb51b | ||
|
|
1e16ac8449 | ||
|
|
5b5e76573d | ||
|
|
27cdbd0d5f | ||
|
|
b2a30b9bf1 | ||
|
|
dfad7b89ab | ||
|
|
4fe35ea42e | ||
|
|
0d852b6165 | ||
|
|
8ac3b836bd | ||
|
|
12bc5957b4 | ||
|
|
a33187c33b | ||
|
|
248bc07cf1 |
2
.github/workflows/integration-tests.yaml
vendored
@@ -38,10 +38,10 @@ jobs:
|
||||
run: yarn install --frozen-lockfile
|
||||
|
||||
- run: yarn cypress install
|
||||
|
||||
- run: yarn build
|
||||
env:
|
||||
CI: false
|
||||
REACT_APP_NETWORK_URL: 'https://mainnet.infura.io/v3/4bf032f2d38a4ed6bb975b80d6340847'
|
||||
REACT_APP_SERVICE_WORKER: false
|
||||
|
||||
- run: yarn test:e2e
|
||||
|
||||
1
.gitignore
vendored
@@ -19,6 +19,7 @@
|
||||
|
||||
# builds
|
||||
/build
|
||||
/cosmos-export
|
||||
/dist
|
||||
/dts
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
],
|
||||
"webpack": {
|
||||
"configPath": "react-scripts/config/webpack.config",
|
||||
"overridePath": "cosmos.override.js"
|
||||
"overridePath": "cosmos.override.cjs"
|
||||
},
|
||||
"port": 5001
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,16 +2,27 @@ describe('Swap', () => {
|
||||
beforeEach(() => {
|
||||
cy.visit('/swap')
|
||||
})
|
||||
|
||||
it('starts with ETH selected by default', () => {
|
||||
cy.get('#swap-currency-input .token-amount-input').should('have.value', '')
|
||||
cy.get('#swap-currency-input .token-symbol-container').should('contain.text', 'ETH')
|
||||
cy.get('#swap-currency-output .token-amount-input').should('not.have.value')
|
||||
cy.get('#swap-currency-output .token-symbol-container').should('contain.text', 'Select a token')
|
||||
})
|
||||
|
||||
it('can enter an amount into input', () => {
|
||||
cy.get('#swap-currency-input .token-amount-input').type('0.001', { delay: 200 }).should('have.value', '0.001')
|
||||
cy.get('#swap-currency-input .token-amount-input')
|
||||
.clear()
|
||||
.type('0.001', { delay: 200 })
|
||||
.should('have.value', '0.001')
|
||||
})
|
||||
|
||||
it('zero swap amount', () => {
|
||||
cy.get('#swap-currency-input .token-amount-input').type('0.0', { delay: 200 }).should('have.value', '0.0')
|
||||
cy.get('#swap-currency-input .token-amount-input').clear().type('0.0', { delay: 200 }).should('have.value', '0.0')
|
||||
})
|
||||
|
||||
it('invalid swap amount', () => {
|
||||
cy.get('#swap-currency-input .token-amount-input').type('\\', { delay: 200 }).should('have.value', '')
|
||||
cy.get('#swap-currency-input .token-amount-input').clear().type('\\', { delay: 200 }).should('have.value', '')
|
||||
})
|
||||
|
||||
it('can enter an amount into output', () => {
|
||||
|
||||
81
package.json
@@ -1,15 +1,30 @@
|
||||
{
|
||||
"name": "@uniswap/widgets",
|
||||
"version": "0.0.14-beta",
|
||||
"version": "0.0.25-beta",
|
||||
"description": "Uniswap Interface",
|
||||
"homepage": ".",
|
||||
"main": "dist/widgets.js",
|
||||
"module": "dist/widgets.esm.js",
|
||||
"types": "dist/widgets.d.ts",
|
||||
"files": [
|
||||
"lib",
|
||||
"dist"
|
||||
],
|
||||
"type": "module",
|
||||
"types": "dist/index.d.ts",
|
||||
"main": "dist/cjs/index.cjs",
|
||||
"module": "dist/index.js",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"import": "./dist/index.js",
|
||||
"require": "./dist/cjs/index.cjs"
|
||||
},
|
||||
"./locales/*": {
|
||||
"import": "./dist/locales/*.js",
|
||||
"require": "./dist/cjs/locales/*.cjs"
|
||||
},
|
||||
"./fonts.css": {
|
||||
"import": "./dist/fonts.css",
|
||||
"require": "./dist/fonts.css"
|
||||
}
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/plugin-transform-runtime": "^7.17.0",
|
||||
"@babel/preset-env": "^7.16.11",
|
||||
@@ -21,6 +36,9 @@
|
||||
"@graphql-codegen/typescript-operations": "^1.18.2",
|
||||
"@graphql-codegen/typescript-rtk-query": "^1.1.1",
|
||||
"@lingui/cli": "^3.9.0",
|
||||
"@lingui/core": "^3.9.0",
|
||||
"@lingui/macro": "^3.9.0",
|
||||
"@lingui/react": "^3.9.0",
|
||||
"@metamask/jazzicon": "^2.0.0",
|
||||
"@reach/dialog": "^0.10.3",
|
||||
"@reach/portal": "^0.10.3",
|
||||
@@ -71,13 +89,8 @@
|
||||
"@uniswap/v2-periphery": "^1.1.0-beta.0",
|
||||
"@uniswap/v3-core": "1.0.0",
|
||||
"@uniswap/v3-periphery": "^1.1.1",
|
||||
"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",
|
||||
"@web3-react/metamask": "8.0.13-beta.0",
|
||||
"@web3-react/walletconnect": "8.0.18-beta.0",
|
||||
"array.prototype.flat": "^1.2.4",
|
||||
"array.prototype.flatmap": "^1.2.4",
|
||||
"babel-plugin-macros": "^3.1.0",
|
||||
@@ -108,15 +121,18 @@
|
||||
"react-ga": "^2.5.7",
|
||||
"react-is": "^17.0.2",
|
||||
"react-markdown": "^4.3.1",
|
||||
"react-redux": "^7.2.2",
|
||||
"react-router-dom": "^5.0.0",
|
||||
"react-scripts": "^4.0.3",
|
||||
"react-spring": "^8.0.27",
|
||||
"react-use-gesture": "^6.0.14",
|
||||
"redux": "^4.1.2",
|
||||
"redux-localstorage-simple": "^2.3.1",
|
||||
"rollup": "^2.63.0",
|
||||
"rollup-plugin-copy": "^3.4.0",
|
||||
"rollup-plugin-delete": "^2.0.0",
|
||||
"rollup-plugin-dts": "^4.1.0",
|
||||
"rollup-plugin-multi-input": "^1.3.1",
|
||||
"rollup-plugin-node-externals": "^3.1.2",
|
||||
"rollup-plugin-scss": "^3.0.0",
|
||||
"rollup-plugin-typescript2": "^0.31.1",
|
||||
@@ -130,6 +146,13 @@
|
||||
"use-resize-observer": "^8.0.0",
|
||||
"wcag-contrast": "^3.0.0",
|
||||
"web-vitals": "^2.1.0",
|
||||
"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.13",
|
||||
"workbox-core": "^6.1.0",
|
||||
"workbox-precaching": "^6.1.0",
|
||||
"workbox-routing": "^6.1.0"
|
||||
@@ -150,7 +173,7 @@
|
||||
"prepublishOnly": "yarn widgets:build",
|
||||
"start": "react-scripts start",
|
||||
"build": "react-scripts build",
|
||||
"test": "react-scripts test --env=./custom-test-env.js",
|
||||
"test": "react-scripts test --env=./custom-test-env.cjs",
|
||||
"test:e2e": "start-server-and-test 'serve build -l 3000' http://localhost:3000 'cypress run --record'",
|
||||
"widgets:start": "cosmos",
|
||||
"widgets:build": "rollup --config --failAfterWarnings --configPlugin typescript2"
|
||||
@@ -180,26 +203,27 @@
|
||||
"@ethersproject/contracts": "^5.4.1",
|
||||
"@ethersproject/experimental": "^5.4.0",
|
||||
"@ethersproject/hash": "^5.4.0",
|
||||
"@ethersproject/providers": "5.4.0",
|
||||
"@ethersproject/providers": "^5.4.0",
|
||||
"@ethersproject/solidity": "^5.4.0",
|
||||
"@ethersproject/strings": "^5.4.0",
|
||||
"@ethersproject/units": "^5.4.0",
|
||||
"@ethersproject/wallet": "^5.4.0",
|
||||
"@fontsource/ibm-plex-mono": "^4.5.1",
|
||||
"@fontsource/inter": "^4.5.1",
|
||||
"@lingui/core": "^3.9.0",
|
||||
"@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/smart-order-router": "^2.5.20",
|
||||
"@uniswap/token-lists": "^1.0.0-beta.27",
|
||||
"@uniswap/v2-sdk": "^3.0.1",
|
||||
"@uniswap/v3-sdk": "^3.8.2",
|
||||
"web3-react-core": "npm:@web3-react/core@^6.0.9",
|
||||
"@web3-react/core": "8.0.17-beta.0",
|
||||
"@web3-react/eip1193": "8.0.12-beta.0",
|
||||
"@web3-react/empty": "8.0.10-beta.0",
|
||||
"@web3-react/types": "8.0.10-beta.0",
|
||||
"@web3-react/url": "8.0.12-beta.0",
|
||||
"ajv": "^6.12.3",
|
||||
"cids": "^1.0.0",
|
||||
"immer": "^9.0.6",
|
||||
@@ -214,25 +238,26 @@
|
||||
"popper-max-size-modifier": "^0.2.0",
|
||||
"react-feather": "^2.0.8",
|
||||
"react-popper": "^2.2.3",
|
||||
"react-redux": "^7.2.2",
|
||||
"react-virtualized-auto-sizer": "^1.0.2",
|
||||
"react-window": "^1.8.5",
|
||||
"rebass": "^4.0.7",
|
||||
"redux": "^4.1.2",
|
||||
"setimmediate": "^1.0.5",
|
||||
"styled-components": "^5.3.0",
|
||||
"tiny-invariant": "^1.2.0",
|
||||
"wcag-contrast": "^3.0.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",
|
||||
"web3-react-core": "npm:@web3-react/core@^6.0.9",
|
||||
"wicg-inert": "^3.1.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@babel/runtime": "^7.17.0",
|
||||
"react": "^17.0.1",
|
||||
"react-dom": "^17.0.1"
|
||||
"react-dom": "^17.0.1",
|
||||
"react-redux": "^7.2.2",
|
||||
"redux": "^4.1.2"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"bufferutil": "^4.0.6",
|
||||
"encoding": "^0.1.13",
|
||||
"utf-8-validate": "^5.0.8"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,8 @@ import { RollupWarning } from 'rollup'
|
||||
import copy from 'rollup-plugin-copy'
|
||||
import del from 'rollup-plugin-delete'
|
||||
import dts from 'rollup-plugin-dts'
|
||||
// @ts-ignore // missing types
|
||||
import multi from 'rollup-plugin-multi-input'
|
||||
import externals from 'rollup-plugin-node-externals'
|
||||
import sass from 'rollup-plugin-scss'
|
||||
import { CompilerOptions } from 'typescript'
|
||||
@@ -24,6 +26,8 @@ import { CompilerOptions } from 'typescript'
|
||||
const REPLACEMENTS = {
|
||||
'process.env.REACT_APP_IS_WIDGET': true,
|
||||
'process.env.REACT_APP_LOCALES': '"./locales"',
|
||||
// esm requires fully-specified paths:
|
||||
'react/jsx-runtime': 'react/jsx-runtime.js',
|
||||
}
|
||||
|
||||
const EXTENSIONS = ['.js', '.jsx', '.ts', '.tsx']
|
||||
@@ -45,54 +49,85 @@ const aliases = Object.entries({ ...paths }).flatMap(([find, replacements]) => {
|
||||
|
||||
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
|
||||
json(), // imports json as ES6; doing so enables type-checking and module resolution
|
||||
]
|
||||
|
||||
const check = {
|
||||
input: 'src/lib/index.tsx',
|
||||
output: { file: 'dist/widgets.tsc' },
|
||||
output: { file: 'dist/widgets.tsc', inlineDynamicImports: true },
|
||||
external: isAsset,
|
||||
plugins: [...plugins, typescript({ tsconfig: TS_CONFIG })],
|
||||
plugins: [
|
||||
externals({ exclude: ['constants'], deps: true, peerDeps: true }), // marks builtins, dependencies, and peerDependencies external
|
||||
...plugins,
|
||||
typescript({ tsconfig: TS_CONFIG }),
|
||||
],
|
||||
onwarn: squelchTranspilationWarnings, // this pipeline is only for typechecking and generating definitions
|
||||
}
|
||||
|
||||
const type = {
|
||||
input: 'dist/dts/lib/index.d.ts',
|
||||
output: { file: 'dist/widgets.d.ts' },
|
||||
output: { file: 'dist/index.d.ts' },
|
||||
external: isAsset,
|
||||
plugins: [
|
||||
externals({ exclude: ['constants'], deps: true, peerDeps: true }),
|
||||
dts({ compilerOptions: { baseUrl: 'dist/dts' } }),
|
||||
process.env.ROLLUP_WATCH ? undefined : del({ hook: 'buildEnd', targets: ['dist/widgets.tsc', 'dist/dts'] }),
|
||||
],
|
||||
}
|
||||
|
||||
/**
|
||||
* This exports scheme works for nextjs and for CRA5.
|
||||
*
|
||||
* It will also work for CRA4 if you use direct imports:
|
||||
* instead of `import { SwapWidget } from '@uniswap/widgets'`,
|
||||
* `import { SwapWidget } from '@uniswap/widgets/dist/index.js'`.
|
||||
* I do not know why CRA4 does not seem to use exports for resolution.
|
||||
*
|
||||
* Note that chunks are enabled. This is so the tokenlist spec can be loaded async,
|
||||
* to improve first load time (due to ajv). Locales are also in separate chunks.
|
||||
*
|
||||
* Lastly, note that JSON and lingui are bundled into the library, as neither are fully
|
||||
* supported/compatible with ES Modules. Both _could_ be bundled only with esm, but this
|
||||
* yields a less complex pipeline.
|
||||
*/
|
||||
|
||||
const transpile = {
|
||||
input: 'src/lib/index.tsx',
|
||||
output: [
|
||||
{
|
||||
file: 'dist/widgets.js',
|
||||
format: 'cjs',
|
||||
dir: 'dist',
|
||||
format: 'esm',
|
||||
sourcemap: false,
|
||||
},
|
||||
{
|
||||
file: 'dist/widgets.esm.js',
|
||||
format: 'esm',
|
||||
dir: 'dist/cjs',
|
||||
entryFileNames: '[name].cjs',
|
||||
chunkFileNames: '[name]-[hash].cjs',
|
||||
format: 'cjs',
|
||||
sourcemap: false,
|
||||
},
|
||||
],
|
||||
plugins: [
|
||||
externals({
|
||||
exclude: [
|
||||
'constants',
|
||||
/@lingui\/(core|react)/, // @lingui incorrectly exports esm, so it must be bundled in
|
||||
/\.json$/, // esm does not support JSON loading, so it must be bundled in
|
||||
],
|
||||
deps: true,
|
||||
peerDeps: true,
|
||||
}),
|
||||
...plugins,
|
||||
|
||||
// Source code transformation
|
||||
url({ include: ASSET_EXTENSIONS.map((extname) => '**/*' + extname) }), // imports assets as data URIs
|
||||
url({ include: ASSET_EXTENSIONS.map((extname) => '**/*' + extname), limit: Infinity }), // imports assets as data URIs
|
||||
svgr({ exportType: 'named', svgo: false }), // imports svgs as React components
|
||||
sass({ insert: true }), // imports inlined sass styles
|
||||
sass({ output: 'dist/fonts.css' }), // generates fonts.css
|
||||
commonjs(), // transforms cjs dependencies into tree-shakeable ES modules
|
||||
|
||||
babel({
|
||||
@@ -104,16 +139,30 @@ const transpile = {
|
||||
'@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 config = [check, type, transpile]
|
||||
const locales = {
|
||||
input: 'src/locales/*.js',
|
||||
output: [
|
||||
{
|
||||
dir: 'dist',
|
||||
format: 'esm',
|
||||
sourcemap: false,
|
||||
},
|
||||
],
|
||||
plugins: [
|
||||
copy({
|
||||
copyOnce: true,
|
||||
targets: [{ src: 'src/locales/*.js', dest: 'dist/cjs/locales', rename: (name) => `${name}.cjs` }],
|
||||
}),
|
||||
commonjs(),
|
||||
multi(),
|
||||
],
|
||||
}
|
||||
|
||||
const config = [check, type, transpile, locales]
|
||||
export default config
|
||||
|
||||
function squelchTranspilationWarnings(warning: RollupWarning, warn: (warning: RollupWarning) => void) {
|
||||
|
||||
BIN
src/assets/images/ukraine.png
Normal file
|
After Width: | Height: | Size: 121 KiB |
@@ -1,3 +1,3 @@
|
||||
<svg width="100%" height="35" viewBox="800 0 300 200" xmlns="http://www.w3.org/2000/svg">
|
||||
<line x1="0" x2="2000" y1="100" y2="100" stroke="currentColor" stroke-width="20" stroke-linecap="round" stroke-dasharray="1, 45"/>
|
||||
<svg width="100%" height="35" viewBox="850 0 300 200" xmlns="http://www.w3.org/2000/svg">
|
||||
<line x1="0" x2="3000" y1="100" y2="100" stroke="currentColor" stroke-width="20" stroke-linecap="round" stroke-dasharray="1, 45"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 233 B After Width: | Height: | Size: 233 B |
@@ -1,16 +1,5 @@
|
||||
<svg width="170" height="168" viewBox="0 0 170 168" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0)">
|
||||
<path opacity="0.6" d="M85.05 168C132.022 168 170.1 130.105 170.1 83.3593C170.1 36.6135 0 36.6135 0 83.3593C0 130.105 38.0782 168 85.05 168Z" fill="#FF505F"/>
|
||||
<path opacity="0.6" d="M85.05 168C132.022 168 170.1 130.105 170.1 83.3593C170.1 36.6135 0 36.6135 0 83.3593C0 130.105 38.0782 168 85.05 168Z" fill="#FF0320"/>
|
||||
<path d="M85.05 0C132.022 0 170.1 37.8949 170.1 84.6407C170.1 131.386 0 131.386 0 84.6407C0 37.8949 38.0782 0 85.05 0Z" fill="white"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M144.665 64.0394L112.444 12.3742L89.0263 78.9477L144.665 64.0394Z" fill="#FF4E65"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M143.777 64.215L112.444 12.3742L165.349 58.4347L143.777 64.215Z" fill="white"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M144.551 63.613L142.479 124.467L88.912 78.5213L144.551 63.613Z" fill="#D0001A"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M143.663 63.7886L142.479 124.467L165.235 58.0083L143.663 63.7886Z" fill="#FF697B"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0">
|
||||
<rect width="170" height="168" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
<svg width="500" height="500" viewBox="0 0 500 500" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="250" cy="250" r="250" fill="#FF0420"/>
|
||||
<path d="M177.133 316.446C162.247 316.446 150.051 312.943 140.544 305.938C131.162 298.808 126.471 288.676 126.471 275.541C126.471 272.789 126.784 269.411 127.409 265.408C129.036 256.402 131.35 245.581 134.352 232.947C142.858 198.547 164.812 181.347 200.213 181.347C209.845 181.347 218.476 182.973 226.107 186.225C233.738 189.352 239.742 194.106 244.12 200.486C248.498 206.74 250.688 214.246 250.688 223.002C250.688 225.629 250.375 228.944 249.749 232.947C247.873 244.08 245.621 254.901 242.994 265.408C238.616 282.546 231.048 295.368 220.29 303.874C209.532 312.255 195.147 316.446 177.133 316.446ZM179.76 289.426C186.766 289.426 192.707 287.362 197.586 283.234C202.59 279.106 206.155 272.789 208.281 264.283C211.158 252.524 213.348 242.266 214.849 233.51C215.349 230.883 215.599 228.194 215.599 225.441C215.599 214.058 209.657 208.366 197.774 208.366C190.768 208.366 184.764 210.43 179.76 214.558C174.882 218.687 171.379 225.004 169.253 233.51C167.001 241.891 164.749 252.149 162.498 264.283C161.997 266.784 161.747 269.411 161.747 272.163C161.747 283.672 167.752 289.426 179.76 289.426Z" fill="white"/>
|
||||
<path d="M259.303 314.57C257.927 314.57 256.863 314.132 256.113 313.256C255.487 312.255 255.3 311.13 255.55 309.879L281.444 187.914C281.694 186.538 282.382 185.412 283.508 184.536C284.634 183.661 285.822 183.223 287.073 183.223H336.985C350.87 183.223 362.003 186.1 370.384 191.854C378.891 197.609 383.144 205.927 383.144 216.81C383.144 219.937 382.769 223.19 382.018 226.567C378.891 240.953 372.574 251.586 363.067 258.466C353.685 265.346 340.8 268.786 324.413 268.786H299.082L290.451 309.879C290.2 311.255 289.512 312.38 288.387 313.256C287.261 314.132 286.072 314.57 284.822 314.57H259.303ZM325.727 242.892C330.98 242.892 335.546 241.453 339.424 238.576C343.427 235.699 346.054 231.571 347.305 226.192C347.68 224.065 347.868 222.189 347.868 220.563C347.868 216.935 346.805 214.183 344.678 212.307C342.551 210.305 338.924 209.305 333.795 209.305H311.278L304.148 242.892H325.727Z" fill="white"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 2.1 KiB |
@@ -51,6 +51,16 @@ const FlyoutHeader = styled.div`
|
||||
font-weight: 400;
|
||||
`
|
||||
const FlyoutMenu = styled.div`
|
||||
position: absolute;
|
||||
top: 54px;
|
||||
width: 272px;
|
||||
z-index: 99;
|
||||
padding-top: 10px;
|
||||
@media screen and (min-width: ${MEDIA_WIDTHS.upToSmall}px) {
|
||||
top: 40px;
|
||||
}
|
||||
`
|
||||
const FlyoutMenuContents = styled.div`
|
||||
align-items: flex-start;
|
||||
background-color: ${({ theme }) => theme.bg0};
|
||||
box-shadow: 0px 0px 1px rgba(0, 0, 0, 0.01), 0px 4px 8px rgba(0, 0, 0, 0.04), 0px 16px 24px rgba(0, 0, 0, 0.04),
|
||||
@@ -61,16 +71,9 @@ const FlyoutMenu = styled.div`
|
||||
font-size: 16px;
|
||||
overflow: auto;
|
||||
padding: 16px;
|
||||
position: absolute;
|
||||
top: 64px;
|
||||
width: 272px;
|
||||
z-index: 99;
|
||||
& > *:not(:last-child) {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
@media screen and (min-width: ${MEDIA_WIDTHS.upToSmall}px) {
|
||||
top: 50px;
|
||||
}
|
||||
`
|
||||
const FlyoutRow = styled.div<{ active: boolean }>`
|
||||
align-items: center;
|
||||
@@ -305,21 +308,23 @@ export default function NetworkSelector() {
|
||||
}
|
||||
|
||||
return (
|
||||
<SelectorWrapper ref={node as any}>
|
||||
<SelectorControls onClick={toggle} interactive>
|
||||
<SelectorWrapper ref={node as any} onMouseEnter={toggle} onMouseLeave={toggle}>
|
||||
<SelectorControls interactive>
|
||||
<SelectorLogo interactive src={info.logoUrl} />
|
||||
<SelectorLabel>{info.label}</SelectorLabel>
|
||||
<StyledChevronDown />
|
||||
</SelectorControls>
|
||||
{open && (
|
||||
<FlyoutMenu onMouseLeave={toggle}>
|
||||
<FlyoutHeader>
|
||||
<Trans>Select a network</Trans>
|
||||
</FlyoutHeader>
|
||||
<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>
|
||||
<FlyoutMenuContents>
|
||||
<FlyoutHeader>
|
||||
<Trans>Select a network</Trans>
|
||||
</FlyoutHeader>
|
||||
<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} />
|
||||
</FlyoutMenuContents>
|
||||
</FlyoutMenu>
|
||||
)}
|
||||
</SelectorWrapper>
|
||||
|
||||
@@ -14,11 +14,12 @@ const MobilePopupWrapper = styled.div<{ height: string | number }>`
|
||||
max-width: 100%;
|
||||
height: ${({ height }) => height};
|
||||
margin: ${({ height }) => (height ? '0 auto;' : 0)};
|
||||
margin-bottom: ${({ height }) => (height ? '20px' : 0)}};
|
||||
margin-bottom: ${({ height }) => (height ? '20px' : 0)};
|
||||
|
||||
display: none;
|
||||
${({ theme }) => theme.mediaWidth.upToSmall`
|
||||
display: block;
|
||||
padding-top: 20px;
|
||||
`};
|
||||
`
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ import { PositionDetails } from 'types/position'
|
||||
import { formatTickPrice } from 'utils/formatTickPrice'
|
||||
import { unwrappedToken } from 'utils/unwrappedToken'
|
||||
|
||||
import { DAI, USDC, USDT, WBTC, WRAPPED_NATIVE_CURRENCY } from '../../constants/tokens'
|
||||
import { DAI, USDC_MAINNET, USDT, WBTC, WRAPPED_NATIVE_CURRENCY } from '../../constants/tokens'
|
||||
|
||||
const LinkRow = styled(Link)`
|
||||
align-items: center;
|
||||
@@ -145,7 +145,7 @@ export function getPriceOrderingFromPositionForUI(position?: Position): {
|
||||
const token1 = position.amount1.currency
|
||||
|
||||
// if token0 is a dollar-stable asset, set it as the quote token
|
||||
const stables = [DAI, USDC, USDT]
|
||||
const stables = [DAI, USDC_MAINNET, USDT]
|
||||
if (stables.some((stable) => stable.equals(token0))) {
|
||||
return {
|
||||
priceLower: position.token0PriceUpper.invert(),
|
||||
@@ -157,7 +157,7 @@ export function getPriceOrderingFromPositionForUI(position?: Position): {
|
||||
|
||||
// if token1 is an ETH-/BTC-stable asset, set it as the base token
|
||||
const bases = [...Object.values(WRAPPED_NATIVE_CURRENCY), WBTC]
|
||||
if (bases.some((base) => base.equals(token1))) {
|
||||
if (bases.some((base) => base && base.equals(token1))) {
|
||||
return {
|
||||
priceLower: position.token0PriceUpper.invert(),
|
||||
priceUpper: position.token0PriceLower.invert(),
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Protocol } from '@uniswap/router-sdk'
|
||||
import { Currency, Percent } from '@uniswap/sdk-core'
|
||||
import { FeeAmount } from '@uniswap/v3-sdk'
|
||||
import { DAI, USDC, WBTC } from 'constants/tokens'
|
||||
import { DAI, USDC_MAINNET, WBTC } from 'constants/tokens'
|
||||
import { render } from 'test-utils'
|
||||
|
||||
import RoutingDiagram, { RoutingDiagramEntry } from './RoutingDiagram'
|
||||
@@ -10,16 +10,16 @@ const percent = (strings: TemplateStringsArray) => new Percent(parseInt(strings[
|
||||
|
||||
const singleRoute: RoutingDiagramEntry = {
|
||||
percent: percent`100`,
|
||||
path: [[USDC, DAI, FeeAmount.LOW]],
|
||||
path: [[USDC_MAINNET, DAI, FeeAmount.LOW]],
|
||||
protocol: Protocol.V3,
|
||||
}
|
||||
|
||||
const multiRoute: RoutingDiagramEntry[] = [
|
||||
{ percent: percent`75`, path: [[USDC, DAI, FeeAmount.LOWEST]], protocol: Protocol.V2 },
|
||||
{ percent: percent`75`, path: [[USDC_MAINNET, DAI, FeeAmount.LOWEST]], protocol: Protocol.V2 },
|
||||
{
|
||||
percent: percent`25`,
|
||||
path: [
|
||||
[USDC, WBTC, FeeAmount.MEDIUM],
|
||||
[USDC_MAINNET, WBTC, FeeAmount.MEDIUM],
|
||||
[WBTC, DAI, FeeAmount.HIGH],
|
||||
],
|
||||
protocol: Protocol.V3,
|
||||
@@ -47,16 +47,16 @@ jest.mock('hooks/useTokenInfoFromActiveList', () => ({
|
||||
}))
|
||||
|
||||
it('renders when no routes are provided', () => {
|
||||
const { asFragment } = render(<RoutingDiagram currencyIn={DAI} currencyOut={USDC} routes={[]} />)
|
||||
const { asFragment } = render(<RoutingDiagram currencyIn={DAI} currencyOut={USDC_MAINNET} routes={[]} />)
|
||||
expect(asFragment()).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it('renders single route', () => {
|
||||
const { asFragment } = render(<RoutingDiagram currencyIn={USDC} currencyOut={DAI} routes={[singleRoute]} />)
|
||||
const { asFragment } = render(<RoutingDiagram currencyIn={USDC_MAINNET} currencyOut={DAI} routes={[singleRoute]} />)
|
||||
expect(asFragment()).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it('renders multi route', () => {
|
||||
const { asFragment } = render(<RoutingDiagram currencyIn={USDC} currencyOut={DAI} routes={multiRoute} />)
|
||||
const { asFragment } = render(<RoutingDiagram currencyIn={USDC_MAINNET} currencyOut={DAI} routes={multiRoute} />)
|
||||
expect(asFragment()).toMatchSnapshot()
|
||||
})
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import { Protocol } from '@uniswap/router-sdk'
|
||||
import { Currency, Percent } from '@uniswap/sdk-core'
|
||||
import { Currency } from '@uniswap/sdk-core'
|
||||
import { FeeAmount } from '@uniswap/v3-sdk'
|
||||
import Badge from 'components/Badge'
|
||||
import CurrencyLogo from 'components/CurrencyLogo'
|
||||
import DoubleCurrencyLogo from 'components/DoubleLogo'
|
||||
import Row, { AutoRow } from 'components/Row'
|
||||
import { useTokenInfoFromActiveList } from 'hooks/useTokenInfoFromActiveList'
|
||||
import { RoutingDiagramEntry } from 'lib/components/Swap/RoutingDiagram/utils'
|
||||
import { Box } from 'rebass'
|
||||
import styled from 'styled-components/macro'
|
||||
import { ThemedText, Z_INDEX } from 'theme'
|
||||
@@ -14,12 +14,6 @@ import { ThemedText, Z_INDEX } from 'theme'
|
||||
import { ReactComponent as DotLine } from '../../assets/svg/dot_line.svg'
|
||||
import { MouseoverTooltip } from '../Tooltip'
|
||||
|
||||
export interface RoutingDiagramEntry {
|
||||
percent: Percent
|
||||
path: [Currency, Currency, FeeAmount][]
|
||||
protocol: Protocol
|
||||
}
|
||||
|
||||
const Wrapper = styled(Box)`
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
|
||||
@@ -48,7 +48,7 @@ export function ImportToken(props: ImportProps) {
|
||||
<RowBetween>
|
||||
{onBack ? <ArrowLeft style={{ cursor: 'pointer' }} onClick={onBack} /> : <div />}
|
||||
<ThemedText.MediumHeader>
|
||||
<Plural value={tokens.length} one="Import token" other="Import tokens" />
|
||||
<Plural value={tokens.length} _1="Import token" other="Import tokens" />
|
||||
</ThemedText.MediumHeader>
|
||||
{onDismiss ? <CloseIcon onClick={onDismiss} /> : <div />}
|
||||
</RowBetween>
|
||||
|
||||
35
src/components/swap/AutoRouterIcon.tsx
Normal file
@@ -0,0 +1,35 @@
|
||||
import { useRef } from 'react'
|
||||
|
||||
let uniqueId = 0
|
||||
const getUniqueId = () => uniqueId++
|
||||
|
||||
export default function AutoRouterIcon({ className, id }: { className?: string; id?: string }) {
|
||||
const componentIdRef = useRef(id ?? getUniqueId())
|
||||
const componentId = `AutoRouterIconGradient${componentIdRef.current}`
|
||||
|
||||
return (
|
||||
<svg
|
||||
width="23"
|
||||
height="20"
|
||||
viewBox="0 0 23 20"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className={className}
|
||||
>
|
||||
<defs>
|
||||
<linearGradient id={componentId} x1="0" y1="0" x2="1" y2="0" gradientTransform="rotate(95)">
|
||||
<stop id="stop1" offset="0" stopColor="#2274E2" />
|
||||
<stop id="stop1" offset="0.5" stopColor="#2274E2" />
|
||||
<stop id="stop2" offset="1" stopColor="#3FB672" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<path
|
||||
d="M16 16C10 16 9 10 5 10M16 16C16 17.6569 17.3431 19 19 19C20.6569 19 22 17.6569 22 16C22 14.3431 20.6569 13 19 13C17.3431 13 16 14.3431 16 16ZM5 10C9 10 10 4 16 4M5 10H1.5M16 4C16 5.65685 17.3431 7 19 7C20.6569 7 22 5.65685 22 4C22 2.34315 20.6569 1 19 1C17.3431 1 16 2.34315 16 4Z"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
stroke={`url(#${componentId})`}
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
@@ -3,8 +3,8 @@ import useAutoRouterSupported from 'hooks/useAutoRouterSupported'
|
||||
import styled from 'styled-components/macro'
|
||||
import { ThemedText } from 'theme'
|
||||
|
||||
import { ReactComponent as AutoRouterIcon } from '../../assets/svg/auto_router.svg'
|
||||
import { ReactComponent as StaticRouterIcon } from '../../assets/svg/static_route.svg'
|
||||
import AutoRouterIcon from './AutoRouterIcon'
|
||||
|
||||
const StyledAutoRouterIcon = styled(AutoRouterIcon)`
|
||||
height: 16px;
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import { Trade } from '@uniswap/router-sdk'
|
||||
import { Currency, Percent, TradeType } from '@uniswap/sdk-core'
|
||||
import { Pair } from '@uniswap/v2-sdk'
|
||||
import { Currency, TradeType } from '@uniswap/sdk-core'
|
||||
import AnimatedDropdown from 'components/AnimatedDropdown'
|
||||
import { AutoColumn } from 'components/Column'
|
||||
import { LoadingRows } from 'components/Loader/styled'
|
||||
import RoutingDiagram, { RoutingDiagramEntry } from 'components/RoutingDiagram/RoutingDiagram'
|
||||
import RoutingDiagram from 'components/RoutingDiagram/RoutingDiagram'
|
||||
import { AutoRow, RowBetween } from 'components/Row'
|
||||
import { SUPPORTED_GAS_ESTIMATE_CHAIN_IDS } from 'constants/chains'
|
||||
import useActiveWeb3React from 'hooks/useActiveWeb3React'
|
||||
import useAutoRouterSupported from 'hooks/useAutoRouterSupported'
|
||||
import { getTokenPath } from 'lib/components/Swap/RoutingDiagram/utils'
|
||||
import { memo, useState } from 'react'
|
||||
import { Plus } from 'react-feather'
|
||||
import { InterfaceTrade } from 'state/routing/types'
|
||||
@@ -39,8 +38,6 @@ const OpenCloseIcon = styled(Plus)<{ open?: boolean }>`
|
||||
}
|
||||
`
|
||||
|
||||
const V2_DEFAULT_FEE_TIER = 3000
|
||||
|
||||
interface SwapRouteProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||
trade: InterfaceTrade<Currency, Currency, TradeType>
|
||||
syncing: boolean
|
||||
@@ -109,35 +106,3 @@ export default memo(function SwapRoute({ trade, syncing, fixedOpen = false, ...r
|
||||
</Wrapper>
|
||||
)
|
||||
})
|
||||
|
||||
function getTokenPath(trade: Trade<Currency, Currency, TradeType>): RoutingDiagramEntry[] {
|
||||
return trade.swaps.map(({ route: { path: tokenPath, pools, protocol }, inputAmount, outputAmount }) => {
|
||||
const portion =
|
||||
trade.tradeType === TradeType.EXACT_INPUT
|
||||
? inputAmount.divide(trade.inputAmount)
|
||||
: outputAmount.divide(trade.outputAmount)
|
||||
|
||||
const percent = new Percent(portion.numerator, portion.denominator)
|
||||
|
||||
const path: RoutingDiagramEntry['path'] = []
|
||||
for (let i = 0; i < pools.length; i++) {
|
||||
const nextPool = pools[i]
|
||||
const tokenIn = tokenPath[i]
|
||||
const tokenOut = tokenPath[i + 1]
|
||||
|
||||
const entry: RoutingDiagramEntry['path'][0] = [
|
||||
tokenIn,
|
||||
tokenOut,
|
||||
nextPool instanceof Pair ? V2_DEFAULT_FEE_TIER : nextPool.fee,
|
||||
]
|
||||
|
||||
path.push(entry)
|
||||
}
|
||||
|
||||
return {
|
||||
percent,
|
||||
path,
|
||||
protocol,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -39,7 +39,12 @@ export const ALL_SUPPORTED_CHAIN_IDS: SupportedChainId[] = Object.values(Support
|
||||
(id) => typeof id === 'number'
|
||||
) as SupportedChainId[]
|
||||
|
||||
export const SUPPORTED_GAS_ESTIMATE_CHAIN_IDS = [SupportedChainId.MAINNET, SupportedChainId.POLYGON]
|
||||
export const SUPPORTED_GAS_ESTIMATE_CHAIN_IDS = [
|
||||
SupportedChainId.MAINNET,
|
||||
SupportedChainId.POLYGON,
|
||||
SupportedChainId.OPTIMISM,
|
||||
SupportedChainId.ARBITRUM_ONE,
|
||||
]
|
||||
|
||||
/**
|
||||
* All the chain IDs that are running the Ethereum protocol.
|
||||
|
||||
@@ -12,6 +12,8 @@ export const COMMON_CONTRACT_NAMES: Record<number, { [address: string]: string }
|
||||
[TIMELOCK_ADDRESS[SupportedChainId.MAINNET]]: 'Timelock',
|
||||
[GOVERNANCE_ALPHA_V0_ADDRESSES[SupportedChainId.MAINNET]]: 'Governance (V0)',
|
||||
[GOVERNANCE_ALPHA_V1_ADDRESSES[SupportedChainId.MAINNET]]: 'Governance',
|
||||
'0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e': 'ENS Registry',
|
||||
'0x4976fb03C32e5B8cfe2b6cCB31c09Ba78EBaBa41': 'ENS Public Resolver',
|
||||
},
|
||||
}
|
||||
|
||||
@@ -21,3 +23,5 @@ export const DEFAULT_AVERAGE_BLOCK_TIME_IN_SECS = 13
|
||||
export const AVERAGE_BLOCK_TIME_IN_SECS: { [chainId: number]: number } = {
|
||||
1: DEFAULT_AVERAGE_BLOCK_TIME_IN_SECS,
|
||||
}
|
||||
|
||||
export const LATEST_GOVERNOR_INDEX = 2
|
||||
|
||||
@@ -18,8 +18,8 @@ import {
|
||||
sETH2,
|
||||
SWISE,
|
||||
TRIBE,
|
||||
USDC,
|
||||
USDC_ARBITRUM,
|
||||
USDC_MAINNET,
|
||||
USDC_OPTIMISM,
|
||||
USDC_POLYGON,
|
||||
USDT,
|
||||
@@ -44,13 +44,21 @@ type ChainCurrencyList = {
|
||||
}
|
||||
|
||||
const WRAPPED_NATIVE_CURRENCIES_ONLY: ChainTokenList = Object.fromEntries(
|
||||
Object.entries(WRAPPED_NATIVE_CURRENCY).map(([key, value]) => [key, [value]])
|
||||
Object.entries(WRAPPED_NATIVE_CURRENCY)
|
||||
.map(([key, value]) => [key, [value]])
|
||||
.filter(Boolean)
|
||||
)
|
||||
|
||||
// used to construct intermediary pairs for trading
|
||||
export const BASES_TO_CHECK_TRADES_AGAINST: ChainTokenList = {
|
||||
...WRAPPED_NATIVE_CURRENCIES_ONLY,
|
||||
[SupportedChainId.MAINNET]: [...WRAPPED_NATIVE_CURRENCIES_ONLY[SupportedChainId.MAINNET], DAI, USDC, USDT, WBTC],
|
||||
[SupportedChainId.MAINNET]: [
|
||||
...WRAPPED_NATIVE_CURRENCIES_ONLY[SupportedChainId.MAINNET],
|
||||
DAI,
|
||||
USDC_MAINNET,
|
||||
USDT,
|
||||
WBTC,
|
||||
],
|
||||
[SupportedChainId.OPTIMISM]: [
|
||||
...WRAPPED_NATIVE_CURRENCIES_ONLY[SupportedChainId.OPTIMISM],
|
||||
DAI_OPTIMISM,
|
||||
@@ -90,7 +98,7 @@ export const ADDITIONAL_BASES: { [chainId: number]: { [tokenAddress: string]: To
|
||||
*/
|
||||
export const CUSTOM_BASES: { [chainId: number]: { [tokenAddress: string]: Token[] } } = {
|
||||
[SupportedChainId.MAINNET]: {
|
||||
[AMPL.address]: [DAI, WRAPPED_NATIVE_CURRENCY[SupportedChainId.MAINNET]],
|
||||
[AMPL.address]: [DAI, WRAPPED_NATIVE_CURRENCY[SupportedChainId.MAINNET] as Token],
|
||||
},
|
||||
}
|
||||
|
||||
@@ -101,32 +109,38 @@ export const COMMON_BASES: ChainCurrencyList = {
|
||||
[SupportedChainId.MAINNET]: [
|
||||
nativeOnChain(SupportedChainId.MAINNET),
|
||||
DAI,
|
||||
USDC,
|
||||
USDC_MAINNET,
|
||||
USDT,
|
||||
WBTC,
|
||||
WRAPPED_NATIVE_CURRENCY[SupportedChainId.MAINNET],
|
||||
WRAPPED_NATIVE_CURRENCY[SupportedChainId.MAINNET] as Token,
|
||||
],
|
||||
[SupportedChainId.ROPSTEN]: [
|
||||
nativeOnChain(SupportedChainId.ROPSTEN),
|
||||
WRAPPED_NATIVE_CURRENCY[SupportedChainId.ROPSTEN],
|
||||
WRAPPED_NATIVE_CURRENCY[SupportedChainId.ROPSTEN] as Token,
|
||||
],
|
||||
[SupportedChainId.RINKEBY]: [
|
||||
nativeOnChain(SupportedChainId.RINKEBY),
|
||||
WRAPPED_NATIVE_CURRENCY[SupportedChainId.RINKEBY],
|
||||
WRAPPED_NATIVE_CURRENCY[SupportedChainId.RINKEBY] as Token,
|
||||
],
|
||||
[SupportedChainId.GOERLI]: [
|
||||
nativeOnChain(SupportedChainId.GOERLI),
|
||||
WRAPPED_NATIVE_CURRENCY[SupportedChainId.GOERLI] as Token,
|
||||
],
|
||||
[SupportedChainId.KOVAN]: [
|
||||
nativeOnChain(SupportedChainId.KOVAN),
|
||||
WRAPPED_NATIVE_CURRENCY[SupportedChainId.KOVAN] as Token,
|
||||
],
|
||||
[SupportedChainId.GOERLI]: [nativeOnChain(SupportedChainId.GOERLI), WRAPPED_NATIVE_CURRENCY[SupportedChainId.GOERLI]],
|
||||
[SupportedChainId.KOVAN]: [nativeOnChain(SupportedChainId.KOVAN), WRAPPED_NATIVE_CURRENCY[SupportedChainId.KOVAN]],
|
||||
[SupportedChainId.ARBITRUM_ONE]: [
|
||||
nativeOnChain(SupportedChainId.ARBITRUM_ONE),
|
||||
DAI_ARBITRUM_ONE,
|
||||
USDC_ARBITRUM,
|
||||
USDT_ARBITRUM_ONE,
|
||||
WBTC_ARBITRUM_ONE,
|
||||
WRAPPED_NATIVE_CURRENCY[SupportedChainId.ARBITRUM_ONE],
|
||||
WRAPPED_NATIVE_CURRENCY[SupportedChainId.ARBITRUM_ONE] as Token,
|
||||
],
|
||||
[SupportedChainId.ARBITRUM_RINKEBY]: [
|
||||
nativeOnChain(SupportedChainId.ARBITRUM_RINKEBY),
|
||||
WRAPPED_NATIVE_CURRENCY[SupportedChainId.ARBITRUM_RINKEBY],
|
||||
WRAPPED_NATIVE_CURRENCY[SupportedChainId.ARBITRUM_RINKEBY] as Token,
|
||||
],
|
||||
[SupportedChainId.OPTIMISM]: [
|
||||
nativeOnChain(SupportedChainId.OPTIMISM),
|
||||
@@ -146,7 +160,7 @@ export const COMMON_BASES: ChainCurrencyList = {
|
||||
],
|
||||
[SupportedChainId.POLYGON_MUMBAI]: [
|
||||
nativeOnChain(SupportedChainId.POLYGON_MUMBAI),
|
||||
WRAPPED_NATIVE_CURRENCY[SupportedChainId.POLYGON_MUMBAI],
|
||||
WRAPPED_NATIVE_CURRENCY[SupportedChainId.POLYGON_MUMBAI] as Token,
|
||||
WETH_POLYGON_MUMBAI,
|
||||
],
|
||||
}
|
||||
@@ -154,7 +168,13 @@ export const COMMON_BASES: ChainCurrencyList = {
|
||||
// used to construct the list of all pairs we consider by default in the frontend
|
||||
export const BASES_TO_TRACK_LIQUIDITY_FOR: ChainTokenList = {
|
||||
...WRAPPED_NATIVE_CURRENCIES_ONLY,
|
||||
[SupportedChainId.MAINNET]: [...WRAPPED_NATIVE_CURRENCIES_ONLY[SupportedChainId.MAINNET], DAI, USDC, USDT, WBTC],
|
||||
[SupportedChainId.MAINNET]: [
|
||||
...WRAPPED_NATIVE_CURRENCIES_ONLY[SupportedChainId.MAINNET],
|
||||
DAI,
|
||||
USDC_MAINNET,
|
||||
USDT,
|
||||
WBTC,
|
||||
],
|
||||
}
|
||||
export const PINNED_PAIRS: { readonly [chainId: number]: [Token, Token][] } = {
|
||||
[SupportedChainId.MAINNET]: [
|
||||
@@ -168,7 +188,7 @@ export const PINNED_PAIRS: { readonly [chainId: number]: [Token, Token][] } = {
|
||||
'Compound USD Coin'
|
||||
),
|
||||
],
|
||||
[USDC, USDT],
|
||||
[USDC_MAINNET, USDT],
|
||||
[DAI, USDT],
|
||||
],
|
||||
}
|
||||
|
||||
@@ -1101,6 +1101,20 @@
|
||||
"name": "BMEX",
|
||||
"symbol": "BMEX",
|
||||
"decimals": 18
|
||||
},
|
||||
{
|
||||
"chainId": 1,
|
||||
"address": "0x322A46E88fa3C78F9c9E3DBb0254b61664a06109",
|
||||
"name": "Ukraine DAO",
|
||||
"symbol": "Ukraine",
|
||||
"decimals": 18
|
||||
},
|
||||
{
|
||||
"chainId": 1,
|
||||
"address": "0x8290D7a64F25e6b5002d98367E8367c1b532b534",
|
||||
"name": "oneUNI",
|
||||
"symbol": "oneUNI",
|
||||
"decimals": 18
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,8 +1,24 @@
|
||||
import { Currency, Ether, NativeCurrency, Token, WETH9 } from '@uniswap/sdk-core'
|
||||
import {
|
||||
USDC_ARBITRUM,
|
||||
USDC_ARBITRUM_RINKEBY,
|
||||
USDC_GÖRLI,
|
||||
USDC_KOVAN,
|
||||
USDC_MAINNET,
|
||||
USDC_OPTIMISM,
|
||||
USDC_OPTIMISTIC_KOVAN,
|
||||
USDC_POLYGON,
|
||||
USDC_POLYGON_MUMBAI,
|
||||
USDC_RINKEBY,
|
||||
USDC_ROPSTEN,
|
||||
} from '@uniswap/smart-order-router'
|
||||
import invariant from 'tiny-invariant'
|
||||
|
||||
import { UNI_ADDRESS } from './addresses'
|
||||
import { SupportedChainId } from './chains'
|
||||
|
||||
export { USDC_ARBITRUM, USDC_MAINNET, USDC_OPTIMISM, USDC_POLYGON }
|
||||
|
||||
export const AMPL = new Token(
|
||||
SupportedChainId.MAINNET,
|
||||
'0xD46bA6D942050d489DBd938a2C909A5d5039A161',
|
||||
@@ -31,27 +47,19 @@ export const DAI_OPTIMISM = new Token(
|
||||
'DAI',
|
||||
'Dai stable coin'
|
||||
)
|
||||
export const USDC = new Token(
|
||||
SupportedChainId.MAINNET,
|
||||
'0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48',
|
||||
6,
|
||||
'USDC',
|
||||
'USD//C'
|
||||
)
|
||||
export const USDC_ARBITRUM = new Token(
|
||||
SupportedChainId.ARBITRUM_ONE,
|
||||
'0xFF970A61A04b1cA14834A43f5dE4533eBDDB5CC8',
|
||||
6,
|
||||
'USDC',
|
||||
'USD//C'
|
||||
)
|
||||
export const USDC_POLYGON = new Token(
|
||||
SupportedChainId.POLYGON,
|
||||
'0x2791bca1f2de4661ed88a30c99a7a9449aa84174',
|
||||
6,
|
||||
'USDC',
|
||||
'USD//C'
|
||||
)
|
||||
export const USDC: { [chainId in SupportedChainId]: Token } = {
|
||||
[SupportedChainId.MAINNET]: USDC_MAINNET,
|
||||
[SupportedChainId.ARBITRUM_ONE]: USDC_ARBITRUM,
|
||||
[SupportedChainId.OPTIMISM]: USDC_OPTIMISM,
|
||||
[SupportedChainId.ARBITRUM_RINKEBY]: USDC_ARBITRUM_RINKEBY,
|
||||
[SupportedChainId.OPTIMISTIC_KOVAN]: USDC_OPTIMISTIC_KOVAN,
|
||||
[SupportedChainId.POLYGON]: USDC_POLYGON,
|
||||
[SupportedChainId.POLYGON_MUMBAI]: USDC_POLYGON_MUMBAI,
|
||||
[SupportedChainId.GOERLI]: USDC_GÖRLI,
|
||||
[SupportedChainId.RINKEBY]: USDC_RINKEBY,
|
||||
[SupportedChainId.KOVAN]: USDC_KOVAN,
|
||||
[SupportedChainId.ROPSTEN]: USDC_ROPSTEN,
|
||||
}
|
||||
export const DAI_POLYGON = new Token(
|
||||
SupportedChainId.POLYGON,
|
||||
'0x8f3Cf7ad23Cd3CaDbD9735AFf958023239c6A063',
|
||||
@@ -73,13 +81,6 @@ export const WBTC_POLYGON = new Token(
|
||||
'WBTC',
|
||||
'Wrapped BTC'
|
||||
)
|
||||
export const USDC_OPTIMISM = new Token(
|
||||
SupportedChainId.OPTIMISM,
|
||||
'0x7F5c764cBc14f9669B88837ca1490cCa17c31607',
|
||||
6,
|
||||
'USDC',
|
||||
'USD//C'
|
||||
)
|
||||
export const USDT = new Token(
|
||||
SupportedChainId.MAINNET,
|
||||
'0xdAC17F958D2ee523a2206206994597C13D831ec7',
|
||||
@@ -208,8 +209,8 @@ export const UNI: { [chainId: number]: Token } = {
|
||||
[SupportedChainId.KOVAN]: new Token(SupportedChainId.KOVAN, UNI_ADDRESS[42], 18, 'UNI', 'Uniswap'),
|
||||
}
|
||||
|
||||
export const WRAPPED_NATIVE_CURRENCY: { [chainId: number]: Token } = {
|
||||
...WETH9,
|
||||
export const WRAPPED_NATIVE_CURRENCY: { [chainId: number]: Token | undefined } = {
|
||||
...(WETH9 as Record<SupportedChainId, Token>),
|
||||
[SupportedChainId.OPTIMISM]: new Token(
|
||||
SupportedChainId.OPTIMISM,
|
||||
'0x4200000000000000000000000000000000000006',
|
||||
@@ -265,7 +266,9 @@ class MaticNativeCurrency extends NativeCurrency {
|
||||
|
||||
get wrapped(): Token {
|
||||
if (!isMatic(this.chainId)) throw new Error('Not matic')
|
||||
return WRAPPED_NATIVE_CURRENCY[this.chainId]
|
||||
const wrapped = WRAPPED_NATIVE_CURRENCY[this.chainId]
|
||||
invariant(wrapped instanceof Token)
|
||||
return wrapped
|
||||
}
|
||||
|
||||
public constructor(chainId: number) {
|
||||
@@ -276,7 +279,8 @@ class MaticNativeCurrency extends NativeCurrency {
|
||||
|
||||
export class ExtendedEther extends Ether {
|
||||
public get wrapped(): Token {
|
||||
if (this.chainId in WRAPPED_NATIVE_CURRENCY) return WRAPPED_NATIVE_CURRENCY[this.chainId]
|
||||
const wrapped = WRAPPED_NATIVE_CURRENCY[this.chainId]
|
||||
if (wrapped) return wrapped
|
||||
throw new Error('Unsupported chain ID')
|
||||
}
|
||||
|
||||
@@ -296,3 +300,19 @@ export function nativeOnChain(chainId: number): NativeCurrency {
|
||||
: ExtendedEther.onChain(chainId))
|
||||
)
|
||||
}
|
||||
|
||||
export const TOKEN_SHORTHANDS: { [shorthand: string]: { [chainId in SupportedChainId]?: string } } = {
|
||||
USDC: {
|
||||
[SupportedChainId.MAINNET]: USDC_MAINNET.address,
|
||||
[SupportedChainId.ARBITRUM_ONE]: USDC_ARBITRUM.address,
|
||||
[SupportedChainId.OPTIMISM]: USDC_OPTIMISM.address,
|
||||
[SupportedChainId.ARBITRUM_RINKEBY]: USDC_ARBITRUM_RINKEBY.address,
|
||||
[SupportedChainId.OPTIMISTIC_KOVAN]: USDC_OPTIMISTIC_KOVAN.address,
|
||||
[SupportedChainId.POLYGON]: USDC_POLYGON.address,
|
||||
[SupportedChainId.POLYGON_MUMBAI]: USDC_POLYGON_MUMBAI.address,
|
||||
[SupportedChainId.GOERLI]: USDC_GÖRLI.address,
|
||||
[SupportedChainId.RINKEBY]: USDC_RINKEBY.address,
|
||||
[SupportedChainId.KOVAN]: USDC_KOVAN.address,
|
||||
[SupportedChainId.ROPSTEN]: USDC_ROPSTEN.address,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ 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 { useCurrencyFromMap, useTokenFromMap } from 'lib/hooks/useCurrency'
|
||||
import { useCurrencyFromMap, useTokenFromMapOrNetwork } from 'lib/hooks/useCurrency'
|
||||
import { getTokenFilter } from 'lib/hooks/useTokenList/filtering'
|
||||
import { useMemo } from 'react'
|
||||
|
||||
@@ -159,7 +159,7 @@ export function useIsUserAddedToken(currency: Currency | undefined | null): bool
|
||||
// otherwise returns the token
|
||||
export function useToken(tokenAddress?: string | null): Token | null | undefined {
|
||||
const tokens = useAllTokens()
|
||||
return useTokenFromMap(tokens, tokenAddress)
|
||||
return useTokenFromMapOrNetwork(tokens, tokenAddress)
|
||||
}
|
||||
|
||||
export function useCurrency(currencyId?: string | null): Currency | null | undefined {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { renderHook } from '@testing-library/react-hooks'
|
||||
import { CurrencyAmount, TradeType } from '@uniswap/sdk-core'
|
||||
import { DAI, USDC } from 'constants/tokens'
|
||||
import { DAI, USDC_MAINNET } from 'constants/tokens'
|
||||
import { TradeState } from 'state/routing/types'
|
||||
|
||||
import { useRoutingAPITrade } from '../state/routing/useRoutingAPITrade'
|
||||
@@ -10,7 +10,7 @@ import { useClientSideV3Trade } from './useClientSideV3Trade'
|
||||
import useDebounce from './useDebounce'
|
||||
import useIsWindowVisible from './useIsWindowVisible'
|
||||
|
||||
const USDCAmount = CurrencyAmount.fromRawAmount(USDC, '10000')
|
||||
const USDCAmount = CurrencyAmount.fromRawAmount(USDC_MAINNET, '10000')
|
||||
const DAIAmount = CurrencyAmount.fromRawAmount(DAI, '10000')
|
||||
|
||||
jest.mock('./useAutoRouterSupported')
|
||||
@@ -126,10 +126,10 @@ describe('#useBestV3Trade ExactOut', () => {
|
||||
expectRouterMock(TradeState.INVALID)
|
||||
expectClientSideMock(TradeState.VALID)
|
||||
|
||||
const { result } = renderHook(() => useBestTrade(TradeType.EXACT_OUTPUT, DAIAmount, USDC))
|
||||
const { result } = renderHook(() => useBestTrade(TradeType.EXACT_OUTPUT, DAIAmount, USDC_MAINNET))
|
||||
|
||||
expect(mockUseRoutingAPITrade).toHaveBeenCalledWith(TradeType.EXACT_OUTPUT, undefined, USDC)
|
||||
expect(mockUseClientSideV3Trade).toHaveBeenCalledWith(TradeType.EXACT_OUTPUT, DAIAmount, USDC)
|
||||
expect(mockUseRoutingAPITrade).toHaveBeenCalledWith(TradeType.EXACT_OUTPUT, undefined, USDC_MAINNET)
|
||||
expect(mockUseClientSideV3Trade).toHaveBeenCalledWith(TradeType.EXACT_OUTPUT, DAIAmount, USDC_MAINNET)
|
||||
expect(result.current).toEqual({ state: TradeState.VALID, trade: undefined })
|
||||
})
|
||||
|
||||
@@ -138,17 +138,17 @@ describe('#useBestV3Trade ExactOut', () => {
|
||||
expectRouterMock(TradeState.NO_ROUTE_FOUND)
|
||||
expectClientSideMock(TradeState.VALID)
|
||||
|
||||
const { result } = renderHook(() => useBestTrade(TradeType.EXACT_OUTPUT, DAIAmount, USDC))
|
||||
const { result } = renderHook(() => useBestTrade(TradeType.EXACT_OUTPUT, DAIAmount, USDC_MAINNET))
|
||||
|
||||
expect(mockUseRoutingAPITrade).toHaveBeenCalledWith(TradeType.EXACT_OUTPUT, undefined, USDC)
|
||||
expect(mockUseClientSideV3Trade).toHaveBeenCalledWith(TradeType.EXACT_OUTPUT, DAIAmount, USDC)
|
||||
expect(mockUseRoutingAPITrade).toHaveBeenCalledWith(TradeType.EXACT_OUTPUT, undefined, USDC_MAINNET)
|
||||
expect(mockUseClientSideV3Trade).toHaveBeenCalledWith(TradeType.EXACT_OUTPUT, DAIAmount, USDC_MAINNET)
|
||||
expect(result.current).toEqual({ state: TradeState.VALID, trade: undefined })
|
||||
})
|
||||
describe('when routing api is in non-error state', () => {
|
||||
it('does not compute client side v3 trade if routing api is LOADING', () => {
|
||||
expectRouterMock(TradeState.LOADING)
|
||||
|
||||
const { result } = renderHook(() => useBestTrade(TradeType.EXACT_OUTPUT, DAIAmount, USDC))
|
||||
const { result } = renderHook(() => useBestTrade(TradeType.EXACT_OUTPUT, DAIAmount, USDC_MAINNET))
|
||||
|
||||
expect(mockUseClientSideV3Trade).toHaveBeenCalledWith(TradeType.EXACT_OUTPUT, undefined, undefined)
|
||||
expect(result.current).toEqual({ state: TradeState.LOADING, trade: undefined })
|
||||
@@ -157,7 +157,7 @@ describe('#useBestV3Trade ExactOut', () => {
|
||||
it('does not compute client side v3 trade if routing api is VALID', () => {
|
||||
expectRouterMock(TradeState.VALID)
|
||||
|
||||
const { result } = renderHook(() => useBestTrade(TradeType.EXACT_OUTPUT, DAIAmount, USDC))
|
||||
const { result } = renderHook(() => useBestTrade(TradeType.EXACT_OUTPUT, DAIAmount, USDC_MAINNET))
|
||||
|
||||
expect(mockUseClientSideV3Trade).toHaveBeenCalledWith(TradeType.EXACT_OUTPUT, undefined, undefined)
|
||||
expect(result.current).toEqual({ state: TradeState.VALID, trade: undefined })
|
||||
@@ -166,7 +166,7 @@ describe('#useBestV3Trade ExactOut', () => {
|
||||
it('does not compute client side v3 trade if routing api is SYNCING', () => {
|
||||
expectRouterMock(TradeState.SYNCING)
|
||||
|
||||
const { result } = renderHook(() => useBestTrade(TradeType.EXACT_OUTPUT, DAIAmount, USDC))
|
||||
const { result } = renderHook(() => useBestTrade(TradeType.EXACT_OUTPUT, DAIAmount, USDC_MAINNET))
|
||||
|
||||
expect(mockUseClientSideV3Trade).toHaveBeenCalledWith(TradeType.EXACT_OUTPUT, undefined, undefined)
|
||||
expect(result.current).toEqual({ state: TradeState.SYNCING, trade: undefined })
|
||||
@@ -178,7 +178,7 @@ describe('#useBestV3Trade ExactOut', () => {
|
||||
expectRouterMock(TradeState.INVALID)
|
||||
expectClientSideMock(TradeState.VALID)
|
||||
|
||||
renderHook(() => useBestTrade(TradeType.EXACT_OUTPUT, DAIAmount, USDC))
|
||||
renderHook(() => useBestTrade(TradeType.EXACT_OUTPUT, DAIAmount, USDC_MAINNET))
|
||||
|
||||
expect(mockUseClientSideV3Trade).toHaveBeenCalledWith(TradeType.EXACT_OUTPUT, undefined, undefined)
|
||||
})
|
||||
@@ -187,9 +187,9 @@ describe('#useBestV3Trade ExactOut', () => {
|
||||
expectRouterMock(TradeState.NO_ROUTE_FOUND)
|
||||
expectClientSideMock(TradeState.VALID)
|
||||
|
||||
const { result } = renderHook(() => useBestTrade(TradeType.EXACT_OUTPUT, DAIAmount, USDC))
|
||||
const { result } = renderHook(() => useBestTrade(TradeType.EXACT_OUTPUT, DAIAmount, USDC_MAINNET))
|
||||
|
||||
expect(mockUseClientSideV3Trade).toHaveBeenCalledWith(TradeType.EXACT_OUTPUT, DAIAmount, USDC)
|
||||
expect(mockUseClientSideV3Trade).toHaveBeenCalledWith(TradeType.EXACT_OUTPUT, DAIAmount, USDC_MAINNET)
|
||||
expect(result.current).toEqual({ state: TradeState.VALID, trade: undefined })
|
||||
})
|
||||
})
|
||||
|
||||
@@ -36,21 +36,8 @@ export function useBestTrade(
|
||||
debouncedOtherCurrency
|
||||
)
|
||||
|
||||
const isLoading = amountSpecified !== undefined && debouncedAmount === undefined
|
||||
|
||||
// consider trade debouncing when inputs/outputs do not match
|
||||
const debouncing =
|
||||
routingAPITrade.trade &&
|
||||
amountSpecified &&
|
||||
(tradeType === TradeType.EXACT_INPUT
|
||||
? !routingAPITrade.trade.inputAmount.equalTo(amountSpecified) ||
|
||||
!amountSpecified.currency.equals(routingAPITrade.trade.inputAmount.currency) ||
|
||||
!debouncedOtherCurrency?.equals(routingAPITrade.trade.outputAmount.currency)
|
||||
: !routingAPITrade.trade.outputAmount.equalTo(amountSpecified) ||
|
||||
!amountSpecified.currency.equals(routingAPITrade.trade.outputAmount.currency) ||
|
||||
!debouncedOtherCurrency?.equals(routingAPITrade.trade.inputAmount.currency))
|
||||
|
||||
const useFallback = !autoRouterSupported || (!debouncing && routingAPITrade.state === TradeState.NO_ROUTE_FOUND)
|
||||
const isLoading = routingAPITrade.state === TradeState.LOADING
|
||||
const useFallback = !autoRouterSupported || routingAPITrade.state === TradeState.NO_ROUTE_FOUND
|
||||
|
||||
// only use client side router if routing api trade failed or is not supported
|
||||
const bestV3Trade = useClientSideV3Trade(
|
||||
@@ -63,9 +50,8 @@ export function useBestTrade(
|
||||
return useMemo(
|
||||
() => ({
|
||||
...(useFallback ? bestV3Trade : routingAPITrade),
|
||||
...(debouncing ? { state: TradeState.SYNCING } : {}),
|
||||
...(isLoading ? { state: TradeState.LOADING } : {}),
|
||||
}),
|
||||
[bestV3Trade, debouncing, isLoading, routingAPITrade, useFallback]
|
||||
[bestV3Trade, isLoading, routingAPITrade, useFallback]
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Token } from '@uniswap/sdk-core'
|
||||
import { SupportedChainId } from 'constants/chains'
|
||||
import uriToHttp from 'lib/utils/uriToHttp'
|
||||
import Vibrant from 'node-vibrant/lib/bundle'
|
||||
import Vibrant from 'node-vibrant/lib/bundle.js'
|
||||
import { shade } from 'polished'
|
||||
import { useLayoutEffect, useState } from 'react'
|
||||
import { WrappedTokenInfo } from 'state/lists/wrappedTokenInfo'
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
import { BigNumber } from '@ethersproject/bignumber'
|
||||
import { useSingleCallResult } from 'lib/hooks/multicall'
|
||||
import { useMemo } from 'react'
|
||||
|
||||
import { useInterfaceMulticall } from './useContract'
|
||||
|
||||
// gets the current timestamp from the blockchain
|
||||
export default function useCurrentBlockTimestamp(): BigNumber | undefined {
|
||||
const multicall = useInterfaceMulticall()
|
||||
return useSingleCallResult(multicall, 'getCurrentBlockTimestamp')?.result?.[0]
|
||||
const resultStr: string | undefined = useSingleCallResult(
|
||||
multicall,
|
||||
'getCurrentBlockTimestamp'
|
||||
)?.result?.[0]?.toString()
|
||||
return useMemo(() => (typeof resultStr === 'string' ? BigNumber.from(resultStr) : undefined), [resultStr])
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ import { useSingleCallResult } from 'lib/hooks/multicall'
|
||||
import { useMemo, useState } from 'react'
|
||||
|
||||
import { SWAP_ROUTER_ADDRESSES, V3_ROUTER_ADDRESS } from '../constants/addresses'
|
||||
import { DAI, UNI, USDC } from '../constants/tokens'
|
||||
import { DAI, UNI, USDC_MAINNET } from '../constants/tokens'
|
||||
import { useEIP2612Contract } from './useContract'
|
||||
import useIsArgentWallet from './useIsArgentWallet'
|
||||
|
||||
@@ -36,7 +36,7 @@ const PERMITTABLE_TOKENS: {
|
||||
}
|
||||
} = {
|
||||
1: {
|
||||
[USDC.address]: { type: PermitType.AMOUNT, name: 'USD Coin', version: '2' },
|
||||
[USDC_MAINNET.address]: { type: PermitType.AMOUNT, name: 'USD Coin', version: '2' },
|
||||
[DAI.address]: { type: PermitType.ALLOWED, name: 'Dai Stablecoin', version: '1' },
|
||||
[UNI[1].address]: { type: PermitType.AMOUNT, name: 'Uniswap' },
|
||||
},
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import JSBI from 'jsbi'
|
||||
import { useSingleCallResult } from 'lib/hooks/multicall'
|
||||
import { useMemo } from 'react'
|
||||
|
||||
import { useContract } from './useContract'
|
||||
import useENSAddress from './useENSAddress'
|
||||
@@ -22,5 +23,5 @@ export default function useGasPrice(): JSBI | undefined {
|
||||
const contract = useContract(address ?? undefined, CHAIN_DATA_ABI, false)
|
||||
|
||||
const resultStr = useSingleCallResult(contract, 'latestAnswer').result?.[0]?.toString()
|
||||
return typeof resultStr === 'string' ? JSBI.BigInt(resultStr) : undefined
|
||||
return useMemo(() => (typeof resultStr === 'string' ? JSBI.BigInt(resultStr) : undefined), [resultStr])
|
||||
}
|
||||
|
||||
@@ -9,5 +9,5 @@ export default function useIsArgentWallet(): boolean {
|
||||
const argentWalletDetector = useArgentWalletDetectorContract()
|
||||
const inputs = useMemo(() => [account ?? undefined], [account])
|
||||
const call = useSingleCallResult(argentWalletDetector, 'isArgentWallet', inputs, NEVER_RELOAD)
|
||||
return call?.result?.[0] ?? false
|
||||
return Boolean(call?.result?.[0])
|
||||
}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
|
||||
const VISIBILITY_STATE_SUPPORTED = 'visibilityState' in document
|
||||
function isVisibilityStateSupported() {
|
||||
return 'visibilityState' in document
|
||||
}
|
||||
|
||||
function isWindowVisible() {
|
||||
return !VISIBILITY_STATE_SUPPORTED || document.visibilityState !== 'hidden'
|
||||
return !isVisibilityStateSupported() || document.visibilityState !== 'hidden'
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -16,7 +18,7 @@ export default function useIsWindowVisible(): boolean {
|
||||
}, [setFocused])
|
||||
|
||||
useEffect(() => {
|
||||
if (!VISIBILITY_STATE_SUPPORTED) return undefined
|
||||
if (!isVisibilityStateSupported()) return undefined
|
||||
|
||||
document.addEventListener('visibilitychange', listener)
|
||||
return () => {
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { Interface } from '@ethersproject/abi'
|
||||
import { Currency, Token } from '@uniswap/sdk-core'
|
||||
import { BigintIsh, Currency, Token } from '@uniswap/sdk-core'
|
||||
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'
|
||||
import JSBI from 'jsbi'
|
||||
import { useMultipleContractSingleData } from 'lib/hooks/multicall'
|
||||
import { useMemo } from 'react'
|
||||
|
||||
@@ -14,6 +15,69 @@ const { abi: IUniswapV3PoolStateABI } = IUniswapV3PoolStateJson
|
||||
|
||||
const POOL_STATE_INTERFACE = new Interface(IUniswapV3PoolStateABI) as IUniswapV3PoolStateInterface
|
||||
|
||||
// Classes are expensive to instantiate, so this caches the recently instantiated pools.
|
||||
// This avoids re-instantiating pools as the other pools in the same request are loaded.
|
||||
class PoolCache {
|
||||
// Evict after 128 entries. Empirically, a swap uses 64 entries.
|
||||
private static MAX_ENTRIES = 128
|
||||
|
||||
// These are FIFOs, using unshift/pop. This makes recent entries faster to find.
|
||||
private static pools: Pool[] = []
|
||||
private static addresses: { key: string; address: string }[] = []
|
||||
|
||||
static getPoolAddress(factoryAddress: string, tokenA: Token, tokenB: Token, fee: FeeAmount): string {
|
||||
if (this.addresses.length > this.MAX_ENTRIES) {
|
||||
this.addresses = this.addresses.slice(0, this.MAX_ENTRIES / 2)
|
||||
}
|
||||
|
||||
const { address: addressA } = tokenA
|
||||
const { address: addressB } = tokenB
|
||||
const key = `${factoryAddress}:${addressA}:${addressB}:${fee.toString()}`
|
||||
const found = this.addresses.find((address) => address.key === key)
|
||||
if (found) return found.address
|
||||
|
||||
const address = {
|
||||
key,
|
||||
address: computePoolAddress({
|
||||
factoryAddress,
|
||||
tokenA,
|
||||
tokenB,
|
||||
fee,
|
||||
}),
|
||||
}
|
||||
this.addresses.unshift(address)
|
||||
return address.address
|
||||
}
|
||||
|
||||
static getPool(
|
||||
tokenA: Token,
|
||||
tokenB: Token,
|
||||
fee: FeeAmount,
|
||||
sqrtPriceX96: BigintIsh,
|
||||
liquidity: BigintIsh,
|
||||
tick: number
|
||||
): Pool {
|
||||
if (this.pools.length > this.MAX_ENTRIES) {
|
||||
this.pools = this.pools.slice(0, this.MAX_ENTRIES / 2)
|
||||
}
|
||||
|
||||
const found = this.pools.find(
|
||||
(pool) =>
|
||||
pool.token0 === tokenA &&
|
||||
pool.token1 === tokenB &&
|
||||
pool.fee === fee &&
|
||||
JSBI.EQ(pool.sqrtRatioX96, sqrtPriceX96) &&
|
||||
JSBI.EQ(pool.liquidity, liquidity) &&
|
||||
pool.tickCurrent === tick
|
||||
)
|
||||
if (found) return found
|
||||
|
||||
const pool = new Pool(tokenA, tokenB, fee, sqrtPriceX96, liquidity, tick)
|
||||
this.pools.unshift(pool)
|
||||
return pool
|
||||
}
|
||||
}
|
||||
|
||||
export enum PoolState {
|
||||
LOADING,
|
||||
NOT_EXISTS,
|
||||
@@ -26,58 +90,57 @@ export function usePools(
|
||||
): [PoolState, Pool | null][] {
|
||||
const { chainId } = useActiveWeb3React()
|
||||
|
||||
const transformed: ([Token, Token, FeeAmount] | null)[] = useMemo(() => {
|
||||
return poolKeys.map(([currencyA, currencyB, feeAmount]) => {
|
||||
if (!chainId || !currencyA || !currencyB || !feeAmount) return null
|
||||
const poolTokens: ([Token, Token, FeeAmount] | undefined)[] = useMemo(() => {
|
||||
if (!chainId) return new Array(poolKeys.length)
|
||||
|
||||
const tokenA = currencyA?.wrapped
|
||||
const tokenB = currencyB?.wrapped
|
||||
if (!tokenA || !tokenB || tokenA.equals(tokenB)) return null
|
||||
const [token0, token1] = tokenA.sortsBefore(tokenB) ? [tokenA, tokenB] : [tokenB, tokenA]
|
||||
return [token0, token1, feeAmount]
|
||||
return poolKeys.map(([currencyA, currencyB, feeAmount]) => {
|
||||
if (currencyA && currencyB && feeAmount) {
|
||||
const tokenA = currencyA.wrapped
|
||||
const tokenB = currencyB.wrapped
|
||||
if (tokenA.equals(tokenB)) return undefined
|
||||
|
||||
return tokenA.sortsBefore(tokenB) ? [tokenA, tokenB, feeAmount] : [tokenB, tokenA, feeAmount]
|
||||
}
|
||||
return undefined
|
||||
})
|
||||
}, [chainId, poolKeys])
|
||||
|
||||
const poolAddresses: (string | undefined)[] = useMemo(() => {
|
||||
const v3CoreFactoryAddress = chainId && V3_CORE_FACTORY_ADDRESSES[chainId]
|
||||
if (!v3CoreFactoryAddress) return new Array(poolTokens.length)
|
||||
|
||||
return transformed.map((value) => {
|
||||
if (!v3CoreFactoryAddress || !value) return undefined
|
||||
return computePoolAddress({
|
||||
factoryAddress: v3CoreFactoryAddress,
|
||||
tokenA: value[0],
|
||||
tokenB: value[1],
|
||||
fee: value[2],
|
||||
})
|
||||
})
|
||||
}, [chainId, transformed])
|
||||
return poolTokens.map((value) => value && PoolCache.getPoolAddress(v3CoreFactoryAddress, ...value))
|
||||
}, [chainId, poolTokens])
|
||||
|
||||
const slot0s = useMultipleContractSingleData(poolAddresses, POOL_STATE_INTERFACE, 'slot0')
|
||||
const liquidities = useMultipleContractSingleData(poolAddresses, POOL_STATE_INTERFACE, 'liquidity')
|
||||
|
||||
return useMemo(() => {
|
||||
return poolKeys.map((_key, index) => {
|
||||
const [token0, token1, fee] = transformed[index] ?? []
|
||||
if (!token0 || !token1 || !fee) return [PoolState.INVALID, null]
|
||||
const tokens = poolTokens[index]
|
||||
if (!tokens) return [PoolState.INVALID, null]
|
||||
const [token0, token1, fee] = tokens
|
||||
|
||||
if (!slot0s[index]) return [PoolState.INVALID, null]
|
||||
const { result: slot0, loading: slot0Loading, valid: slot0Valid } = slot0s[index]
|
||||
|
||||
if (!liquidities[index]) return [PoolState.INVALID, null]
|
||||
const { result: liquidity, loading: liquidityLoading, valid: liquidityValid } = liquidities[index]
|
||||
|
||||
if (!slot0Valid || !liquidityValid) return [PoolState.INVALID, null]
|
||||
if (!tokens || !slot0Valid || !liquidityValid) return [PoolState.INVALID, null]
|
||||
if (slot0Loading || liquidityLoading) return [PoolState.LOADING, null]
|
||||
|
||||
if (!slot0 || !liquidity) return [PoolState.NOT_EXISTS, null]
|
||||
|
||||
if (!slot0.sqrtPriceX96 || slot0.sqrtPriceX96.eq(0)) return [PoolState.NOT_EXISTS, null]
|
||||
|
||||
try {
|
||||
return [PoolState.EXISTS, new Pool(token0, token1, fee, slot0.sqrtPriceX96, liquidity[0], slot0.tick)]
|
||||
const pool = PoolCache.getPool(token0, token1, fee, slot0.sqrtPriceX96, liquidity[0], slot0.tick)
|
||||
return [PoolState.EXISTS, pool]
|
||||
} catch (error) {
|
||||
console.error('Error when constructing the pool', error)
|
||||
return [PoolState.NOT_EXISTS, null]
|
||||
}
|
||||
})
|
||||
}, [liquidities, poolKeys, slot0s, transformed])
|
||||
}, [liquidities, poolKeys, slot0s, poolTokens])
|
||||
}
|
||||
|
||||
export function usePool(
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { BigNumber } from '@ethersproject/bignumber'
|
||||
import { Currency, CurrencyAmount, Token } from '@uniswap/sdk-core'
|
||||
import { useSingleCallResult } from 'lib/hooks/multicall'
|
||||
import { useMemo } from 'react'
|
||||
|
||||
import { useTokenContract } from './useContract'
|
||||
|
||||
@@ -9,7 +9,10 @@ import { useTokenContract } from './useContract'
|
||||
export function useTotalSupply(token?: Currency): CurrencyAmount<Token> | undefined {
|
||||
const contract = useTokenContract(token?.isToken ? token.address : undefined, false)
|
||||
|
||||
const totalSupply: BigNumber = useSingleCallResult(contract, 'totalSupply')?.result?.[0]
|
||||
const totalSupplyStr: string | undefined = useSingleCallResult(contract, 'totalSupply')?.result?.[0]?.toString()
|
||||
|
||||
return token?.isToken && totalSupply ? CurrencyAmount.fromRawAmount(token, totalSupply.toString()) : undefined
|
||||
return useMemo(
|
||||
() => (token?.isToken && totalSupplyStr ? CurrencyAmount.fromRawAmount(token, totalSupplyStr) : undefined),
|
||||
[token, totalSupplyStr]
|
||||
)
|
||||
}
|
||||
|
||||
@@ -4,14 +4,14 @@ import tryParseCurrencyAmount from 'lib/utils/tryParseCurrencyAmount'
|
||||
import { useMemo } from 'react'
|
||||
|
||||
import { SupportedChainId } from '../constants/chains'
|
||||
import { DAI_OPTIMISM, USDC, USDC_ARBITRUM, USDC_POLYGON } from '../constants/tokens'
|
||||
import { DAI_OPTIMISM, USDC_ARBITRUM, USDC_MAINNET, USDC_POLYGON } from '../constants/tokens'
|
||||
import { useBestV2Trade } from './useBestV2Trade'
|
||||
import { useClientSideV3Trade } from './useClientSideV3Trade'
|
||||
|
||||
// Stablecoin amounts used when calculating spot price for a given currency.
|
||||
// The amount is large enough to filter low liquidity pairs.
|
||||
export const STABLECOIN_AMOUNT_OUT: { [chainId: number]: CurrencyAmount<Token> } = {
|
||||
[SupportedChainId.MAINNET]: CurrencyAmount.fromRawAmount(USDC, 100_000e6),
|
||||
[SupportedChainId.MAINNET]: CurrencyAmount.fromRawAmount(USDC_MAINNET, 100_000e6),
|
||||
[SupportedChainId.ARBITRUM_ONE]: CurrencyAmount.fromRawAmount(USDC_ARBITRUM, 10_000e6),
|
||||
[SupportedChainId.OPTIMISM]: CurrencyAmount.fromRawAmount(DAI_OPTIMISM, 10_000e18),
|
||||
[SupportedChainId.POLYGON]: CurrencyAmount.fromRawAmount(USDC_POLYGON, 10_000e6),
|
||||
@@ -78,17 +78,18 @@ export function useStablecoinAmountFromFiatValue(fiatValue: string | null | unde
|
||||
const { chainId } = useActiveWeb3React()
|
||||
const stablecoin = chainId ? STABLECOIN_AMOUNT_OUT[chainId]?.currency : undefined
|
||||
|
||||
if (fiatValue === null || fiatValue === undefined || !chainId || !stablecoin) {
|
||||
return undefined
|
||||
}
|
||||
return useMemo(() => {
|
||||
if (fiatValue === null || fiatValue === undefined || !chainId || !stablecoin) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
// trim for decimal precision when parsing
|
||||
const parsedForDecimals = parseFloat(fiatValue).toFixed(stablecoin.decimals).toString()
|
||||
|
||||
try {
|
||||
// parse USD string into CurrencyAmount based on stablecoin decimals
|
||||
return tryParseCurrencyAmount(parsedForDecimals, stablecoin)
|
||||
} catch (error) {
|
||||
return undefined
|
||||
}
|
||||
// trim for decimal precision when parsing
|
||||
const parsedForDecimals = parseFloat(fiatValue).toFixed(stablecoin.decimals).toString()
|
||||
try {
|
||||
// parse USD string into CurrencyAmount based on stablecoin decimals
|
||||
return tryParseCurrencyAmount(parsedForDecimals, stablecoin)
|
||||
} catch (error) {
|
||||
return undefined
|
||||
}
|
||||
}, [chainId, fiatValue, stablecoin])
|
||||
}
|
||||
|
||||
@@ -73,4 +73,3 @@ ReactDOM.render(
|
||||
if (process.env.REACT_APP_SERVICE_WORKER !== 'false') {
|
||||
serviceWorkerRegistration.register()
|
||||
}
|
||||
export { INFURA_NETWORK_URLS } from 'constants/infura'
|
||||
|
||||
BIN
src/lib/assets/missing-token-image.png
Normal file
|
After Width: | Height: | Size: 2.5 KiB |
@@ -1,10 +1,10 @@
|
||||
<svg width="23" height="20" viewBox="0 0 23 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<linearGradient id="gradient1" x1="0" y1="0" x2="1" y2="0" gradientTransform="rotate(95)">
|
||||
<linearGradient id="gradient" x1="0" y1="0" x2="1" y2="0" gradientTransform="rotate(95)">
|
||||
<stop id="stop1" offset="0" stop-color="#2274E2"/>
|
||||
<stop id="stop1" offset="0.5" stop-color="#2274E2"/>
|
||||
<stop id="stop2" offset="1" stop-color="#3FB672" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<path d="M16 16C10 16 9 10 5 10M16 16C16 17.6569 17.3431 19 19 19C20.6569 19 22 17.6569 22 16C22 14.3431 20.6569 13 19 13C17.3431 13 16 14.3431 16 16ZM5 10C9 10 10 4 16 4M5 10H1.5M16 4C16 5.65685 17.3431 7 19 7C20.6569 7 22 5.65685 22 4C22 2.34315 20.6569 1 19 1C17.3431 1 16 2.34315 16 4Z" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" stroke="url(#gradient1)" />
|
||||
<path d="M16 16C10 16 9 10 5 10M16 16C16 17.6569 17.3431 19 19 19C20.6569 19 22 17.6569 22 16C22 14.3431 20.6569 13 19 13C17.3431 13 16 14.3431 16 16ZM5 10C9 10 10 4 16 4M5 10H1.5M16 4C16 5.65685 17.3431 7 19 7C20.6569 7 22 5.65685 22 4C22 2.34315 20.6569 1 19 1C17.3431 1 16 2.34315 16 4Z" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" stroke="url(#gradient)" />
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 782 B After Width: | Height: | Size: 780 B |
35
src/lib/assets/svg/inline_spinner.svg
Normal file
@@ -0,0 +1,35 @@
|
||||
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<mask id="mask">
|
||||
<circle cx="12" cy="12" r="10" fill="black" stroke="black" stroke-width="2" />
|
||||
<rect width="12" height="12" fill="white" stroke-width="0" />
|
||||
<circle cx="2" cy="12" r="1" fill="white" stroke-width="0" />
|
||||
<circle cx="12" cy="2" r="1" fill="white" stroke-width="0" />
|
||||
</mask>
|
||||
<circle
|
||||
cx="12"
|
||||
cy="12"
|
||||
r="6"
|
||||
stroke="none"
|
||||
/>
|
||||
<circle
|
||||
id="track"
|
||||
cx="12"
|
||||
cy="12"
|
||||
r="10"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
fill="none"
|
||||
/>
|
||||
<circle
|
||||
cx="12"
|
||||
cy="12"
|
||||
r="10"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
fill="none"
|
||||
mask="url(#mask)"
|
||||
/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 931 B |
@@ -1,12 +1,11 @@
|
||||
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<mask id="mask">
|
||||
<rect width="24" height="24" fill="white" stroke-width="0" />
|
||||
<circle cx="12" cy="12" r="10" stroke="white" stroke-width="2" />
|
||||
<rect width="12" height="12" fill="black" stroke-width="0" />
|
||||
<circle cx="2" cy="12" r="1" fill="white" stroke-width="0" />
|
||||
<circle cx="12" cy="2" r="1" fill="white" stroke-width="0" />
|
||||
</mask>
|
||||
<circle
|
||||
id="circle"
|
||||
cx="12"
|
||||
cy="12"
|
||||
r="10"
|
||||
|
||||
|
Before Width: | Height: | Size: 608 B After Width: | Height: | Size: 592 B |
@@ -61,14 +61,13 @@ export interface Action {
|
||||
children: ReactNode
|
||||
}
|
||||
|
||||
export interface ActionButtonProps {
|
||||
export interface BaseProps {
|
||||
color?: Color
|
||||
disabled?: boolean
|
||||
action?: Action
|
||||
onClick: () => void
|
||||
children: ReactNode
|
||||
}
|
||||
|
||||
export type ActionButtonProps = BaseProps & Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, keyof BaseProps>
|
||||
|
||||
export default function ActionButton({ color = 'accent', disabled, action, onClick, children }: ActionButtonProps) {
|
||||
const textColor = useMemo(() => (color === 'accent' && !disabled ? 'onAccent' : 'currentColor'), [color, disabled])
|
||||
return (
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Trans } from '@lingui/macro'
|
||||
import Row from 'lib/components/Row'
|
||||
import { Logo } from 'lib/icons'
|
||||
import styled, { brand, ThemedText } from 'lib/theme'
|
||||
import { memo } from 'react'
|
||||
|
||||
import ExternalLink from './ExternalLink'
|
||||
|
||||
@@ -24,7 +25,7 @@ const UniswapA = styled(ExternalLink)`
|
||||
}
|
||||
`
|
||||
|
||||
export default function BrandedFooter() {
|
||||
export default memo(function BrandedFooter() {
|
||||
return (
|
||||
<Row justify="center">
|
||||
<UniswapA href={`https://uniswap.org/`}>
|
||||
@@ -37,4 +38,4 @@ export default function BrandedFooter() {
|
||||
</UniswapA>
|
||||
</Row>
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -10,6 +10,7 @@ export const BaseButton = styled.button`
|
||||
cursor: pointer;
|
||||
font-size: inherit;
|
||||
font-weight: inherit;
|
||||
height: inherit;
|
||||
line-height: inherit;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
|
||||
@@ -40,7 +40,10 @@ export function Provider({ value, children }: ProviderProps) {
|
||||
}
|
||||
}, [active])
|
||||
return (
|
||||
<div ref={ref}>
|
||||
<div
|
||||
ref={ref}
|
||||
style={{ isolation: 'isolate' }} // creates a new stacking context, preventing the dialog from intercepting non-dialog clicks
|
||||
>
|
||||
<Context.Provider value={context}>{children}</Context.Provider>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -1,17 +1,14 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import useScrollbar from 'lib/hooks/useScrollbar'
|
||||
import { AlertTriangle, Expando, Icon, Info, LargeIcon } from 'lib/icons'
|
||||
import ActionButton from 'lib/components/ActionButton'
|
||||
import Column from 'lib/components/Column'
|
||||
import Expando from 'lib/components/Expando'
|
||||
import { AlertTriangle, Icon, LargeIcon } from 'lib/icons'
|
||||
import styled, { Color, ThemedText } from 'lib/theme'
|
||||
import { ReactNode, useState } from 'react'
|
||||
|
||||
import ActionButton from '../ActionButton'
|
||||
import { IconButton } from '../Button'
|
||||
import Column from '../Column'
|
||||
import Row from '../Row'
|
||||
import Rule from '../Rule'
|
||||
import { ReactNode, useCallback, useState } from 'react'
|
||||
|
||||
const HeaderIcon = styled(LargeIcon)`
|
||||
flex-grow: 1;
|
||||
transition: height 0.25s, width 0.25s;
|
||||
|
||||
svg {
|
||||
transition: height 0.25s, width 0.25s;
|
||||
@@ -34,7 +31,6 @@ export function StatusHeader({ icon: Icon, iconColor, iconSize = 4, children }:
|
||||
{children}
|
||||
</Column>
|
||||
</Column>
|
||||
<Rule />
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -48,40 +44,6 @@ const ErrorHeader = styled(Column)<{ open: boolean }>`
|
||||
transition: max-height 0.25s;
|
||||
}
|
||||
`
|
||||
const ErrorColumn = styled(Column)``
|
||||
const ExpandoColumn = styled(Column)<{ open: boolean }>`
|
||||
flex-grow: ${({ open }) => (open ? 2 : 0)};
|
||||
transition: flex-grow 0.25s, gap 0.25s;
|
||||
|
||||
${Rule} {
|
||||
margin-bottom: ${({ open }) => (open ? 0 : 0.75)}em;
|
||||
transition: margin-bottom 0.25s;
|
||||
}
|
||||
|
||||
${ErrorColumn} {
|
||||
flex-basis: 0;
|
||||
flex-grow: ${({ open }) => (open ? 1 : 0)};
|
||||
overflow-y: hidden;
|
||||
position: relative;
|
||||
transition: flex-grow 0.25s;
|
||||
|
||||
${Column} {
|
||||
height: 100%;
|
||||
padding: ${({ open }) => (open ? '0.5em 0' : 0)};
|
||||
transition: padding 0.25s;
|
||||
|
||||
:after {
|
||||
background: linear-gradient(#ffffff00, ${({ theme }) => theme.dialog});
|
||||
bottom: 0;
|
||||
content: '';
|
||||
height: 0.75em;
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
width: calc(100% - 1em);
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
interface ErrorDialogProps {
|
||||
header?: ReactNode
|
||||
@@ -92,8 +54,8 @@ interface ErrorDialogProps {
|
||||
|
||||
export default function ErrorDialog({ header, error, action, onClick }: ErrorDialogProps) {
|
||||
const [open, setOpen] = useState(false)
|
||||
const [details, setDetails] = useState<HTMLDivElement | null>(null)
|
||||
const scrollbar = useScrollbar(details)
|
||||
const onExpand = useCallback(() => setOpen((open) => !open), [])
|
||||
|
||||
return (
|
||||
<Column flex padded gap={0.75} align="stretch" style={{ height: '100%' }}>
|
||||
<StatusHeader icon={AlertTriangle} iconColor="error" iconSize={open ? 3 : 4}>
|
||||
@@ -104,27 +66,15 @@ export default function ErrorDialog({ header, error, action, onClick }: ErrorDia
|
||||
<ThemedText.Body2>{header}</ThemedText.Body2>
|
||||
</ErrorHeader>
|
||||
</StatusHeader>
|
||||
<Row>
|
||||
<Row gap={0.5}>
|
||||
<Info color="secondary" />
|
||||
<ThemedText.Subhead2 color="secondary">
|
||||
<Trans>Error details</Trans>
|
||||
</ThemedText.Subhead2>
|
||||
</Row>
|
||||
<IconButton color="secondary" onClick={() => setOpen(!open)} icon={Expando} iconProps={{ open }} />
|
||||
</Row>
|
||||
<ExpandoColumn flex align="stretch" open={open}>
|
||||
<Rule />
|
||||
<ErrorColumn>
|
||||
<Column gap={0.5} ref={setDetails} css={scrollbar}>
|
||||
<ThemedText.Code>
|
||||
{error.name}
|
||||
{error.message ? `: ${error.message}` : ''}
|
||||
</ThemedText.Code>
|
||||
</Column>
|
||||
</ErrorColumn>
|
||||
<Column gap={open ? 0 : 0.75} style={{ transition: 'gap 0.25s' }}>
|
||||
<Expando title={<Trans>Error details</Trans>} open={open} onExpand={onExpand} height={7.5}>
|
||||
<ThemedText.Code userSelect>
|
||||
{error.name}
|
||||
{error.message ? `: ${error.message}` : ''}
|
||||
</ThemedText.Code>
|
||||
</Expando>
|
||||
<ActionButton onClick={onClick}>{action}</ActionButton>
|
||||
</ExpandoColumn>
|
||||
</Column>
|
||||
</Column>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import { SupportedChainId } from 'constants/chains'
|
||||
import useActiveWeb3React from 'lib/hooks/useActiveWeb3React'
|
||||
import { Link } from 'lib/icons'
|
||||
import styled, { Color } from 'lib/theme'
|
||||
import { ReactNode, useMemo } from 'react'
|
||||
import { ExplorerDataType, getExplorerLink } from 'utils/getExplorerLink'
|
||||
|
||||
import ExternalLink from './ExternalLink'
|
||||
import Row from './Row'
|
||||
|
||||
const Link = styled(ExternalLink)<{ color: Color }>`
|
||||
const StyledExternalLink = styled(ExternalLink)<{ color: Color }>`
|
||||
color: ${({ theme, color }) => theme[color]};
|
||||
text-decoration: none;
|
||||
`
|
||||
@@ -25,8 +27,10 @@ export default function EtherscanLink({ data, type, color = 'currentColor', chil
|
||||
[chainId, data, type]
|
||||
)
|
||||
return (
|
||||
<Link href={url} color={color} target="_blank">
|
||||
{children}
|
||||
</Link>
|
||||
<StyledExternalLink href={url} color={color} target="_blank">
|
||||
<Row gap={0.25}>
|
||||
{children} <Link />
|
||||
</Row>
|
||||
</StyledExternalLink>
|
||||
)
|
||||
}
|
||||
|
||||
66
src/lib/components/Expando.tsx
Normal file
@@ -0,0 +1,66 @@
|
||||
import { IconButton } from 'lib/components/Button'
|
||||
import Column from 'lib/components/Column'
|
||||
import Row from 'lib/components/Row'
|
||||
import Rule from 'lib/components/Rule'
|
||||
import useScrollbar from 'lib/hooks/useScrollbar'
|
||||
import { Expando as ExpandoIcon } from 'lib/icons'
|
||||
import styled from 'lib/theme'
|
||||
import { PropsWithChildren, ReactNode, useState } from 'react'
|
||||
|
||||
const HeaderColumn = styled(Column)`
|
||||
transition: gap 0.25s;
|
||||
`
|
||||
|
||||
const ExpandoColumn = styled(Column)<{ height: number; open: boolean }>`
|
||||
height: ${({ height, open }) => (open ? height : 0)}em;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
transition: height 0.25s, padding 0.25s;
|
||||
|
||||
:after {
|
||||
background: linear-gradient(#ffffff00, ${({ theme }) => theme.dialog});
|
||||
bottom: 0;
|
||||
content: '';
|
||||
height: 0.75em;
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
width: calc(100% - 1em);
|
||||
}
|
||||
`
|
||||
|
||||
const InnerColumn = styled(Column)<{ height: number }>`
|
||||
height: ${({ height }) => height}em;
|
||||
padding: 0.5em 0;
|
||||
`
|
||||
|
||||
interface ExpandoProps {
|
||||
title: ReactNode
|
||||
open: boolean
|
||||
onExpand: () => void
|
||||
// The absolute height of the expanded container, in em.
|
||||
height: number
|
||||
}
|
||||
|
||||
/** A scrollable Expando with an absolute height. */
|
||||
export default function Expando({ title, open, onExpand, height, children }: PropsWithChildren<ExpandoProps>) {
|
||||
const [scrollingEl, setScrollingEl] = useState<HTMLDivElement | null>(null)
|
||||
const scrollbar = useScrollbar(scrollingEl)
|
||||
return (
|
||||
<Column>
|
||||
<HeaderColumn gap={open ? 0.5 : 0.75}>
|
||||
<Rule />
|
||||
<Row>
|
||||
{title}
|
||||
<IconButton color="secondary" onClick={onExpand} icon={ExpandoIcon} iconProps={{ open }} />
|
||||
</Row>
|
||||
<Rule />
|
||||
</HeaderColumn>
|
||||
<ExpandoColumn open={open} height={height}>
|
||||
<InnerColumn flex align="stretch" height={height} ref={setScrollingEl} css={scrollbar}>
|
||||
{children}
|
||||
</InnerColumn>
|
||||
</ExpandoColumn>
|
||||
</Column>
|
||||
)
|
||||
return null
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
import JSBI from 'jsbi'
|
||||
import styled, { css } from 'lib/theme'
|
||||
import { forwardRef, HTMLProps, useCallback, useEffect, useState } from 'react'
|
||||
|
||||
@@ -78,12 +77,16 @@ interface EnforcedNumericInputProps extends NumericInputProps {
|
||||
}
|
||||
|
||||
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 [aInteger, aDecimal] = toParts(a)
|
||||
const [bInteger, bDecimal] = toParts(b)
|
||||
return aInteger === bInteger && aDecimal === bDecimal
|
||||
|
||||
function toParts(num: string) {
|
||||
let [integer, decimal] = num.split('.')
|
||||
integer = integer?.match(/([1-9]\d*)/)?.[1] || ''
|
||||
decimal = decimal?.match(/(\d*[1-9])/)?.[1] || ''
|
||||
return [integer, decimal]
|
||||
}
|
||||
}
|
||||
|
||||
const NumericInput = forwardRef<HTMLInputElement, EnforcedNumericInputProps>(function NumericInput(
|
||||
|
||||
@@ -14,14 +14,16 @@ const PopoverContainer = styled.div<{ show: boolean }>`
|
||||
border: 1px solid ${({ theme }) => theme.outline};
|
||||
border-radius: 0.5em;
|
||||
opacity: ${(props) => (props.show ? 1 : 0)};
|
||||
padding: 8px;
|
||||
padding: 10px 12px;
|
||||
transition: visibility 0.25s linear, opacity 0.25s linear;
|
||||
visibility: ${(props) => (props.show ? 'visible' : 'hidden')};
|
||||
z-index: ${Layer.TOOLTIP};
|
||||
`
|
||||
|
||||
const Reference = styled.div`
|
||||
align-self: flex-start;
|
||||
display: inline-block;
|
||||
height: 1em;
|
||||
`
|
||||
|
||||
const Arrow = styled.div`
|
||||
|
||||
@@ -6,6 +6,10 @@ const Rule = styled.hr<{ padded?: true; scrollingEdge?: 'top' | 'bottom' }>`
|
||||
margin: 0 ${({ padded }) => (padded ? '0.75em' : 0)};
|
||||
margin-bottom: ${({ scrollingEdge }) => (scrollingEdge === 'bottom' ? -1 : 0)}px;
|
||||
margin-top: ${({ scrollingEdge }) => (scrollingEdge !== 'bottom' ? -1 : 0)}px;
|
||||
|
||||
// Integrators will commonly modify hr width - this overrides any modifications within the widget.
|
||||
max-width: auto;
|
||||
width: auto;
|
||||
`
|
||||
|
||||
export default Rule
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useLingui } from '@lingui/react'
|
||||
import { useUSDCValue } from 'hooks/useUSDCPrice'
|
||||
import { loadingOpacityCss } from 'lib/css/loading'
|
||||
import { Currency, CurrencyAmount } from '@uniswap/sdk-core'
|
||||
import { loadingTransitionCss } from 'lib/css/loading'
|
||||
import {
|
||||
useIsSwapFieldIndependent,
|
||||
useSwapAmount,
|
||||
@@ -21,8 +21,8 @@ import Row from '../Row'
|
||||
import TokenImg from '../TokenImg'
|
||||
import TokenInput from './TokenInput'
|
||||
|
||||
export const LoadingRow = styled(Row)<{ $loading: boolean }>`
|
||||
${loadingOpacityCss};
|
||||
export const USDC = styled(Row)`
|
||||
${loadingTransitionCss};
|
||||
`
|
||||
|
||||
export const Balance = styled(ThemedText.Body2)<{ focused: boolean }>`
|
||||
@@ -45,21 +45,40 @@ export interface InputProps {
|
||||
focused: boolean
|
||||
}
|
||||
|
||||
interface UseFormattedFieldAmountArguments {
|
||||
disabled: boolean
|
||||
currencyAmount?: CurrencyAmount<Currency>
|
||||
fieldAmount?: string
|
||||
}
|
||||
|
||||
export function useFormattedFieldAmount({ disabled, currencyAmount, fieldAmount }: UseFormattedFieldAmountArguments) {
|
||||
return useMemo(() => {
|
||||
if (disabled) {
|
||||
return ''
|
||||
}
|
||||
if (fieldAmount !== undefined) {
|
||||
return fieldAmount
|
||||
}
|
||||
if (currencyAmount) {
|
||||
return currencyAmount.toSignificant(6)
|
||||
}
|
||||
return ''
|
||||
}, [disabled, currencyAmount, fieldAmount])
|
||||
}
|
||||
|
||||
export default function Input({ disabled, focused }: InputProps) {
|
||||
const { i18n } = useLingui()
|
||||
const {
|
||||
[Field.INPUT]: { balance, amount: tradeCurrencyAmount, usdc },
|
||||
trade: { state: tradeState },
|
||||
currencyBalances: { [Field.INPUT]: balance },
|
||||
currencyAmounts: { [Field.INPUT]: swapInputCurrencyAmount },
|
||||
} = useSwapInfo()
|
||||
const inputUSDC = useUSDCValue(swapInputCurrencyAmount)
|
||||
|
||||
const [swapInputAmount, updateSwapInputAmount] = useSwapAmount(Field.INPUT)
|
||||
const [swapInputCurrency, updateSwapInputCurrency] = useSwapCurrency(Field.INPUT)
|
||||
const [inputAmount, updateInputAmount] = useSwapAmount(Field.INPUT)
|
||||
const [inputCurrency, updateInputCurrency] = useSwapCurrency(Field.INPUT)
|
||||
const inputCurrencyAmount = useSwapCurrencyAmount(Field.INPUT)
|
||||
|
||||
// extract eagerly in case of reversal
|
||||
usePrefetchCurrencyColor(swapInputCurrency)
|
||||
usePrefetchCurrencyColor(inputCurrency)
|
||||
|
||||
const isRouteLoading = tradeState === TradeState.SYNCING || tradeState === TradeState.LOADING
|
||||
const isDependentField = !useIsSwapFieldIndependent(Field.INPUT)
|
||||
@@ -69,39 +88,41 @@ export default function Input({ disabled, focused }: InputProps) {
|
||||
const mockApproved = true
|
||||
|
||||
// account for gas needed if using max on native token
|
||||
const maxAmount = useMemo(() => maxAmountSpend(balance), [balance])
|
||||
|
||||
const onMax = useMemo(() => {
|
||||
if (maxAmount?.greaterThan(0)) {
|
||||
return () => updateSwapInputAmount(maxAmount.toExact())
|
||||
}
|
||||
return
|
||||
}, [maxAmount, updateSwapInputAmount])
|
||||
const max = useMemo(() => {
|
||||
const maxAmount = maxAmountSpend(balance)
|
||||
return maxAmount?.greaterThan(0) ? maxAmount.toExact() : undefined
|
||||
}, [balance])
|
||||
|
||||
const balanceColor = useMemo(() => {
|
||||
const insufficientBalance =
|
||||
balance &&
|
||||
(inputCurrencyAmount ? inputCurrencyAmount.greaterThan(balance) : swapInputCurrencyAmount?.greaterThan(balance))
|
||||
(inputCurrencyAmount ? inputCurrencyAmount.greaterThan(balance) : tradeCurrencyAmount?.greaterThan(balance))
|
||||
return insufficientBalance ? 'error' : undefined
|
||||
}, [balance, inputCurrencyAmount, swapInputCurrencyAmount])
|
||||
}, [balance, inputCurrencyAmount, tradeCurrencyAmount])
|
||||
|
||||
const amount = useFormattedFieldAmount({
|
||||
disabled,
|
||||
currencyAmount: tradeCurrencyAmount,
|
||||
fieldAmount: inputAmount,
|
||||
})
|
||||
|
||||
return (
|
||||
<InputColumn gap={0.5} approved={mockApproved}>
|
||||
<TokenInput
|
||||
currency={swapInputCurrency}
|
||||
amount={(swapInputAmount !== undefined ? swapInputAmount : swapInputCurrencyAmount?.toSignificant(6)) ?? ''}
|
||||
currency={inputCurrency}
|
||||
amount={amount}
|
||||
max={max}
|
||||
disabled={disabled}
|
||||
onMax={onMax}
|
||||
onChangeInput={updateSwapInputAmount}
|
||||
onChangeCurrency={updateSwapInputCurrency}
|
||||
onChangeInput={updateInputAmount}
|
||||
onChangeCurrency={updateInputCurrency}
|
||||
loading={isLoading}
|
||||
>
|
||||
<ThemedText.Body2 color="secondary">
|
||||
<ThemedText.Body2 color="secondary" userSelect>
|
||||
<Row>
|
||||
<LoadingRow $loading={isLoading}>{inputUSDC ? `$${inputUSDC.toFixed(2)}` : '-'}</LoadingRow>
|
||||
<USDC isLoading={isRouteLoading}>{usdc ? `$${formatCurrencyAmount(usdc, 6, 'en', 2)}` : '-'}</USDC>
|
||||
{balance && (
|
||||
<Balance color={balanceColor} focused={focused}>
|
||||
Balance: <span style={{ userSelect: 'text' }}>{formatCurrencyAmount(balance, 4, i18n.locale)}</span>
|
||||
Balance: <span>{formatCurrencyAmount(balance, 4, i18n.locale)}</span>
|
||||
</Balance>
|
||||
)}
|
||||
</Row>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
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 BrandedFooter from 'lib/components/BrandedFooter'
|
||||
@@ -8,15 +7,13 @@ import { useIsSwapFieldIndependent, useSwapAmount, useSwapCurrency, useSwapInfo
|
||||
import useCurrencyColor from 'lib/hooks/useCurrencyColor'
|
||||
import { Field } from 'lib/state/swap'
|
||||
import styled, { DynamicThemeProvider, ThemedText } from 'lib/theme'
|
||||
import { PropsWithChildren, useMemo } from 'react'
|
||||
import { PropsWithChildren } from 'react'
|
||||
import { TradeState } from 'state/routing/types'
|
||||
import { computeFiatValuePriceImpact } from 'utils/computeFiatValuePriceImpact'
|
||||
import { formatCurrencyAmount } from 'utils/formatCurrencyAmount'
|
||||
import { getPriceImpactWarning } from 'utils/prices'
|
||||
|
||||
import Column from '../Column'
|
||||
import Row from '../Row'
|
||||
import { Balance, InputProps, LoadingRow } from './Input'
|
||||
import { Balance, InputProps, USDC, useFormattedFieldAmount } from './Input'
|
||||
import TokenInput from './TokenInput'
|
||||
|
||||
export const colorAtom = atom<string | undefined>(undefined)
|
||||
@@ -41,9 +38,9 @@ export default function Output({ disabled, focused, children }: PropsWithChildre
|
||||
const { i18n } = useLingui()
|
||||
|
||||
const {
|
||||
[Field.OUTPUT]: { balance, amount: outputCurrencyAmount, usdc: outputUSDC },
|
||||
trade: { state: tradeState },
|
||||
currencyBalances: { [Field.OUTPUT]: balance },
|
||||
currencyAmounts: { [Field.INPUT]: inputCurrencyAmount, [Field.OUTPUT]: outputCurrencyAmount },
|
||||
impact,
|
||||
} = useSwapInfo()
|
||||
|
||||
const [swapOutputAmount, updateSwapOutputAmount] = useSwapAmount(Field.OUTPUT)
|
||||
@@ -60,23 +57,11 @@ export default function Output({ disabled, focused, children }: PropsWithChildre
|
||||
// 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 fiatValuePriceImpact = computeFiatValuePriceImpact(inputUSDC, outputUSDC)
|
||||
if (!fiatValuePriceImpact) return null
|
||||
|
||||
const color = getPriceImpactWarning(fiatValuePriceImpact)
|
||||
const sign = fiatValuePriceImpact.lessThan(0) ? '+' : ''
|
||||
const displayedPriceImpact = parseFloat(fiatValuePriceImpact.multiply(-1)?.toSignificant(3))
|
||||
return (
|
||||
<ThemedText.Body2 color={color}>
|
||||
({sign}
|
||||
{displayedPriceImpact}%)
|
||||
</ThemedText.Body2>
|
||||
)
|
||||
}, [inputUSDC, outputUSDC])
|
||||
const amount = useFormattedFieldAmount({
|
||||
disabled,
|
||||
currencyAmount: outputCurrencyAmount,
|
||||
fieldAmount: swapOutputAmount,
|
||||
})
|
||||
|
||||
return (
|
||||
<DynamicThemeProvider color={color}>
|
||||
@@ -88,20 +73,21 @@ export default function Output({ disabled, focused, children }: PropsWithChildre
|
||||
</Row>
|
||||
<TokenInput
|
||||
currency={swapOutputCurrency}
|
||||
amount={(swapOutputAmount !== undefined ? swapOutputAmount : outputCurrencyAmount?.toSignificant(6)) ?? ''}
|
||||
amount={amount}
|
||||
disabled={disabled}
|
||||
onChangeInput={updateSwapOutputAmount}
|
||||
onChangeCurrency={updateSwapOutputCurrency}
|
||||
loading={isLoading}
|
||||
>
|
||||
<ThemedText.Body2 color="secondary">
|
||||
<ThemedText.Body2 color="secondary" userSelect>
|
||||
<Row>
|
||||
<LoadingRow gap={0.5} $loading={isLoading}>
|
||||
{outputUSDC?.toFixed(2)} {priceImpact}
|
||||
</LoadingRow>
|
||||
<USDC gap={0.5} isLoading={isRouteLoading}>
|
||||
{outputUSDC ? `$${formatCurrencyAmount(outputUSDC, 6, 'en', 2)}` : '-'}{' '}
|
||||
{impact.display && <ThemedText.Body2 color={impact.warning}>({impact.display})</ThemedText.Body2>}
|
||||
</USDC>
|
||||
{balance && (
|
||||
<Balance focused={focused}>
|
||||
Balance: <span style={{ userSelect: 'text' }}>{formatCurrencyAmount(balance, 4, i18n.locale)}</span>
|
||||
Balance: <span>{formatCurrencyAmount(balance, 4, i18n.locale)}</span>
|
||||
</Balance>
|
||||
)}
|
||||
</Row>
|
||||
|
||||
54
src/lib/components/Swap/Price.tsx
Normal file
@@ -0,0 +1,54 @@
|
||||
import { useLingui } from '@lingui/react'
|
||||
import { Trade } from '@uniswap/router-sdk'
|
||||
import { Currency, CurrencyAmount, TradeType } from '@uniswap/sdk-core'
|
||||
import Row from 'lib/components/Row'
|
||||
import { ThemedText } from 'lib/theme'
|
||||
import formatLocaleNumber from 'lib/utils/formatLocaleNumber'
|
||||
import { useCallback, useMemo, useState } from 'react'
|
||||
import { formatCurrencyAmount, formatPrice } from 'utils/formatCurrencyAmount'
|
||||
|
||||
import { TextButton } from '../Button'
|
||||
|
||||
interface PriceProps {
|
||||
trade: Trade<Currency, Currency, TradeType>
|
||||
outputUSDC?: CurrencyAmount<Currency>
|
||||
}
|
||||
|
||||
/** Displays the price of a trade. If outputUSDC is included, also displays the unit price. */
|
||||
export default function Price({ trade, outputUSDC }: PriceProps) {
|
||||
const { i18n } = useLingui()
|
||||
const { inputAmount, outputAmount, executionPrice } = trade
|
||||
|
||||
const [base, setBase] = useState<'input' | 'output'>('input')
|
||||
const onClick = useCallback(() => setBase((base) => (base === 'input' ? 'output' : 'input')), [])
|
||||
|
||||
// Compute the usdc price from the output price, so that it aligns with the displayed price.
|
||||
const { price, usdcPrice } = useMemo(() => {
|
||||
switch (base) {
|
||||
case 'input':
|
||||
return {
|
||||
price: executionPrice,
|
||||
usdcPrice: outputUSDC?.multiply(inputAmount.decimalScale).divide(inputAmount),
|
||||
}
|
||||
case 'output':
|
||||
return {
|
||||
price: executionPrice.invert(),
|
||||
usdcPrice: outputUSDC?.multiply(outputAmount.decimalScale).divide(outputAmount),
|
||||
}
|
||||
}
|
||||
}, [base, executionPrice, inputAmount, outputAmount, outputUSDC])
|
||||
|
||||
return (
|
||||
<TextButton color="primary" onClick={onClick}>
|
||||
<ThemedText.Caption>
|
||||
<Row gap={0.25}>
|
||||
{formatLocaleNumber({ number: 1, sigFigs: 1, locale: i18n.locale })} {price.baseCurrency.symbol} ={' '}
|
||||
{formatPrice(price, 6, i18n.locale)} {price.quoteCurrency.symbol}
|
||||
{usdcPrice && (
|
||||
<ThemedText.Caption color="secondary">(${formatCurrencyAmount(usdcPrice, 6, 'en', 2)})</ThemedText.Caption>
|
||||
)}
|
||||
</Row>
|
||||
</ThemedText.Caption>
|
||||
</TextButton>
|
||||
)
|
||||
}
|
||||
139
src/lib/components/Swap/RoutingDiagram/index.tsx
Normal file
@@ -0,0 +1,139 @@
|
||||
import { Plural, Trans } from '@lingui/macro'
|
||||
import { Currency, TradeType } from '@uniswap/sdk-core'
|
||||
import { FeeAmount } from '@uniswap/v3-sdk'
|
||||
import { ReactComponent as DotLine } from 'assets/svg/dot_line.svg'
|
||||
import Column from 'lib/components/Column'
|
||||
import Row from 'lib/components/Row'
|
||||
import Rule from 'lib/components/Rule'
|
||||
import TokenImg from 'lib/components/TokenImg'
|
||||
import { AutoRouter } from 'lib/icons'
|
||||
import styled, { Layer, ThemedText } from 'lib/theme'
|
||||
import { useMemo } from 'react'
|
||||
import { InterfaceTrade } from 'state/routing/types'
|
||||
|
||||
import { getTokenPath, RoutingDiagramEntry } from './utils'
|
||||
|
||||
const StyledAutoRouterLabel = styled(ThemedText.ButtonSmall)`
|
||||
@supports (-webkit-background-clip: text) and (-webkit-text-fill-color: transparent) {
|
||||
background-image: linear-gradient(90deg, #2172e5 0%, #54e521 163.16%);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
}
|
||||
`
|
||||
|
||||
function Header({ routes }: { routes: RoutingDiagramEntry[] }) {
|
||||
return (
|
||||
<Row justify="space-between" gap={1}>
|
||||
<ThemedText.Subhead2>
|
||||
<Row gap={0.25}>
|
||||
<AutoRouter />
|
||||
<StyledAutoRouterLabel color="primary" lineHeight={'16px'}>
|
||||
<Trans>Auto Router</Trans>
|
||||
</StyledAutoRouterLabel>
|
||||
</Row>
|
||||
</ThemedText.Subhead2>
|
||||
<ThemedText.Body2>
|
||||
<Plural value={routes.length} _1="Best route via 1 hop" other="Best route via # hops" />
|
||||
</ThemedText.Body2>
|
||||
</Row>
|
||||
)
|
||||
}
|
||||
|
||||
const Dots = styled(DotLine)`
|
||||
color: ${({ theme }) => theme.outline};
|
||||
position: absolute;
|
||||
z-index: ${Layer.UNDERLAYER};
|
||||
`
|
||||
|
||||
const RouteRow = styled(Row)`
|
||||
flex-wrap: nowrap;
|
||||
`
|
||||
|
||||
const RouteNode = styled(Row)`
|
||||
background-color: ${({ theme }) => theme.interactive};
|
||||
border-radius: ${({ theme }) => `${(theme.borderRadius ?? 1) * 0.5}em`};
|
||||
margin-left: 1.625em;
|
||||
padding: 0.25em 0.375em;
|
||||
width: max-content;
|
||||
`
|
||||
|
||||
const RouteBadge = styled.div`
|
||||
background-color: ${({ theme }) => theme.module};
|
||||
border-radius: ${({ theme }) => `${(theme.borderRadius ?? 1) * 0.25}em`};
|
||||
padding: 0.125em;
|
||||
`
|
||||
|
||||
function RouteDetail({ route }: { route: RoutingDiagramEntry }) {
|
||||
const protocol = route.protocol.toUpperCase()
|
||||
return (
|
||||
<RouteNode>
|
||||
<Row gap={0.375}>
|
||||
<ThemedText.Caption>{route.percent.toSignificant(2)}%</ThemedText.Caption>
|
||||
<RouteBadge>
|
||||
<ThemedText.Badge color="secondary">{protocol}</ThemedText.Badge>
|
||||
</RouteBadge>
|
||||
</Row>
|
||||
</RouteNode>
|
||||
)
|
||||
}
|
||||
|
||||
const RoutePool = styled(RouteNode)`
|
||||
margin: 0 0.75em;
|
||||
`
|
||||
|
||||
function Pool({
|
||||
originCurrency,
|
||||
targetCurrency,
|
||||
feeAmount,
|
||||
}: {
|
||||
originCurrency: Currency
|
||||
targetCurrency: Currency
|
||||
feeAmount: FeeAmount
|
||||
}) {
|
||||
return (
|
||||
<RoutePool>
|
||||
<ThemedText.Caption>
|
||||
<Row gap={0.25}>
|
||||
<TokenImg token={originCurrency} />
|
||||
<TokenImg token={targetCurrency} style={{ marginLeft: '-0.65em' }} />
|
||||
{feeAmount / 10_000}%
|
||||
</Row>
|
||||
</ThemedText.Caption>
|
||||
</RoutePool>
|
||||
)
|
||||
}
|
||||
|
||||
function Route({ route }: { route: RoutingDiagramEntry }) {
|
||||
const [originCurrency] = route.path[0]
|
||||
const [, targetCurrency] = route.path[route.path.length - 1]
|
||||
|
||||
return (
|
||||
<Row align="center" style={{ gridTemplateColumns: '1em 1fr 1em' }}>
|
||||
<TokenImg token={originCurrency} />
|
||||
<RouteRow flex style={{ position: 'relative' }}>
|
||||
<Dots />
|
||||
<RouteDetail route={route} />
|
||||
<RouteRow justify="space-evenly" flex>
|
||||
{route.path.map(([originCurrency, targetCurrency, feeAmount], index) => (
|
||||
<Pool key={index} originCurrency={originCurrency} targetCurrency={targetCurrency} feeAmount={feeAmount} />
|
||||
))}
|
||||
</RouteRow>
|
||||
</RouteRow>
|
||||
<TokenImg token={targetCurrency} />
|
||||
</Row>
|
||||
)
|
||||
}
|
||||
|
||||
export default function RoutingDiagram({ trade }: { trade: InterfaceTrade<Currency, Currency, TradeType> }) {
|
||||
const routes: RoutingDiagramEntry[] = useMemo(() => getTokenPath(trade), [trade])
|
||||
|
||||
return (
|
||||
<Column gap={0.75}>
|
||||
<Header routes={routes} />
|
||||
<Rule />
|
||||
{routes.map((route, index) => (
|
||||
<Route key={index} route={route} />
|
||||
))}
|
||||
</Column>
|
||||
)
|
||||
}
|
||||
43
src/lib/components/Swap/RoutingDiagram/utils.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import { Protocol } from '@uniswap/router-sdk'
|
||||
import { Currency, Percent, TradeType } from '@uniswap/sdk-core'
|
||||
import { Pair } from '@uniswap/v2-sdk'
|
||||
import { FeeAmount } from '@uniswap/v3-sdk'
|
||||
import { InterfaceTrade } from 'state/routing/types'
|
||||
|
||||
export interface RoutingDiagramEntry {
|
||||
percent: Percent
|
||||
path: [Currency, Currency, FeeAmount][]
|
||||
protocol: Protocol
|
||||
}
|
||||
|
||||
const V2_DEFAULT_FEE_TIER = 3000
|
||||
|
||||
/**
|
||||
* Loops through all routes on a trade and returns an array of diagram entries.
|
||||
*/
|
||||
export function getTokenPath(trade: InterfaceTrade<Currency, Currency, TradeType>): RoutingDiagramEntry[] {
|
||||
return trade.swaps.map(({ route: { path: tokenPath, pools, protocol }, inputAmount, outputAmount }) => {
|
||||
const portion =
|
||||
trade.tradeType === TradeType.EXACT_INPUT
|
||||
? inputAmount.divide(trade.inputAmount)
|
||||
: outputAmount.divide(trade.outputAmount)
|
||||
const percent = new Percent(portion.numerator, portion.denominator)
|
||||
const path: RoutingDiagramEntry['path'] = []
|
||||
for (let i = 0; i < pools.length; i++) {
|
||||
const nextPool = pools[i]
|
||||
const tokenIn = tokenPath[i]
|
||||
const tokenOut = tokenPath[i + 1]
|
||||
const entry: RoutingDiagramEntry['path'][0] = [
|
||||
tokenIn,
|
||||
tokenOut,
|
||||
nextPool instanceof Pair ? V2_DEFAULT_FEE_TIER : nextPool.fee,
|
||||
]
|
||||
path.push(entry)
|
||||
}
|
||||
return {
|
||||
percent,
|
||||
path,
|
||||
protocol,
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -2,11 +2,11 @@ import { Trans } from '@lingui/macro'
|
||||
import { useAtom } from 'jotai'
|
||||
import Popover from 'lib/components/Popover'
|
||||
import { useTooltip } from 'lib/components/Tooltip'
|
||||
import { getSlippageWarning, toPercent } from 'lib/hooks/useAllowedSlippage'
|
||||
import { getSlippageWarning, toPercent } from 'lib/hooks/useSlippage'
|
||||
import { AlertTriangle, Check, Icon, LargeIcon, XOctagon } from 'lib/icons'
|
||||
import { autoSlippageAtom, maxSlippageAtom } from 'lib/state/settings'
|
||||
import styled, { ThemedText } from 'lib/theme'
|
||||
import { forwardRef, memo, ReactNode, useCallback, useMemo, useRef, useState } from 'react'
|
||||
import { forwardRef, memo, ReactNode, useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
|
||||
import { BaseButton, TextButton } from '../../Button'
|
||||
import Column from '../../Column'
|
||||
@@ -88,7 +88,6 @@ export default function MaxSlippageSelect() {
|
||||
const [autoSlippage, setAutoSlippage] = useAtom(autoSlippageAtom)
|
||||
const [maxSlippage, setMaxSlippage] = useAtom(maxSlippageAtom)
|
||||
const maxSlippageInput = useMemo(() => maxSlippage?.toString() || '', [maxSlippage])
|
||||
const [warning, setWarning] = useState<'warning' | 'error' | undefined>(getSlippageWarning(toPercent(maxSlippage)))
|
||||
|
||||
const option = useRef<HTMLButtonElement>(null)
|
||||
const showTooltip = useTooltip(option.current)
|
||||
@@ -96,20 +95,27 @@ export default function MaxSlippageSelect() {
|
||||
const input = useRef<HTMLInputElement>(null)
|
||||
const focus = useCallback(() => input.current?.focus(), [input])
|
||||
|
||||
const [warning, setWarning] = useState<'warning' | 'error' | undefined>(getSlippageWarning(toPercent(maxSlippage)))
|
||||
useEffect(() => {
|
||||
setWarning(getSlippageWarning(toPercent(maxSlippage)))
|
||||
}, [maxSlippage])
|
||||
|
||||
const onInputSelect = useCallback(() => {
|
||||
focus()
|
||||
const percent = toPercent(maxSlippage)
|
||||
const warning = getSlippageWarning(percent)
|
||||
setAutoSlippage(!percent || warning === 'error')
|
||||
}, [focus, maxSlippage, setAutoSlippage])
|
||||
|
||||
const processValue = useCallback(
|
||||
(value: number | undefined) => {
|
||||
const percent = toPercent(value)
|
||||
const warning = getSlippageWarning(percent)
|
||||
setWarning(warning)
|
||||
setMaxSlippage(value)
|
||||
setAutoSlippage(!percent || warning === 'error')
|
||||
},
|
||||
[setAutoSlippage, setMaxSlippage]
|
||||
)
|
||||
const onInputSelect = useCallback(() => {
|
||||
focus()
|
||||
processValue(maxSlippage)
|
||||
}, [focus, maxSlippage, processValue])
|
||||
|
||||
return (
|
||||
<Column gap={0.75}>
|
||||
@@ -130,7 +136,7 @@ export default function MaxSlippageSelect() {
|
||||
>
|
||||
<Row color={warning === 'error' ? 'error' : undefined}>
|
||||
<DecimalInput
|
||||
size={Math.max(maxSlippageInput.length, 3)}
|
||||
size={Math.max(maxSlippageInput.length, 4)}
|
||||
value={maxSlippageInput}
|
||||
onChange={(input) => processValue(+input)}
|
||||
placeholder={placeholder}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import ErrorDialog, { StatusHeader } from 'lib/components/Error/ErrorDialog'
|
||||
import EtherscanLink from 'lib/components/EtherscanLink'
|
||||
import SwapSummary from 'lib/components/Swap/Summary'
|
||||
import useInterval from 'lib/hooks/useInterval'
|
||||
import { CheckCircle, Clock, Spinner } from 'lib/icons'
|
||||
import { SwapTransactionInfo, Transaction } from 'lib/state/transactions'
|
||||
import { SwapTransactionInfo, Transaction, TransactionType, WrapTransactionInfo } from 'lib/state/transactions'
|
||||
import styled, { ThemedText } from 'lib/theme'
|
||||
import ms from 'ms.macro'
|
||||
import { useCallback, useMemo, useState } from 'react'
|
||||
@@ -12,7 +13,6 @@ import { ExplorerDataType } from 'utils/getExplorerLink'
|
||||
import ActionButton from '../../ActionButton'
|
||||
import Column from '../../Column'
|
||||
import Row from '../../Row'
|
||||
import Summary from '../Summary'
|
||||
|
||||
const errorMessage = (
|
||||
<Trans>
|
||||
@@ -26,7 +26,9 @@ const TransactionRow = styled(Row)`
|
||||
flex-direction: row-reverse;
|
||||
`
|
||||
|
||||
function ElapsedTime({ tx }: { tx: Transaction<SwapTransactionInfo> }) {
|
||||
type PendingTransaction = Transaction<SwapTransactionInfo | WrapTransactionInfo>
|
||||
|
||||
function ElapsedTime({ tx }: { tx: PendingTransaction }) {
|
||||
const [elapsedMs, setElapsedMs] = useState(0)
|
||||
|
||||
useInterval(() => setElapsedMs(Date.now() - tx.addedTime), tx.receipt ? null : ms`1s`)
|
||||
@@ -54,7 +56,7 @@ function ElapsedTime({ tx }: { tx: Transaction<SwapTransactionInfo> }) {
|
||||
}
|
||||
|
||||
interface TransactionStatusProps {
|
||||
tx: Transaction<SwapTransactionInfo>
|
||||
tx: PendingTransaction
|
||||
onClose: () => void
|
||||
}
|
||||
|
||||
@@ -63,14 +65,24 @@ function TransactionStatus({ tx, onClose }: TransactionStatusProps) {
|
||||
return tx.receipt?.status ? CheckCircle : Spinner
|
||||
}, [tx.receipt?.status])
|
||||
const heading = useMemo(() => {
|
||||
return tx.receipt?.status ? <Trans>Transaction submitted</Trans> : <Trans>Transaction pending</Trans>
|
||||
}, [tx.receipt?.status])
|
||||
if (tx.info.type === TransactionType.SWAP) {
|
||||
return tx.receipt?.status ? <Trans>Swap confirmed</Trans> : <Trans>Swap pending</Trans>
|
||||
} else if (tx.info.type === TransactionType.WRAP) {
|
||||
if (tx.info.unwrapped) {
|
||||
return tx.receipt?.status ? <Trans>Unwrap confirmed</Trans> : <Trans>Unwrap pending</Trans>
|
||||
}
|
||||
return tx.receipt?.status ? <Trans>Wrap confirmed</Trans> : <Trans>Wrap pending</Trans>
|
||||
}
|
||||
return tx.receipt?.status ? <Trans>Transaction confirmed</Trans> : <Trans>Transaction pending</Trans>
|
||||
}, [tx.info, tx.receipt?.status])
|
||||
|
||||
return (
|
||||
<Column flex padded gap={0.75} align="stretch" style={{ height: '100%' }}>
|
||||
<StatusHeader icon={Icon} iconColor={tx.receipt?.status ? 'success' : undefined}>
|
||||
<ThemedText.Subhead1>{heading}</ThemedText.Subhead1>
|
||||
<Summary input={tx.info.inputCurrencyAmount} output={tx.info.outputCurrencyAmount} />
|
||||
{tx.info.type === TransactionType.SWAP ? (
|
||||
<SwapSummary input={tx.info.inputCurrencyAmount} output={tx.info.outputCurrencyAmount} />
|
||||
) : null}
|
||||
</StatusHeader>
|
||||
<TransactionRow flex>
|
||||
<ThemedText.ButtonSmall>
|
||||
|
||||
@@ -22,8 +22,11 @@ const UNI = (function () {
|
||||
function Fixture() {
|
||||
const setState = useUpdateAtom(swapAtom)
|
||||
const {
|
||||
allowedSlippage,
|
||||
[Field.INPUT]: { usdc: inputUSDC },
|
||||
[Field.OUTPUT]: { usdc: outputUSDC },
|
||||
trade: { trade },
|
||||
slippage,
|
||||
impact,
|
||||
} = useSwapInfo()
|
||||
|
||||
useEffect(() => {
|
||||
@@ -37,7 +40,14 @@ function Fixture() {
|
||||
|
||||
return trade ? (
|
||||
<Modal color="dialog">
|
||||
<SummaryDialog onConfirm={() => void 0} trade={trade} allowedSlippage={allowedSlippage} />
|
||||
<SummaryDialog
|
||||
onConfirm={() => void 0}
|
||||
trade={trade}
|
||||
slippage={slippage}
|
||||
inputUSDC={inputUSDC}
|
||||
outputUSDC={outputUSDC}
|
||||
impact={impact}
|
||||
/>
|
||||
</Modal>
|
||||
) : null
|
||||
}
|
||||
|
||||
@@ -3,15 +3,14 @@ import { useLingui } from '@lingui/react'
|
||||
import { Trade } from '@uniswap/router-sdk'
|
||||
import { Currency, Percent, TradeType } from '@uniswap/sdk-core'
|
||||
import { useAtomValue } from 'jotai/utils'
|
||||
import { getSlippageWarning } from 'lib/hooks/useAllowedSlippage'
|
||||
import Column from 'lib/components/Column'
|
||||
import Row from 'lib/components/Row'
|
||||
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, getPriceImpactWarning } from 'utils/prices'
|
||||
|
||||
import Row from '../../Row'
|
||||
import { computeRealizedLPFeeAmount } from 'utils/prices'
|
||||
|
||||
const Value = styled.span<{ color?: Color }>`
|
||||
color: ${({ color, theme }) => color && theme[color]};
|
||||
@@ -26,7 +25,7 @@ interface DetailProps {
|
||||
|
||||
function Detail({ label, value, color }: DetailProps) {
|
||||
return (
|
||||
<ThemedText.Caption>
|
||||
<ThemedText.Caption userSelect>
|
||||
<Row gap={2}>
|
||||
<span>{label}</span>
|
||||
<Value color={color}>{value}</Value>
|
||||
@@ -37,16 +36,16 @@ function Detail({ label, value, color }: DetailProps) {
|
||||
|
||||
interface DetailsProps {
|
||||
trade: Trade<Currency, Currency, TradeType>
|
||||
allowedSlippage: Percent
|
||||
slippage: { auto: boolean; allowed: Percent; warning?: Color }
|
||||
priceImpact: { priceImpact?: string; warning?: Color }
|
||||
}
|
||||
|
||||
export default function Details({ trade, allowedSlippage }: DetailsProps) {
|
||||
export default function Details({ trade, slippage, priceImpact }: DetailsProps) {
|
||||
const { inputAmount, outputAmount } = trade
|
||||
const inputCurrency = inputAmount.currency
|
||||
const outputCurrency = outputAmount.currency
|
||||
const integrator = window.location.hostname
|
||||
const feeOptions = useAtomValue(feeOptionsAtom)
|
||||
const priceImpact = useMemo(() => computeRealizedPriceImpact(trade), [trade])
|
||||
const lpFeeAmount = useMemo(() => computeRealizedLPFeeAmount(trade), [trade])
|
||||
const { i18n } = useLingui()
|
||||
|
||||
@@ -62,7 +61,9 @@ export default function Details({ trade, allowedSlippage }: DetailsProps) {
|
||||
}
|
||||
}
|
||||
|
||||
rows.push([t`Price impact`, `${priceImpact.toFixed(2)}%`, getPriceImpactWarning(priceImpact)])
|
||||
if (priceImpact.priceImpact) {
|
||||
rows.push([t`Price impact`, priceImpact.priceImpact, priceImpact.warning])
|
||||
}
|
||||
|
||||
if (lpFeeAmount) {
|
||||
const parsedLpFee = formatCurrencyAmount(lpFeeAmount, 6, i18n.locale)
|
||||
@@ -70,16 +71,16 @@ export default function Details({ trade, allowedSlippage }: DetailsProps) {
|
||||
}
|
||||
|
||||
if (trade.tradeType === TradeType.EXACT_OUTPUT) {
|
||||
const localizedMaxSent = formatCurrencyAmount(trade.maximumAmountIn(allowedSlippage), 6, i18n.locale)
|
||||
const localizedMaxSent = formatCurrencyAmount(trade.maximumAmountIn(slippage.allowed), 6, i18n.locale)
|
||||
rows.push([t`Maximum sent`, `${localizedMaxSent} ${inputCurrency.symbol}`])
|
||||
}
|
||||
|
||||
if (trade.tradeType === TradeType.EXACT_INPUT) {
|
||||
const localizedMaxSent = formatCurrencyAmount(trade.minimumAmountOut(allowedSlippage), 6, i18n.locale)
|
||||
const localizedMaxSent = formatCurrencyAmount(trade.minimumAmountOut(slippage.allowed), 6, i18n.locale)
|
||||
rows.push([t`Minimum received`, `${localizedMaxSent} ${outputCurrency.symbol}`])
|
||||
}
|
||||
|
||||
rows.push([t`Slippage tolerance`, `${allowedSlippage.toFixed(2)}%`, getSlippageWarning(allowedSlippage)])
|
||||
rows.push([t`Slippage tolerance`, `${slippage.allowed.toFixed(2)}%`, slippage.warning])
|
||||
|
||||
return rows
|
||||
}, [
|
||||
@@ -87,7 +88,7 @@ export default function Details({ trade, allowedSlippage }: DetailsProps) {
|
||||
priceImpact,
|
||||
lpFeeAmount,
|
||||
trade,
|
||||
allowedSlippage,
|
||||
slippage,
|
||||
outputAmount,
|
||||
i18n.locale,
|
||||
integrator,
|
||||
@@ -96,10 +97,10 @@ export default function Details({ trade, allowedSlippage }: DetailsProps) {
|
||||
])
|
||||
|
||||
return (
|
||||
<>
|
||||
<Column gap={0.5}>
|
||||
{details.map(([label, detail, color]) => (
|
||||
<Detail key={label} label={label} value={detail} color={color} />
|
||||
))}
|
||||
</>
|
||||
</Column>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,54 +1,37 @@
|
||||
import { useLingui } from '@lingui/react'
|
||||
import { Currency, CurrencyAmount } from '@uniswap/sdk-core'
|
||||
import { useUSDCValue } from 'hooks/useUSDCPrice'
|
||||
import { PriceImpact } from 'lib/hooks/useUSDCPriceImpact'
|
||||
import { ArrowRight } from 'lib/icons'
|
||||
import styled from 'lib/theme'
|
||||
import { ThemedText } from 'lib/theme'
|
||||
import { useMemo } from 'react'
|
||||
import { computeFiatValuePriceImpact } from 'utils/computeFiatValuePriceImpact'
|
||||
import { PropsWithChildren } from 'react'
|
||||
import { formatCurrencyAmount } from 'utils/formatCurrencyAmount'
|
||||
|
||||
import Column from '../../Column'
|
||||
import Row from '../../Row'
|
||||
import TokenImg from '../../TokenImg'
|
||||
|
||||
const Percent = styled.span<{ gain: boolean }>`
|
||||
color: ${({ gain, theme }) => (gain ? theme.success : theme.error)};
|
||||
`
|
||||
|
||||
interface TokenValueProps {
|
||||
input: CurrencyAmount<Currency>
|
||||
usdc?: boolean
|
||||
change?: number
|
||||
usdc?: CurrencyAmount<Currency>
|
||||
}
|
||||
|
||||
function TokenValue({ input, usdc, change }: TokenValueProps) {
|
||||
function TokenValue({ input, usdc, children }: PropsWithChildren<TokenValueProps>) {
|
||||
const { i18n } = useLingui()
|
||||
const percent = useMemo(() => {
|
||||
if (change) {
|
||||
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.currency} />
|
||||
<ThemedText.Body2>
|
||||
<ThemedText.Body2 userSelect>
|
||||
{formatCurrencyAmount(input, 6, i18n.locale)} {input.currency.symbol}
|
||||
</ThemedText.Body2>
|
||||
</Row>
|
||||
{usdc && usdcAmount && (
|
||||
<Row justify="flex-start">
|
||||
<ThemedText.Caption color="secondary">
|
||||
${formatCurrencyAmount(usdcAmount, 2, i18n.locale)}
|
||||
{change && <Percent gain={change > 0}> {percent}</Percent>}
|
||||
</ThemedText.Caption>
|
||||
</Row>
|
||||
{usdc && (
|
||||
<ThemedText.Caption color="secondary" userSelect>
|
||||
<Row justify="flex-start" gap={0.25}>
|
||||
${formatCurrencyAmount(usdc, 6, 'en', 2)}
|
||||
{children}
|
||||
</Row>
|
||||
</ThemedText.Caption>
|
||||
)}
|
||||
</Column>
|
||||
)
|
||||
@@ -57,23 +40,19 @@ function TokenValue({ input, usdc, change }: TokenValueProps) {
|
||||
interface SummaryProps {
|
||||
input: CurrencyAmount<Currency>
|
||||
output: CurrencyAmount<Currency>
|
||||
usdc?: boolean
|
||||
inputUSDC?: CurrencyAmount<Currency>
|
||||
outputUSDC?: CurrencyAmount<Currency>
|
||||
priceImpact?: PriceImpact
|
||||
}
|
||||
|
||||
export default function Summary({ input, output, usdc }: SummaryProps) {
|
||||
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])
|
||||
|
||||
export default function Summary({ input, output, inputUSDC, outputUSDC, priceImpact }: SummaryProps) {
|
||||
return (
|
||||
<Row gap={usdc ? 1 : 0.25}>
|
||||
<TokenValue input={input} usdc={usdc} />
|
||||
<Row gap={priceImpact ? 1 : 0.25}>
|
||||
<TokenValue input={input} usdc={inputUSDC} />
|
||||
<ArrowRight />
|
||||
<TokenValue input={output} usdc={usdc} change={priceImpact} />
|
||||
<TokenValue input={output} usdc={outputUSDC}>
|
||||
{priceImpact && <ThemedText.Caption color={priceImpact.warning}>({priceImpact.display})</ThemedText.Caption>}
|
||||
</TokenValue>
|
||||
</Row>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,121 +1,116 @@
|
||||
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 { IconButton } from 'lib/components/Button'
|
||||
import { useSwapTradeType } from 'lib/hooks/swap'
|
||||
import { getSlippageWarning } from 'lib/hooks/useAllowedSlippage'
|
||||
import useScrollbar from 'lib/hooks/useScrollbar'
|
||||
import { AlertTriangle, BarChart, Expando, Info } from 'lib/icons'
|
||||
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, getPriceImpactWarning } from 'utils/prices'
|
||||
import { Currency, CurrencyAmount, TradeType } from '@uniswap/sdk-core'
|
||||
import ActionButton, { Action } from 'lib/components/ActionButton'
|
||||
import Column from 'lib/components/Column'
|
||||
import { Header } from 'lib/components/Dialog'
|
||||
import Expando from 'lib/components/Expando'
|
||||
import Row from 'lib/components/Row'
|
||||
import { Slippage } from 'lib/hooks/useSlippage'
|
||||
import { PriceImpact } from 'lib/hooks/useUSDCPriceImpact'
|
||||
import { AlertTriangle, BarChart, Info } from 'lib/icons'
|
||||
import styled, { Color, ThemedText } from 'lib/theme'
|
||||
import { useCallback, useMemo, useState } from 'react'
|
||||
import { formatCurrencyAmount } from 'utils/formatCurrencyAmount'
|
||||
import { tradeMeaningfullyDiffers } from 'utils/tradeMeaningFullyDiffer'
|
||||
|
||||
import ActionButton, { Action } from '../../ActionButton'
|
||||
import Column from '../../Column'
|
||||
import { Header } from '../../Dialog'
|
||||
import Row from '../../Row'
|
||||
import Rule from '../../Rule'
|
||||
import Price from '../Price'
|
||||
import Details from './Details'
|
||||
import Summary from './Summary'
|
||||
|
||||
export default Summary
|
||||
|
||||
const SummaryColumn = styled(Column)``
|
||||
const ExpandoColumn = styled(Column)``
|
||||
const DetailsColumn = styled(Column)``
|
||||
const Estimate = styled(ThemedText.Caption)``
|
||||
const Content = styled(Column)``
|
||||
const Heading = styled(Column)``
|
||||
const Footing = styled(Column)``
|
||||
const Body = styled(Column)<{ open: boolean }>`
|
||||
height: calc(100% - 2.5em);
|
||||
|
||||
${SummaryColumn} {
|
||||
flex-grow: ${({ open }) => (open ? 0 : 1)};
|
||||
${Content}, ${Heading} {
|
||||
flex-grow: 1;
|
||||
transition: flex-grow 0.25s;
|
||||
}
|
||||
|
||||
${ExpandoColumn} {
|
||||
flex-grow: ${({ open }) => (open ? 1 : 0)};
|
||||
transition: flex-grow 0.25s;
|
||||
|
||||
${DetailsColumn} {
|
||||
flex-basis: ${({ open }) => (open ? 7.5 : 0)}em;
|
||||
overflow-y: hidden;
|
||||
position: relative;
|
||||
transition: flex-basis 0.25s;
|
||||
|
||||
${Column} {
|
||||
height: 7.5em;
|
||||
grid-template-rows: repeat(auto-fill, 1em);
|
||||
padding: ${({ open }) => (open ? '0.5em 0' : 0)};
|
||||
transition: padding 0.25s;
|
||||
|
||||
:after {
|
||||
background: linear-gradient(#ffffff00, ${({ theme }) => theme.dialog});
|
||||
bottom: 0;
|
||||
content: '';
|
||||
height: 0.75em;
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
width: calc(100% - 1em);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
${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 }) =>
|
||||
open
|
||||
? 'max-height 0.1s ease-out, padding 0.25s ease-out'
|
||||
: 'flex-grow 0.25s ease-out, max-height 0.1s ease-in, padding 0.25s ease-out'};
|
||||
}
|
||||
${Footing} {
|
||||
margin-bottom: ${({ open }) => (open ? '-0.75em' : undefined)};
|
||||
max-height: ${({ open }) => (open ? 0 : '3em')};
|
||||
opacity: ${({ open }) => (open ? 0 : 1)};
|
||||
transition: max-height 0.25s, margin-bottom 0.25s, opacity 0.15s 0.1s;
|
||||
visibility: ${({ open }) => (open ? 'hidden' : undefined)};
|
||||
}
|
||||
`
|
||||
|
||||
interface SummaryDialogProps {
|
||||
trade: Trade<Currency, Currency, TradeType>
|
||||
allowedSlippage: Percent
|
||||
onConfirm: () => void
|
||||
function Subhead({ priceImpact, slippage }: { priceImpact: { warning?: Color }; slippage: Slippage }) {
|
||||
return (
|
||||
<Row gap={0.5}>
|
||||
{priceImpact.warning || slippage.warning ? (
|
||||
<AlertTriangle color={priceImpact.warning || slippage.warning} />
|
||||
) : (
|
||||
<Info color="secondary" />
|
||||
)}
|
||||
<ThemedText.Subhead2 color={priceImpact.warning || slippage.warning || 'secondary'}>
|
||||
{priceImpact.warning ? (
|
||||
<Trans>High price impact</Trans>
|
||||
) : slippage.warning ? (
|
||||
<Trans>High slippage</Trans>
|
||||
) : (
|
||||
<Trans>Swap details</Trans>
|
||||
)}
|
||||
</ThemedText.Subhead2>
|
||||
</Row>
|
||||
)
|
||||
}
|
||||
|
||||
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 tradeType = useSwapTradeType()
|
||||
function Estimate({ trade, slippage }: { trade: Trade<Currency, Currency, TradeType>; slippage: Slippage }) {
|
||||
const { i18n } = useLingui()
|
||||
const text = useMemo(() => {
|
||||
switch (trade.tradeType) {
|
||||
case TradeType.EXACT_INPUT:
|
||||
return (
|
||||
<Trans>
|
||||
Output is estimated. You will receive at least{' '}
|
||||
{formatCurrencyAmount(trade.minimumAmountOut(slippage.allowed), 6, i18n.locale)}{' '}
|
||||
{trade.outputAmount.currency.symbol} or the transaction will revert.
|
||||
</Trans>
|
||||
)
|
||||
case TradeType.EXACT_OUTPUT:
|
||||
return (
|
||||
<Trans>
|
||||
Output is estimated. You will send at most{' '}
|
||||
{formatCurrencyAmount(trade.maximumAmountIn(slippage.allowed), 6, i18n.locale)}{' '}
|
||||
{trade.inputAmount.currency.symbol} or the transaction will revert.
|
||||
</Trans>
|
||||
)
|
||||
}
|
||||
}, [i18n.locale, slippage.allowed, trade])
|
||||
return <ThemedText.Caption color="secondary">{text}</ThemedText.Caption>
|
||||
}
|
||||
|
||||
const [open, setOpen] = useState(false)
|
||||
const [details, setDetails] = useState<HTMLDivElement | null>(null)
|
||||
const scrollbar = useScrollbar(details)
|
||||
|
||||
const warning = useMemo(() => {
|
||||
return getPriceImpactWarning(priceImpact) || getSlippageWarning(allowedSlippage)
|
||||
}, [allowedSlippage, priceImpact])
|
||||
|
||||
function ConfirmButton({
|
||||
trade,
|
||||
highPriceImpact,
|
||||
onConfirm,
|
||||
}: {
|
||||
trade: Trade<Currency, Currency, TradeType>
|
||||
highPriceImpact: boolean
|
||||
onConfirm: () => void
|
||||
}) {
|
||||
const [ackPriceImpact, setAckPriceImpact] = useState(false)
|
||||
|
||||
const [confirmedTrade, setConfirmedTrade] = useState(trade)
|
||||
const [ackTrade, setAckTrade] = useState(trade)
|
||||
const doesTradeDiffer = useMemo(
|
||||
() => Boolean(trade && confirmedTrade && tradeMeaningfullyDiffers(trade, confirmedTrade)),
|
||||
[confirmedTrade, trade]
|
||||
() => Boolean(trade && ackTrade && tradeMeaningfullyDiffers(trade, ackTrade)),
|
||||
[ackTrade, trade]
|
||||
)
|
||||
|
||||
const action = useMemo((): Action | undefined => {
|
||||
if (doesTradeDiffer) {
|
||||
return {
|
||||
message: <Trans>Price updated</Trans>,
|
||||
icon: BarChart,
|
||||
onClick: () => setConfirmedTrade(trade),
|
||||
onClick: () => setAckTrade(trade),
|
||||
children: <Trans>Accept</Trans>,
|
||||
}
|
||||
} else if (getPriceImpactWarning(priceImpact) === 'error' && !ackPriceImpact) {
|
||||
} else if (highPriceImpact && !ackPriceImpact) {
|
||||
return {
|
||||
message: <Trans>High price impact</Trans>,
|
||||
onClick: () => setAckPriceImpact(true),
|
||||
@@ -123,60 +118,58 @@ export function SummaryDialog({ trade, allowedSlippage, onConfirm }: SummaryDial
|
||||
}
|
||||
}
|
||||
return
|
||||
}, [ackPriceImpact, doesTradeDiffer, priceImpact, trade])
|
||||
}, [ackPriceImpact, doesTradeDiffer, highPriceImpact, trade])
|
||||
|
||||
if (!(inputAmount && outputAmount && inputCurrency && outputCurrency)) {
|
||||
return null
|
||||
}
|
||||
return (
|
||||
<ActionButton onClick={onConfirm} action={action}>
|
||||
<Trans>Confirm swap</Trans>
|
||||
</ActionButton>
|
||||
)
|
||||
}
|
||||
|
||||
interface SummaryDialogProps {
|
||||
trade: Trade<Currency, Currency, TradeType>
|
||||
slippage: Slippage
|
||||
inputUSDC?: CurrencyAmount<Currency>
|
||||
outputUSDC?: CurrencyAmount<Currency>
|
||||
impact: PriceImpact
|
||||
onConfirm: () => void
|
||||
}
|
||||
|
||||
export function SummaryDialog({ trade, slippage, inputUSDC, outputUSDC, impact, onConfirm }: SummaryDialogProps) {
|
||||
const { inputAmount, outputAmount } = trade
|
||||
|
||||
const [open, setOpen] = useState(false)
|
||||
const onExpand = useCallback(() => setOpen((open) => !open), [])
|
||||
|
||||
return (
|
||||
<>
|
||||
<Header title={<Trans>Swap summary</Trans>} ruled />
|
||||
<Body flex align="stretch" gap={0.75} padded open={open}>
|
||||
<SummaryColumn gap={0.75} flex justify="center">
|
||||
<Summary input={inputAmount} output={outputAmount} usdc={true} />
|
||||
<ThemedText.Caption>
|
||||
{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}>
|
||||
{warning ? <AlertTriangle color={warning} /> : <Info color="secondary" />}
|
||||
<ThemedText.Subhead2 color="secondary">
|
||||
<Trans>Swap details</Trans>
|
||||
</ThemedText.Subhead2>
|
||||
</Row>
|
||||
<IconButton color="secondary" onClick={() => setOpen(!open)} icon={Expando} iconProps={{ open }} />
|
||||
</Row>
|
||||
<ExpandoColumn flex align="stretch">
|
||||
<Rule />
|
||||
<DetailsColumn>
|
||||
<Column gap={0.5} ref={setDetails} css={scrollbar}>
|
||||
<Details trade={trade} allowedSlippage={allowedSlippage} />
|
||||
</Column>
|
||||
</DetailsColumn>
|
||||
<Estimate color="secondary">
|
||||
<Trans>Output is estimated.</Trans>
|
||||
{tradeType === TradeType.EXACT_INPUT && (
|
||||
<Trans>
|
||||
You will receive at least{' '}
|
||||
{formatCurrencyAmount(trade.minimumAmountOut(allowedSlippage), 6, i18n.locale)} {outputCurrency.symbol}{' '}
|
||||
or the transaction will revert.
|
||||
</Trans>
|
||||
)}
|
||||
{tradeType === TradeType.EXACT_OUTPUT && (
|
||||
<Trans>
|
||||
You will send at most {formatCurrencyAmount(trade.maximumAmountIn(allowedSlippage), 6, i18n.locale)}{' '}
|
||||
{inputCurrency.symbol} or the transaction will revert.
|
||||
</Trans>
|
||||
)}
|
||||
</Estimate>
|
||||
<ActionButton onClick={onConfirm} action={action}>
|
||||
<Trans>Confirm swap</Trans>
|
||||
</ActionButton>
|
||||
</ExpandoColumn>
|
||||
<Body flex align="stretch" padded gap={0.75} open={open}>
|
||||
<Heading gap={0.75} flex justify="center">
|
||||
<Summary
|
||||
input={inputAmount}
|
||||
output={outputAmount}
|
||||
inputUSDC={inputUSDC}
|
||||
outputUSDC={outputUSDC}
|
||||
priceImpact={impact}
|
||||
/>
|
||||
<Price trade={trade} />
|
||||
</Heading>
|
||||
<Column gap={open ? 0 : 0.75} style={{ transition: 'gap 0.25s' }}>
|
||||
<Expando
|
||||
title={<Subhead priceImpact={impact} slippage={slippage} />}
|
||||
open={open}
|
||||
onExpand={onExpand}
|
||||
height={7.25}
|
||||
>
|
||||
<Details trade={trade} slippage={slippage} priceImpact={impact} />
|
||||
</Expando>
|
||||
<Footing>
|
||||
<Estimate trade={trade} slippage={slippage} />
|
||||
</Footing>
|
||||
<ConfirmButton trade={trade} highPriceImpact={impact.warning === 'error'} onConfirm={onConfirm} />
|
||||
</Column>
|
||||
</Body>
|
||||
</>
|
||||
)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { tokens } from '@uniswap/default-token-list'
|
||||
import { DAI, USDC } from 'constants/tokens'
|
||||
import { DAI, USDC_MAINNET } from 'constants/tokens'
|
||||
import { useUpdateAtom } from 'jotai/utils'
|
||||
import { useEffect } from 'react'
|
||||
import { useSelect, useValue } from 'react-cosmos/fixture'
|
||||
@@ -37,40 +37,36 @@ function Fixture() {
|
||||
defaultValue: FEE_RECIPIENT_OPTIONS[1],
|
||||
})
|
||||
|
||||
const optionsToAddressMap: Record<string, string> = {
|
||||
none: '',
|
||||
const optionsToAddressMap: Record<string, string | undefined> = {
|
||||
None: undefined,
|
||||
Native: 'NATIVE',
|
||||
DAI: DAI.address,
|
||||
USDC: USDC.address,
|
||||
USDC: USDC_MAINNET.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', {
|
||||
|
||||
const [defaultInputToken] = useSelect('defaultInputToken', {
|
||||
options: addressOptions,
|
||||
defaultValue: addressOptions[1],
|
||||
})
|
||||
const [defaultOutputAmount] = useSelect('defaultOutputAmount', {
|
||||
options: inputOptions,
|
||||
defaultValue: inputOptions[0],
|
||||
const [defaultInputAmount] = useValue('defaultInputAmount', { defaultValue: 1 })
|
||||
|
||||
const [defaultOutputToken] = useSelect('defaultOutputToken', {
|
||||
options: addressOptions,
|
||||
defaultValue: addressOptions[2],
|
||||
})
|
||||
const [defaultOutputAmount] = useValue('defaultOutputAmount', { defaultValue: 0 })
|
||||
|
||||
return (
|
||||
<Swap
|
||||
convenienceFee={convenienceFee}
|
||||
convenienceFeeRecipient={convenienceFeeRecipient}
|
||||
defaultInputAddress={optionsToAddressMap[defaultInput]}
|
||||
defaultInputTokenAddress={optionsToAddressMap[defaultInputToken]}
|
||||
defaultInputAmount={defaultInputAmount}
|
||||
defaultOutputAddress={optionsToAddressMap[defaultOutput]}
|
||||
defaultOutputTokenAddress={optionsToAddressMap[defaultOutputToken]}
|
||||
defaultOutputAmount={defaultOutputAmount}
|
||||
tokenList={tokens}
|
||||
onConnectWallet={() => console.log('onConnectWallet')} // this handler is included as a test of functionality, but only logs
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,30 +1,30 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import { Token } from '@uniswap/sdk-core'
|
||||
import { useERC20PermitFromTrade } from 'hooks/useERC20Permit'
|
||||
import { useUpdateAtom } from 'jotai/utils'
|
||||
import { useAtomValue, useUpdateAtom } from 'jotai/utils'
|
||||
import { WrapErrorText } from 'lib/components/Swap/WrapErrorText'
|
||||
import { useSwapCurrencyAmount, useSwapInfo, useSwapTradeType } from 'lib/hooks/swap'
|
||||
import useSwapApproval, {
|
||||
ApprovalState,
|
||||
import {
|
||||
ApproveOrPermitState,
|
||||
useApproveOrPermit,
|
||||
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 useWrapCallback, { WrapError, WrapType } from 'lib/hooks/swap/useWrapCallback'
|
||||
import { useAddTransaction, 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 } from 'lib/state/swap'
|
||||
import { Spinner } from 'lib/icons'
|
||||
import { displayTxHashAtom, feeOptionsAtom, Field } from 'lib/state/swap'
|
||||
import { TransactionType } from 'lib/state/transactions'
|
||||
import { useTheme } from 'lib/theme'
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import { memo, useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import invariant from 'tiny-invariant'
|
||||
import { ExplorerDataType } from 'utils/getExplorerLink'
|
||||
|
||||
import ActionButton, { ActionButtonProps } from '../ActionButton'
|
||||
import Dialog from '../Dialog'
|
||||
import EtherscanLink from '../EtherscanLink'
|
||||
import Row from '../Row'
|
||||
import { SummaryDialog } from './Summary'
|
||||
|
||||
interface SwapButtonProps {
|
||||
@@ -35,19 +35,24 @@ function useIsPendingApproval(token?: Token, spender?: string): boolean {
|
||||
return Boolean(usePendingApproval(token, spender))
|
||||
}
|
||||
|
||||
export default function SwapButton({ disabled }: SwapButtonProps) {
|
||||
export default memo(function SwapButton({ disabled }: SwapButtonProps) {
|
||||
const { account, chainId } = useActiveWeb3React()
|
||||
|
||||
const { tokenColorExtraction } = useTheme()
|
||||
|
||||
const {
|
||||
[Field.INPUT]: {
|
||||
currency: inputCurrency,
|
||||
amount: inputTradeCurrencyAmount,
|
||||
balance: inputCurrencyBalance,
|
||||
usdc: inputUSDC,
|
||||
},
|
||||
[Field.OUTPUT]: { amount: outputTradeCurrencyAmount, usdc: outputUSDC },
|
||||
trade,
|
||||
allowedSlippage,
|
||||
currencies: { [Field.INPUT]: inputCurrency },
|
||||
currencyBalances: { [Field.INPUT]: inputCurrencyBalance },
|
||||
currencyAmounts: { [Field.INPUT]: inputCurrencyAmount, [Field.OUTPUT]: outputCurrencyAmount },
|
||||
feeOptions,
|
||||
slippage,
|
||||
impact,
|
||||
} = useSwapInfo()
|
||||
const feeOptions = useAtomValue(feeOptionsAtom)
|
||||
|
||||
const tradeType = useSwapTradeType()
|
||||
|
||||
@@ -56,86 +61,113 @@ export default function SwapButton({ disabled }: SwapButtonProps) {
|
||||
setActiveTrade((activeTrade) => activeTrade && trade.trade)
|
||||
}, [trade])
|
||||
|
||||
// clear active trade on chain change
|
||||
useEffect(() => {
|
||||
setActiveTrade(undefined)
|
||||
}, [chainId])
|
||||
|
||||
// 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
|
||||
useSwapApprovalOptimizedTrade(trade.trade, slippage.allowed, useIsPendingApproval) || trade.trade
|
||||
|
||||
const approvalCurrencyAmount = useSwapCurrencyAmount(Field.INPUT)
|
||||
const [approval, getApproval] = useSwapApproval(
|
||||
|
||||
const { approvalState, signatureData, handleApproveOrPermit } = useApproveOrPermit(
|
||||
optimizedTrade,
|
||||
allowedSlippage,
|
||||
slippage.allowed,
|
||||
useIsPendingApproval,
|
||||
approvalCurrencyAmount
|
||||
)
|
||||
|
||||
const approvalHash = usePendingApproval(
|
||||
inputCurrency?.isToken ? inputCurrency : undefined,
|
||||
useSwapRouterAddress(optimizedTrade)
|
||||
)
|
||||
|
||||
const addTransaction = useAddTransaction()
|
||||
const addApprovalTransaction = useCallback(() => {
|
||||
getApproval().then((transaction) => {
|
||||
const onApprove = useCallback(() => {
|
||||
handleApproveOrPermit().then((transaction) => {
|
||||
if (transaction) {
|
||||
addTransaction({ type: TransactionType.APPROVAL, ...transaction })
|
||||
}
|
||||
})
|
||||
}, [addTransaction, getApproval])
|
||||
}, [addTransaction, handleApproveOrPermit])
|
||||
|
||||
const { type: wrapType, callback: wrapCallback, error: wrapError, loading: wrapLoading } = useWrapCallback()
|
||||
|
||||
const disableSwap = useMemo(
|
||||
() =>
|
||||
disabled ||
|
||||
!optimizedTrade ||
|
||||
!chainId ||
|
||||
wrapLoading ||
|
||||
(wrapType !== WrapType.NOT_APPLICABLE && wrapError) ||
|
||||
approvalState === ApproveOrPermitState.PENDING_SIGNATURE ||
|
||||
!(inputTradeCurrencyAmount && inputCurrencyBalance) ||
|
||||
inputCurrencyBalance.lessThan(inputTradeCurrencyAmount),
|
||||
[
|
||||
disabled,
|
||||
optimizedTrade,
|
||||
chainId,
|
||||
wrapLoading,
|
||||
wrapType,
|
||||
wrapError,
|
||||
approvalState,
|
||||
inputTradeCurrencyAmount,
|
||||
inputCurrencyBalance,
|
||||
]
|
||||
)
|
||||
|
||||
const actionProps = useMemo((): Partial<ActionButtonProps> | undefined => {
|
||||
if (!disabled && chainId) {
|
||||
if (approval === ApprovalState.NOT_APPROVED) {
|
||||
const currency = inputCurrency || approvalCurrencyAmount?.currency
|
||||
invariant(currency)
|
||||
return {
|
||||
action: {
|
||||
message: <Trans>Approve {currency.symbol} first</Trans>,
|
||||
onClick: addApprovalTransaction,
|
||||
children: <Trans>Approve</Trans>,
|
||||
},
|
||||
}
|
||||
} else if (approval === ApprovalState.PENDING) {
|
||||
return {
|
||||
disabled: true,
|
||||
action: {
|
||||
message: (
|
||||
<EtherscanLink type={ExplorerDataType.TRANSACTION} data={approvalHash}>
|
||||
<Row gap={0.25}>
|
||||
<Trans>
|
||||
Approval pending <Link />
|
||||
</Trans>
|
||||
</Row>
|
||||
</EtherscanLink>
|
||||
if (disableSwap) {
|
||||
return { disabled: true }
|
||||
}
|
||||
if (
|
||||
wrapType === WrapType.NOT_APPLICABLE &&
|
||||
(approvalState === ApproveOrPermitState.REQUIRES_APPROVAL ||
|
||||
approvalState === ApproveOrPermitState.REQUIRES_SIGNATURE)
|
||||
) {
|
||||
const currency = inputCurrency || approvalCurrencyAmount?.currency
|
||||
invariant(currency)
|
||||
return {
|
||||
action: {
|
||||
message:
|
||||
approvalState === ApproveOrPermitState.REQUIRES_SIGNATURE ? (
|
||||
<Trans>Allow {currency.symbol} first</Trans>
|
||||
) : (
|
||||
<Trans>Approve {currency.symbol} first</Trans>
|
||||
),
|
||||
icon: Spinner,
|
||||
onClick: addApprovalTransaction,
|
||||
children: <Trans>Approve</Trans>,
|
||||
},
|
||||
}
|
||||
} else if (inputCurrencyAmount && inputCurrencyBalance && !inputCurrencyBalance.lessThan(inputCurrencyAmount)) {
|
||||
return {}
|
||||
onClick: onApprove,
|
||||
children:
|
||||
approvalState === ApproveOrPermitState.REQUIRES_SIGNATURE ? <Trans>Allow</Trans> : <Trans>Approve</Trans>,
|
||||
},
|
||||
}
|
||||
}
|
||||
return { disabled: true }
|
||||
}, [
|
||||
addApprovalTransaction,
|
||||
approval,
|
||||
approvalCurrencyAmount?.currency,
|
||||
approvalHash,
|
||||
chainId,
|
||||
disabled,
|
||||
inputCurrency,
|
||||
inputCurrencyAmount,
|
||||
inputCurrencyBalance,
|
||||
])
|
||||
if (approvalState === ApproveOrPermitState.PENDING_APPROVAL) {
|
||||
return {
|
||||
disabled: true,
|
||||
action: {
|
||||
message: (
|
||||
<EtherscanLink type={ExplorerDataType.TRANSACTION} data={approvalHash}>
|
||||
<Trans>Approval pending</Trans>
|
||||
</EtherscanLink>
|
||||
),
|
||||
icon: Spinner,
|
||||
onClick: () => void 0, // @TODO: should not require an onclick
|
||||
children: <Trans>Approve</Trans>,
|
||||
},
|
||||
}
|
||||
}
|
||||
return {}
|
||||
}, [approvalCurrencyAmount?.currency, approvalHash, approvalState, disableSwap, inputCurrency, onApprove, wrapType])
|
||||
|
||||
const deadline = useTransactionDeadline()
|
||||
const { signatureData } = useERC20PermitFromTrade(optimizedTrade, allowedSlippage, deadline)
|
||||
|
||||
// the callback to execute the swap
|
||||
const { callback: swapCallback } = useSwapCallback({
|
||||
trade: optimizedTrade,
|
||||
allowedSlippage,
|
||||
allowedSlippage: slippage.allowed,
|
||||
recipientAddressOrName: account ?? null,
|
||||
signatureData,
|
||||
deadline,
|
||||
@@ -149,13 +181,13 @@ export default function SwapButton({ disabled }: SwapButtonProps) {
|
||||
swapCallback?.()
|
||||
.then((response) => {
|
||||
setDisplayTxHash(response.hash)
|
||||
invariant(inputCurrencyAmount && outputCurrencyAmount)
|
||||
invariant(inputTradeCurrencyAmount && outputTradeCurrencyAmount)
|
||||
addTransaction({
|
||||
response,
|
||||
type: TransactionType.SWAP,
|
||||
tradeType,
|
||||
inputCurrencyAmount,
|
||||
outputCurrencyAmount,
|
||||
inputCurrencyAmount: inputTradeCurrencyAmount,
|
||||
outputCurrencyAmount: outputTradeCurrencyAmount,
|
||||
})
|
||||
})
|
||||
.catch((error) => {
|
||||
@@ -165,22 +197,64 @@ export default function SwapButton({ disabled }: SwapButtonProps) {
|
||||
.finally(() => {
|
||||
setActiveTrade(undefined)
|
||||
})
|
||||
}, [addTransaction, inputCurrencyAmount, outputCurrencyAmount, setDisplayTxHash, swapCallback, tradeType])
|
||||
}, [addTransaction, inputTradeCurrencyAmount, outputTradeCurrencyAmount, setDisplayTxHash, swapCallback, tradeType])
|
||||
|
||||
const ButtonText = useCallback(() => {
|
||||
if ((wrapType === WrapType.WRAP || wrapType === WrapType.UNWRAP) && wrapError !== WrapError.NO_ERROR) {
|
||||
return <WrapErrorText wrapError={wrapError} />
|
||||
}
|
||||
switch (wrapType) {
|
||||
case WrapType.UNWRAP:
|
||||
return <Trans>Unwrap</Trans>
|
||||
case WrapType.WRAP:
|
||||
return <Trans>Wrap</Trans>
|
||||
case WrapType.NOT_APPLICABLE:
|
||||
default:
|
||||
return <Trans>Review swap</Trans>
|
||||
}
|
||||
}, [wrapError, wrapType])
|
||||
|
||||
const handleDialogClose = useCallback(() => {
|
||||
setActiveTrade(undefined)
|
||||
}, [])
|
||||
|
||||
const handleActionButtonClick = useCallback(async () => {
|
||||
if (wrapType === WrapType.NOT_APPLICABLE) {
|
||||
setActiveTrade(trade.trade)
|
||||
} else {
|
||||
const transaction = await wrapCallback()
|
||||
addTransaction({
|
||||
response: transaction,
|
||||
type: TransactionType.WRAP,
|
||||
unwrapped: wrapType === WrapType.UNWRAP,
|
||||
currencyAmountRaw: transaction.value?.toString() ?? '0',
|
||||
chainId,
|
||||
})
|
||||
setDisplayTxHash(transaction.hash)
|
||||
}
|
||||
}, [addTransaction, chainId, setDisplayTxHash, trade.trade, wrapCallback, wrapType])
|
||||
|
||||
return (
|
||||
<>
|
||||
<ActionButton
|
||||
color={tokenColorExtraction ? 'interactive' : 'accent'}
|
||||
onClick={() => setActiveTrade(trade.trade)}
|
||||
onClick={handleActionButtonClick}
|
||||
{...actionProps}
|
||||
>
|
||||
<Trans>Review swap</Trans>
|
||||
<ButtonText />
|
||||
</ActionButton>
|
||||
{activeTrade && (
|
||||
<Dialog color="dialog" onClose={() => setActiveTrade(undefined)}>
|
||||
<SummaryDialog trade={activeTrade} allowedSlippage={allowedSlippage} onConfirm={onConfirm} />
|
||||
<Dialog color="dialog" onClose={handleDialogClose}>
|
||||
<SummaryDialog
|
||||
trade={activeTrade}
|
||||
slippage={slippage}
|
||||
inputUSDC={inputUSDC}
|
||||
outputUSDC={outputUSDC}
|
||||
impact={impact}
|
||||
onConfirm={onConfirm}
|
||||
/>
|
||||
</Dialog>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { BigNumber } from '@ethersproject/bignumber'
|
||||
import { DefaultAddress, SwapProps } from 'lib/components/Swap'
|
||||
import { IntegrationError } from 'lib/errors'
|
||||
import { FeeOptions } from 'lib/hooks/swap/useSyncConvenienceFee'
|
||||
import { DefaultAddress, TokenDefaults } from 'lib/hooks/swap/useSyncTokenDefaults'
|
||||
import { PropsWithChildren, useEffect } from 'react'
|
||||
|
||||
import { isAddress } from '../../../utils'
|
||||
@@ -15,7 +16,7 @@ function isAddressOrAddressMap(addressOrMap: DefaultAddress): boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
type ValidatorProps = PropsWithChildren<SwapProps>
|
||||
type ValidatorProps = PropsWithChildren<TokenDefaults & FeeOptions>
|
||||
|
||||
export default function SwapPropValidator(props: ValidatorProps) {
|
||||
const { convenienceFee, convenienceFeeRecipient } = props
|
||||
@@ -47,7 +48,7 @@ export default function SwapPropValidator(props: ValidatorProps) {
|
||||
}
|
||||
}, [convenienceFee, convenienceFeeRecipient])
|
||||
|
||||
const { defaultInputAddress, defaultInputAmount, defaultOutputAddress, defaultOutputAmount } = props
|
||||
const { defaultInputTokenAddress, defaultInputAmount, defaultOutputTokenAddress, defaultOutputAmount } = props
|
||||
useEffect(() => {
|
||||
if (defaultOutputAmount && defaultInputAmount) {
|
||||
throw new IntegrationError('defaultInputAmount and defaultOutputAmount may not both be defined.')
|
||||
@@ -60,17 +61,25 @@ export default function SwapPropValidator(props: ValidatorProps) {
|
||||
`defaultOutputAmount must be a positive number. (You set it to ${defaultOutputAmount})`
|
||||
)
|
||||
}
|
||||
if (defaultInputAddress && !isAddressOrAddressMap(defaultInputAddress) && defaultInputAddress !== 'NATIVE') {
|
||||
if (
|
||||
defaultInputTokenAddress &&
|
||||
!isAddressOrAddressMap(defaultInputTokenAddress) &&
|
||||
defaultInputTokenAddress !== 'NATIVE'
|
||||
) {
|
||||
throw new IntegrationError(
|
||||
`defaultInputAddress(es) must be a valid address or "NATIVE". (You set it to ${defaultInputAddress}`
|
||||
`defaultInputTokenAddress(es) must be a valid address or "NATIVE". (You set it to ${defaultInputTokenAddress}`
|
||||
)
|
||||
}
|
||||
if (defaultOutputAddress && !isAddressOrAddressMap(defaultOutputAddress) && defaultOutputAddress !== 'NATIVE') {
|
||||
if (
|
||||
defaultOutputTokenAddress &&
|
||||
!isAddressOrAddressMap(defaultOutputTokenAddress) &&
|
||||
defaultOutputTokenAddress !== 'NATIVE'
|
||||
) {
|
||||
throw new IntegrationError(
|
||||
`defaultOutputAddress(es) must be a valid address or "NATIVE". (You set it to ${defaultOutputAddress}`
|
||||
`defaultOutputTokenAddress(es) must be a valid address or "NATIVE". (You set it to ${defaultOutputTokenAddress}`
|
||||
)
|
||||
}
|
||||
}, [defaultInputAddress, defaultInputAmount, defaultOutputAddress, defaultOutputAmount])
|
||||
}, [defaultInputTokenAddress, defaultInputAmount, defaultOutputTokenAddress, defaultOutputAmount])
|
||||
|
||||
return <>{props.children}</>
|
||||
}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import 'setimmediate'
|
||||
|
||||
import { Trans } from '@lingui/macro'
|
||||
import { Currency } from '@uniswap/sdk-core'
|
||||
import { loadingOpacityCss } from 'lib/css/loading'
|
||||
import { loadingTransitionCss } from 'lib/css/loading'
|
||||
import styled, { keyframes, ThemedText } from 'lib/theme'
|
||||
import { FocusEvent, ReactNode, useCallback, useRef, useState } from 'react'
|
||||
import { ReactNode, useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
|
||||
import Button from '../Button'
|
||||
import Column from '../Column'
|
||||
@@ -14,7 +16,7 @@ const TokenInputRow = styled(Row)`
|
||||
grid-template-columns: 1fr;
|
||||
`
|
||||
|
||||
const ValueInput = styled(DecimalInput)<{ $loading: boolean }>`
|
||||
const ValueInput = styled(DecimalInput)`
|
||||
color: ${({ theme }) => theme.primary};
|
||||
height: 1em;
|
||||
|
||||
@@ -26,7 +28,7 @@ const ValueInput = styled(DecimalInput)<{ $loading: boolean }>`
|
||||
color: ${({ theme }) => theme.onHover(theme.secondary)};
|
||||
}
|
||||
|
||||
${loadingOpacityCss}
|
||||
${loadingTransitionCss}
|
||||
`
|
||||
|
||||
const delayedFadeIn = keyframes`
|
||||
@@ -50,8 +52,8 @@ const MaxButton = styled(Button)`
|
||||
interface TokenInputProps {
|
||||
currency?: Currency
|
||||
amount: string
|
||||
max?: string
|
||||
disabled?: boolean
|
||||
onMax?: () => void
|
||||
onChangeInput: (input: string) => void
|
||||
onChangeCurrency: (currency: Currency) => void
|
||||
loading?: boolean
|
||||
@@ -61,47 +63,59 @@ interface TokenInputProps {
|
||||
export default function TokenInput({
|
||||
currency,
|
||||
amount,
|
||||
max,
|
||||
disabled,
|
||||
onMax,
|
||||
onChangeInput,
|
||||
onChangeCurrency,
|
||||
loading,
|
||||
children,
|
||||
}: TokenInputProps) {
|
||||
const max = useRef<HTMLButtonElement>(null)
|
||||
const [showMax, setShowMax] = useState(false)
|
||||
const onFocus = useCallback(() => setShowMax(Boolean(onMax)), [onMax])
|
||||
const onBlur = useCallback((e: FocusEvent) => {
|
||||
if (e.relatedTarget !== max.current && e.relatedTarget !== input.current) {
|
||||
setShowMax(false)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const input = useRef<HTMLInputElement>(null)
|
||||
const onSelect = useCallback(
|
||||
(currency: Currency) => {
|
||||
onChangeCurrency(currency)
|
||||
setTimeout(() => input.current?.focus(), 0)
|
||||
setImmediate(() => input.current?.focus())
|
||||
},
|
||||
[onChangeCurrency]
|
||||
)
|
||||
|
||||
const maxButton = useRef<HTMLButtonElement>(null)
|
||||
const hasMax = useMemo(() => Boolean(max && max !== amount), [max, amount])
|
||||
const [showMax, setShowMax] = useState<boolean>(hasMax)
|
||||
useEffect(() => setShowMax((hasMax && input.current?.contains(document.activeElement)) ?? false), [hasMax])
|
||||
const onBlur = useCallback((e) => {
|
||||
// Filters out clicks on input or maxButton, because onBlur fires before onClickMax.
|
||||
if (!input.current?.contains(e.relatedTarget) && !maxButton.current?.contains(e.relatedTarget)) {
|
||||
setShowMax(false)
|
||||
}
|
||||
}, [])
|
||||
const onClickMax = useCallback(() => {
|
||||
onChangeInput(max || '')
|
||||
setShowMax(false)
|
||||
setImmediate(() => {
|
||||
input.current?.focus()
|
||||
// Brings the start of the input into view. NB: This only works for clicks, not eg keyboard interactions.
|
||||
input.current?.setSelectionRange(0, null)
|
||||
})
|
||||
}, [max, onChangeInput])
|
||||
|
||||
return (
|
||||
<Column gap={0.25}>
|
||||
<TokenInputRow gap={0.5} onBlur={onBlur}>
|
||||
<ThemedText.H2>
|
||||
<ValueInput
|
||||
value={amount}
|
||||
onFocus={onFocus}
|
||||
onFocus={() => setShowMax(hasMax)}
|
||||
onChange={onChangeInput}
|
||||
disabled={disabled || !currency}
|
||||
$loading={Boolean(loading)}
|
||||
isLoading={Boolean(loading)}
|
||||
ref={input}
|
||||
></ValueInput>
|
||||
</ThemedText.H2>
|
||||
{showMax && (
|
||||
<MaxButton onClick={onMax} ref={max}>
|
||||
<ThemedText.ButtonMedium>
|
||||
<MaxButton onClick={onClickMax} ref={maxButton}>
|
||||
{/* Without a tab index, Safari would not populate the FocusEvent.relatedTarget needed by onBlur. */}
|
||||
<ThemedText.ButtonMedium tabIndex={-1}>
|
||||
<Trans>Max</Trans>
|
||||
</ThemedText.ButtonMedium>
|
||||
</MaxButton>
|
||||
|
||||
@@ -1,14 +1,23 @@
|
||||
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 { Currency, CurrencyAmount, TradeType } from '@uniswap/sdk-core'
|
||||
import Column from 'lib/components/Column'
|
||||
import Rule from 'lib/components/Rule'
|
||||
import Tooltip from 'lib/components/Tooltip'
|
||||
import { loadingCss } from 'lib/css/loading'
|
||||
import { WrapType } from 'lib/hooks/swap/useWrapCallback'
|
||||
import { PriceImpact } from 'lib/hooks/useUSDCPriceImpact'
|
||||
import { AlertTriangle, Icon, Info, InlineSpinner } from 'lib/icons'
|
||||
import styled, { ThemedText } from 'lib/theme'
|
||||
import { ReactNode, useCallback } from 'react'
|
||||
import { InterfaceTrade } from 'state/routing/types'
|
||||
|
||||
import { TextButton } from '../../Button'
|
||||
import Row from '../../Row'
|
||||
import RoutingTooltip from './RoutingTooltip'
|
||||
import Price from '../Price'
|
||||
import RoutingDiagram from '../RoutingDiagram'
|
||||
|
||||
const Loading = styled.span`
|
||||
color: ${({ theme }) => theme.secondary};
|
||||
${loadingCss};
|
||||
`
|
||||
|
||||
interface CaptionProps {
|
||||
icon?: Icon
|
||||
@@ -27,55 +36,72 @@ function Caption({ icon: Icon = AlertTriangle, caption }: CaptionProps) {
|
||||
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>} />
|
||||
return (
|
||||
<Caption
|
||||
icon={InlineSpinner}
|
||||
caption={
|
||||
<Loading>
|
||||
<Trans>Fetching best price…</Trans>
|
||||
</Loading>
|
||||
}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
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)
|
||||
export function WrapCurrency({ loading, wrapType }: { loading: boolean; wrapType: WrapType.UNWRAP | WrapType.WRAP }) {
|
||||
const WrapText = useCallback(() => {
|
||||
if (wrapType === WrapType.WRAP) {
|
||||
return loading ? <Trans>Wrapping native currency.</Trans> : <Trans>Wrap native currency.</Trans>
|
||||
}
|
||||
return loading ? <Trans>Unwrapping native currency.</Trans> : <Trans>Unwrap native currency.</Trans>
|
||||
}, [loading, wrapType])
|
||||
|
||||
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 <Caption icon={Info} caption={<WrapText />} />
|
||||
}
|
||||
|
||||
export function Trade({
|
||||
trade,
|
||||
outputUSDC,
|
||||
impact,
|
||||
}: {
|
||||
trade: InterfaceTrade<Currency, Currency, TradeType>
|
||||
outputUSDC?: CurrencyAmount<Currency>
|
||||
impact: PriceImpact
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
<RoutingTooltip />
|
||||
<TextButton color="primary" onClick={() => setFlip(!flip)}>
|
||||
{ratio}
|
||||
</TextButton>
|
||||
<Tooltip placement="bottom" icon={impact.warning ? AlertTriangle : Info}>
|
||||
<Column gap={0.75}>
|
||||
{impact.warning && (
|
||||
<>
|
||||
<ThemedText.Caption>
|
||||
The output amount is estimated at {impact.display} less than the input amount due to high price impact
|
||||
</ThemedText.Caption>
|
||||
<Rule />
|
||||
</>
|
||||
)}
|
||||
<RoutingDiagram trade={trade} />
|
||||
</Column>
|
||||
</Tooltip>
|
||||
<Price trade={trade} outputUSDC={outputUSDC} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
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>
|
||||
)
|
||||
*/
|
||||
}
|
||||
@@ -1,10 +1,11 @@
|
||||
import { ALL_SUPPORTED_CHAIN_IDS } from 'constants/chains'
|
||||
import { useIsAmountPopulated, useSwapInfo } from 'lib/hooks/swap'
|
||||
import useWrapCallback, { WrapType } from 'lib/hooks/swap/useWrapCallback'
|
||||
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 { memo, useMemo } from 'react'
|
||||
import { TradeState } from 'state/routing/types'
|
||||
|
||||
import Row from '../../Row'
|
||||
@@ -16,16 +17,17 @@ const ToolbarRow = styled(Row)`
|
||||
${largeIconCss}
|
||||
`
|
||||
|
||||
export default function Toolbar({ disabled }: { disabled?: boolean }) {
|
||||
export default memo(function Toolbar({ disabled }: { disabled?: boolean }) {
|
||||
const { chainId } = useActiveWeb3React()
|
||||
const {
|
||||
[Field.INPUT]: { currency: inputCurrency, balance },
|
||||
[Field.OUTPUT]: { currency: outputCurrency, usdc: outputUSDC },
|
||||
trade: { trade, state },
|
||||
currencies: { [Field.INPUT]: inputCurrency, [Field.OUTPUT]: outputCurrency },
|
||||
currencyBalances: { [Field.INPUT]: balance },
|
||||
impact,
|
||||
} = useSwapInfo()
|
||||
const isRouteLoading = state === TradeState.SYNCING || state === TradeState.LOADING
|
||||
const isAmountPopulated = useIsAmountPopulated()
|
||||
|
||||
const { type: wrapType, loading: wrapLoading } = useWrapCallback()
|
||||
const caption = useMemo(() => {
|
||||
if (disabled) {
|
||||
return <Caption.ConnectWallet />
|
||||
@@ -36,6 +38,9 @@ export default function Toolbar({ disabled }: { disabled?: boolean }) {
|
||||
}
|
||||
|
||||
if (inputCurrency && outputCurrency && isAmountPopulated) {
|
||||
if (wrapType !== WrapType.NOT_APPLICABLE) {
|
||||
return <Caption.WrapCurrency wrapType={wrapType} loading={wrapLoading} />
|
||||
}
|
||||
if (isRouteLoading) {
|
||||
return <Caption.LoadingTrade />
|
||||
}
|
||||
@@ -46,12 +51,25 @@ export default function Toolbar({ disabled }: { disabled?: boolean }) {
|
||||
return <Caption.InsufficientBalance currency={trade.inputAmount.currency} />
|
||||
}
|
||||
if (trade.inputAmount && trade.outputAmount) {
|
||||
return <Caption.Trade trade={trade} />
|
||||
return <Caption.Trade trade={trade} outputUSDC={outputUSDC} impact={impact} />
|
||||
}
|
||||
}
|
||||
|
||||
return <Caption.Empty />
|
||||
}, [balance, chainId, disabled, inputCurrency, isAmountPopulated, isRouteLoading, outputCurrency, trade])
|
||||
}, [
|
||||
balance,
|
||||
chainId,
|
||||
disabled,
|
||||
impact,
|
||||
inputCurrency,
|
||||
isAmountPopulated,
|
||||
isRouteLoading,
|
||||
outputCurrency,
|
||||
outputUSDC,
|
||||
trade,
|
||||
wrapLoading,
|
||||
wrapType,
|
||||
])
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -63,4 +81,4 @@ export default function Toolbar({ disabled }: { disabled?: boolean }) {
|
||||
</ThemedText.Caption>
|
||||
</>
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
22
src/lib/components/Swap/WrapErrorText.tsx
Normal file
@@ -0,0 +1,22 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import { WrapError } from 'lib/hooks/swap/useWrapCallback'
|
||||
import useNativeCurrency from 'lib/hooks/useNativeCurrency'
|
||||
|
||||
export function WrapErrorText({ wrapError }: { wrapError: WrapError }) {
|
||||
const native = useNativeCurrency()
|
||||
const wrapped = native?.wrapped
|
||||
|
||||
switch (wrapError) {
|
||||
case WrapError.ENTER_NATIVE_AMOUNT:
|
||||
return <Trans>Enter {native?.symbol} amount</Trans>
|
||||
case WrapError.ENTER_WRAPPED_AMOUNT:
|
||||
return <Trans>Enter {wrapped?.symbol} amount</Trans>
|
||||
case WrapError.INSUFFICIENT_NATIVE_BALANCE:
|
||||
return <Trans>Insufficient {native?.symbol} balance</Trans>
|
||||
case WrapError.INSUFFICIENT_WRAPPED_BALANCE:
|
||||
return <Trans>Insufficient {wrapped?.symbol} balance</Trans>
|
||||
case WrapError.NO_ERROR:
|
||||
default:
|
||||
return null
|
||||
}
|
||||
}
|
||||
@@ -1,16 +1,16 @@
|
||||
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 useSyncConvenienceFee, { FeeOptions } from 'lib/hooks/swap/useSyncConvenienceFee'
|
||||
import useSyncTokenDefaults, { TokenDefaults } from 'lib/hooks/swap/useSyncTokenDefaults'
|
||||
import { usePendingTransactions } from 'lib/hooks/transactions'
|
||||
import useActiveWeb3React from 'lib/hooks/useActiveWeb3React'
|
||||
import useHasFocus from 'lib/hooks/useHasFocus'
|
||||
import useOnSupportedNetwork from 'lib/hooks/useOnSupportedNetwork'
|
||||
import useTokenList, { useSyncTokenList } from 'lib/hooks/useTokenList'
|
||||
import { displayTxHashAtom } from 'lib/state/swap'
|
||||
import { SwapTransactionInfo, Transaction, TransactionType } from 'lib/state/transactions'
|
||||
import { SwapTransactionInfo, Transaction, TransactionType, WrapTransactionInfo } from 'lib/state/transactions'
|
||||
import { useMemo, useState } from 'react'
|
||||
|
||||
import Dialog from '../Dialog'
|
||||
@@ -26,63 +26,64 @@ import SwapButton from './SwapButton'
|
||||
import SwapPropValidator from './SwapPropValidator'
|
||||
import Toolbar from './Toolbar'
|
||||
|
||||
export type DefaultAddress = string | { [chainId: number]: string | 'NATIVE' } | 'NATIVE'
|
||||
|
||||
function getSwapTx(txs: { [hash: string]: Transaction }, hash?: string): Transaction<SwapTransactionInfo> | undefined {
|
||||
function getTransactionFromMap(
|
||||
txs: { [hash: string]: Transaction },
|
||||
hash?: string
|
||||
): Transaction<SwapTransactionInfo | WrapTransactionInfo> | undefined {
|
||||
if (hash) {
|
||||
const tx = txs[hash]
|
||||
if (tx?.info?.type === TransactionType.SWAP) {
|
||||
return tx as Transaction<SwapTransactionInfo>
|
||||
}
|
||||
if (tx?.info?.type === TransactionType.WRAP) {
|
||||
return tx as Transaction<WrapTransactionInfo>
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
export interface SwapProps {
|
||||
export interface SwapProps extends TokenDefaults, FeeOptions {
|
||||
tokenList?: string | TokenInfo[]
|
||||
defaultInputAddress?: DefaultAddress
|
||||
defaultInputAmount?: string
|
||||
defaultOutputAddress?: DefaultAddress
|
||||
defaultOutputAmount?: string
|
||||
convenienceFee?: number
|
||||
convenienceFeeRecipient?: string | { [chainId: number]: string }
|
||||
onConnectWallet?: () => void
|
||||
}
|
||||
|
||||
export default function Swap(props: SwapProps) {
|
||||
useSyncTokenList(props.tokenList)
|
||||
useSyncSwapDefaults(props)
|
||||
useSyncTokenDefaults(props)
|
||||
useSyncConvenienceFee(props)
|
||||
|
||||
const { active, account, chainId } = useActiveWeb3React()
|
||||
const { active, account } = useActiveWeb3React()
|
||||
const [wrapper, setWrapper] = useState<HTMLDivElement | null>(null)
|
||||
|
||||
const [displayTxHash, setDisplayTxHash] = useAtom(displayTxHashAtom)
|
||||
const pendingTxs = usePendingTransactions()
|
||||
const displayTx = getSwapTx(pendingTxs, displayTxHash)
|
||||
const displayTx = getTransactionFromMap(pendingTxs, displayTxHash)
|
||||
|
||||
const tokenList = useTokenList()
|
||||
const onSupportedNetwork = useOnSupportedNetwork()
|
||||
const isSwapSupported = useMemo(
|
||||
() => Boolean(chainId && ALL_SUPPORTED_CHAIN_IDS.includes(chainId) && tokenList?.length),
|
||||
[chainId, tokenList]
|
||||
() => Boolean(active && onSupportedNetwork && tokenList?.length),
|
||||
[active, onSupportedNetwork, tokenList?.length]
|
||||
)
|
||||
|
||||
const focused = useHasFocus(wrapper)
|
||||
|
||||
const isInteractive = Boolean(active && onSupportedNetwork)
|
||||
|
||||
return (
|
||||
<SwapPropValidator {...props}>
|
||||
{isSwapSupported && <SwapInfoUpdater />}
|
||||
<Header title={<Trans>Swap</Trans>}>
|
||||
{active && <Wallet disabled={!account} onClick={props.onConnectWallet} />}
|
||||
<Settings disabled={!active} />
|
||||
<Settings disabled={!isInteractive} />
|
||||
</Header>
|
||||
<div ref={setWrapper}>
|
||||
<BoundaryProvider value={wrapper}>
|
||||
<Input disabled={!active} focused={focused} />
|
||||
<ReverseButton disabled={!active} />
|
||||
<Output disabled={!active} focused={focused}>
|
||||
<Input disabled={!isInteractive} focused={focused} />
|
||||
<ReverseButton disabled={!isInteractive} />
|
||||
<Output disabled={!isInteractive} focused={focused}>
|
||||
<Toolbar disabled={!active} />
|
||||
<SwapButton disabled={!account} />
|
||||
<SwapButton disabled={!isSwapSupported} />
|
||||
</Output>
|
||||
</BoundaryProvider>
|
||||
</div>
|
||||
|
||||
@@ -1,18 +1,23 @@
|
||||
import { Currency } from '@uniswap/sdk-core'
|
||||
import { useToken } from 'lib/hooks/useCurrency'
|
||||
import useCurrencyLogoURIs from 'lib/hooks/useCurrencyLogoURIs'
|
||||
import { Slash } from 'lib/icons'
|
||||
import { MissingToken } from 'lib/icons'
|
||||
import styled from 'lib/theme'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
|
||||
const badSrcs = new Set<string>()
|
||||
|
||||
interface TokenImgProps {
|
||||
className?: string
|
||||
interface BaseProps {
|
||||
token: Currency
|
||||
}
|
||||
|
||||
function TokenImg({ className, token }: TokenImgProps) {
|
||||
const srcs = useCurrencyLogoURIs(token)
|
||||
type TokenImgProps = BaseProps & Omit<React.ImgHTMLAttributes<HTMLImageElement>, keyof BaseProps>
|
||||
|
||||
function TokenImg({ token, ...rest }: TokenImgProps) {
|
||||
// Use the wrapped token info so that it includes the logoURI.
|
||||
const tokenInfo = useToken(token.isToken ? token.wrapped.address : undefined) ?? token
|
||||
|
||||
const srcs = useCurrencyLogoURIs(tokenInfo)
|
||||
const [src, setSrc] = useState<string | undefined>()
|
||||
useEffect(() => {
|
||||
setSrc(srcs.find((src) => !badSrcs.has(src)))
|
||||
@@ -23,9 +28,9 @@ function TokenImg({ className, token }: TokenImgProps) {
|
||||
}, [src, srcs])
|
||||
|
||||
if (src) {
|
||||
return <img className={className} src={src} alt={token.name || token.symbol} onError={onError} />
|
||||
return <img src={src} alt={tokenInfo.name || tokenInfo.symbol} onError={onError} {...rest} />
|
||||
}
|
||||
return <Slash className={className} color="secondary" />
|
||||
return <MissingToken color="secondary" {...rest} />
|
||||
}
|
||||
|
||||
export default styled(TokenImg)<{ size?: number }>`
|
||||
|
||||
@@ -22,9 +22,14 @@ const StyledTokenButton = styled(Button)<{ empty?: boolean }>`
|
||||
|
||||
const TokenButtonRow = styled(Row)<{ collapsed: boolean }>`
|
||||
height: 1.2em;
|
||||
max-width: ${({ collapsed }) => (collapsed ? '1.2' : '8.2')}em;
|
||||
overflow-x: hidden;
|
||||
// max-width must have an absolute value in order to transition.
|
||||
max-width: ${({ collapsed }) => (collapsed ? '1.2em' : '12em')};
|
||||
overflow: hidden;
|
||||
transition: max-width 0.25s linear;
|
||||
|
||||
img {
|
||||
min-width: 1.2em;
|
||||
}
|
||||
`
|
||||
|
||||
interface TokenButtonProps {
|
||||
|
||||
@@ -73,7 +73,6 @@ const TokenBalance = styled.div<{ isLoading: boolean }>`
|
||||
background-color: ${({ theme, isLoading }) => isLoading && theme.secondary};
|
||||
border-radius: 0.25em;
|
||||
padding: 0.375em 0;
|
||||
width: 1.5em;
|
||||
`
|
||||
|
||||
function TokenOption({ index, value, style }: TokenOptionProps) {
|
||||
|
||||
@@ -5,7 +5,7 @@ import { useCurrencyBalances } from 'lib/hooks/useCurrencyBalance'
|
||||
import useNativeCurrency from 'lib/hooks/useNativeCurrency'
|
||||
import useTokenList, { useIsTokenListLoaded, useQueryCurrencies } from 'lib/hooks/useTokenList'
|
||||
import styled, { ThemedText } from 'lib/theme'
|
||||
import { ElementRef, useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { ElementRef, memo, useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { currencyId } from 'utils/currencyId'
|
||||
|
||||
import Column from '../Column'
|
||||
@@ -55,7 +55,7 @@ export function TokenSelectDialog({ value, onSelect }: TokenSelectDialogProps) {
|
||||
// Give the balance-less tokens a small block period to avoid layout thrashing from re-sorting.
|
||||
useEffect(() => {
|
||||
if (!isLoaded) {
|
||||
const timeout = setTimeout(() => setIsLoaded(true), 1500)
|
||||
const timeout = setTimeout(() => setIsLoaded(true), 250)
|
||||
return () => clearTimeout(timeout)
|
||||
}
|
||||
return
|
||||
@@ -97,7 +97,21 @@ export function TokenSelectDialog({ value, onSelect }: TokenSelectDialogProps) {
|
||||
)}
|
||||
<Rule padded />
|
||||
</Column>
|
||||
{isLoaded ? <TokenOptions tokens={tokens} onSelect={onSelect} ref={setOptions} /> : <TokenOptionsSkeleton />}
|
||||
{isLoaded ? (
|
||||
tokens.length ? (
|
||||
<TokenOptions tokens={tokens} onSelect={onSelect} ref={setOptions} />
|
||||
) : (
|
||||
<Column padded>
|
||||
<Row justify="center">
|
||||
<ThemedText.Body1 color="secondary">
|
||||
<Trans>No results found.</Trans>
|
||||
</ThemedText.Body1>
|
||||
</Row>
|
||||
</Column>
|
||||
)
|
||||
) : (
|
||||
<TokenOptionsSkeleton />
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -109,10 +123,11 @@ interface TokenSelectProps {
|
||||
onSelect: (value: Currency) => void
|
||||
}
|
||||
|
||||
export default function TokenSelect({ value, collapsed, disabled, onSelect }: TokenSelectProps) {
|
||||
export default memo(function TokenSelect({ value, collapsed, disabled, onSelect }: TokenSelectProps) {
|
||||
usePrefetchBalances()
|
||||
|
||||
const [open, setOpen] = useState(false)
|
||||
const onOpen = useCallback(() => setOpen(true), [])
|
||||
const selectAndClose = useCallback(
|
||||
(value: Currency) => {
|
||||
onSelect(value)
|
||||
@@ -122,7 +137,7 @@ export default function TokenSelect({ value, collapsed, disabled, onSelect }: To
|
||||
)
|
||||
return (
|
||||
<>
|
||||
<TokenButton value={value} collapsed={collapsed} disabled={disabled} onClick={() => setOpen(true)} />
|
||||
<TokenButton value={value} collapsed={collapsed} disabled={disabled} onClick={onOpen} />
|
||||
{open && (
|
||||
<Dialog color="module" onClose={() => setOpen(false)}>
|
||||
<TokenSelectDialog value={value} onSelect={selectAndClose} />
|
||||
@@ -130,4 +145,4 @@ export default function TokenSelect({ value, collapsed, disabled, onSelect }: To
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import { Wallet as WalletIcon } from 'lib/icons'
|
||||
import styled, { ThemedText } from 'lib/theme'
|
||||
import { ThemedText } from 'lib/theme'
|
||||
|
||||
import { TextButton } from './Button'
|
||||
import Row from './Row'
|
||||
|
||||
interface WalletProps {
|
||||
@@ -9,17 +10,15 @@ interface WalletProps {
|
||||
onClick?: () => void
|
||||
}
|
||||
|
||||
const ClickableRow = styled(Row)<{ onClick?: unknown }>`
|
||||
cursor: ${({ onClick }) => onClick && 'pointer'};
|
||||
`
|
||||
|
||||
export default function Wallet({ disabled, onClick }: WalletProps) {
|
||||
return disabled ? (
|
||||
<ThemedText.Caption color="secondary">
|
||||
<ClickableRow gap={0.5} onClick={onClick}>
|
||||
<WalletIcon />
|
||||
<Trans>Connect your wallet</Trans>
|
||||
</ClickableRow>
|
||||
</ThemedText.Caption>
|
||||
<TextButton disabled={!onClick} onClick={onClick} color="secondary" style={{ filter: 'none' }}>
|
||||
<ThemedText.Caption>
|
||||
<Row gap={0.5}>
|
||||
<WalletIcon />
|
||||
<Trans>Connect your wallet</Trans>
|
||||
</Row>
|
||||
</ThemedText.Caption>
|
||||
</TextButton>
|
||||
) : null
|
||||
}
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
import { initializeConnector, Web3ReactHooks } from '@web3-react/core'
|
||||
import { EIP1193 } from '@web3-react/eip1193'
|
||||
import { Actions, Connector, Provider as Eip1193Provider } from '@web3-react/types'
|
||||
import { Url } from '@web3-react/url'
|
||||
import { SetStateAction } from 'jotai'
|
||||
import { RESET, useUpdateAtom } from 'jotai/utils'
|
||||
import { injectedAtom, urlAtom } from 'lib/state/web3'
|
||||
import { ReactNode, useEffect } from 'react'
|
||||
|
||||
interface Web3ProviderProps {
|
||||
jsonRpcEndpoint?: string
|
||||
provider?: Eip1193Provider
|
||||
children: ReactNode
|
||||
}
|
||||
|
||||
function useConnector<T extends { new (actions: Actions, initializer: I): Connector }, I>(
|
||||
Connector: T,
|
||||
initializer: I | undefined,
|
||||
setContext: (update: typeof RESET | SetStateAction<[Connector, Web3ReactHooks]>) => void
|
||||
) {
|
||||
return useEffect(() => {
|
||||
if (initializer) {
|
||||
const [connector, hooks] = initializeConnector((actions) => new Connector(actions, initializer))
|
||||
setContext([connector, hooks])
|
||||
} else {
|
||||
setContext(RESET)
|
||||
}
|
||||
}, [Connector, initializer, setContext])
|
||||
}
|
||||
|
||||
export default function Web3Provider({ jsonRpcEndpoint, provider, children }: Web3ProviderProps) {
|
||||
const setUrl = useUpdateAtom(urlAtom)
|
||||
useConnector(Url, jsonRpcEndpoint, setUrl)
|
||||
|
||||
const setInjected = useUpdateAtom(injectedAtom)
|
||||
useConnector(EIP1193, provider, setInjected)
|
||||
|
||||
return <>{children}</>
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
import { Provider as EthersProvider } from '@ethersproject/abstract-provider'
|
||||
import { Signer as EthersSigner } from '@ethersproject/abstract-signer'
|
||||
import { Provider as Eip1193Provider } from '@web3-react/types'
|
||||
import { DEFAULT_LOCALE, SupportedLocale } from 'constants/locales'
|
||||
import { Provider as AtomProvider } from 'jotai'
|
||||
import { TransactionsUpdater } from 'lib/hooks/transactions'
|
||||
import { Web3Provider } from 'lib/hooks/useActiveWeb3React'
|
||||
import { BlockUpdater } from 'lib/hooks/useBlockNumber'
|
||||
import useEip1193Provider from 'lib/hooks/useEip1193Provider'
|
||||
import { UNMOUNTING } from 'lib/hooks/useUnmount'
|
||||
@@ -16,7 +16,6 @@ import { Provider as ReduxProvider } from 'react-redux'
|
||||
import { Modal, Provider as DialogProvider } from './Dialog'
|
||||
import ErrorBoundary, { ErrorHandler } from './Error/ErrorBoundary'
|
||||
import WidgetPropValidator from './Error/WidgetsPropsValidator'
|
||||
import Web3Provider from './Web3Provider'
|
||||
|
||||
const WidgetWrapper = styled.div<{ width?: number | string }>`
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
@@ -24,6 +23,7 @@ const WidgetWrapper = styled.div<{ width?: number | string }>`
|
||||
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
|
||||
background-color: ${({ theme }) => theme.container};
|
||||
border-radius: ${({ theme }) => theme.borderRadius}em;
|
||||
box-sizing: border-box;
|
||||
color: ${({ theme }) => theme.primary};
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -31,19 +31,19 @@ const WidgetWrapper = styled.div<{ width?: number | string }>`
|
||||
font-size: 16px;
|
||||
font-smooth: always;
|
||||
font-variant: none;
|
||||
height: 348px;
|
||||
height: 356px;
|
||||
min-width: 300px;
|
||||
padding: 0.25em;
|
||||
position: relative;
|
||||
user-select: none;
|
||||
width: ${({ width }) => width && (isNaN(Number(width)) ? width : `${width}px`)};
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
font-family: ${({ theme }) => theme.fontFamily};
|
||||
user-select: none;
|
||||
font-family: ${({ theme }) => (typeof theme.fontFamily === 'string' ? theme.fontFamily : theme.fontFamily.font)};
|
||||
|
||||
@supports (font-variation-settings: normal) {
|
||||
font-family: ${({ theme }) => theme.fontFamilyVariable};
|
||||
font-family: ${({ theme }) => (typeof theme.fontFamily === 'string' ? undefined : theme.fontFamily.variable)};
|
||||
}
|
||||
}
|
||||
`
|
||||
@@ -94,7 +94,7 @@ function Updaters() {
|
||||
export type WidgetProps = {
|
||||
theme?: Theme
|
||||
locale?: SupportedLocale
|
||||
provider?: Eip1193Provider | EthersProvider | { provider: EthersProvider; signer: EthersSigner }
|
||||
provider?: Eip1193Provider | EthersProvider
|
||||
jsonRpcEndpoint?: string
|
||||
width?: string | number
|
||||
dialog?: HTMLElement | null
|
||||
|
||||
@@ -1,14 +1,19 @@
|
||||
import { initializeConnector } from '@web3-react/core'
|
||||
import { MetaMask } from '@web3-react/metamask'
|
||||
import { Connector } from '@web3-react/types'
|
||||
import { WalletConnect } from '@web3-react/walletconnect'
|
||||
import { SupportedChainId } from 'constants/chains'
|
||||
import { INFURA_NETWORK_URLS } from 'constants/infura'
|
||||
import { DEFAULT_LOCALE, SUPPORTED_LOCALES } from 'constants/locales'
|
||||
import Widget from 'lib/components/Widget'
|
||||
import { darkTheme, defaultTheme, lightTheme } from 'lib/theme'
|
||||
import { ReactNode, useEffect, useMemo } from 'react'
|
||||
import { ReactNode, useEffect, useState } from 'react'
|
||||
import { useSelect, useValue } from 'react-cosmos/fixture'
|
||||
|
||||
export const [metaMask] = initializeConnector<MetaMask>((actions) => new MetaMask(actions))
|
||||
const [metaMask] = initializeConnector<MetaMask>((actions) => new MetaMask(actions))
|
||||
const [walletConnect] = initializeConnector<WalletConnect>(
|
||||
(actions) => new WalletConnect(actions, { rpc: INFURA_NETWORK_URLS })
|
||||
)
|
||||
|
||||
export default function Wrapper({ children }: { children: ReactNode }) {
|
||||
const [width] = useValue('width', { defaultValue: 360 })
|
||||
@@ -27,21 +32,40 @@ export default function Wrapper({ children }: { children: ReactNode }) {
|
||||
options: [NO_JSON_RPC, ...Object.values(INFURA_NETWORK_URLS).sort()],
|
||||
})
|
||||
|
||||
const NO_PROVIDER = 'None'
|
||||
const NO_CONNECTOR = 'None'
|
||||
const META_MASK = 'MetaMask'
|
||||
const [providerType] = useSelect('Provider', {
|
||||
defaultValue: NO_PROVIDER,
|
||||
options: [NO_PROVIDER, META_MASK],
|
||||
const WALLET_CONNECT = 'WalletConnect'
|
||||
const [connectorType] = useSelect('Provider', {
|
||||
defaultValue: NO_CONNECTOR,
|
||||
options: [NO_CONNECTOR, META_MASK, WALLET_CONNECT],
|
||||
})
|
||||
const provider = useMemo(() => {
|
||||
switch (providerType) {
|
||||
case META_MASK:
|
||||
metaMask.activate()
|
||||
return metaMask.provider
|
||||
default:
|
||||
return undefined
|
||||
const [connector, setConnector] = useState<Connector>()
|
||||
useEffect(() => {
|
||||
let stale = false
|
||||
activateConnector(connectorType)
|
||||
return () => {
|
||||
stale = true
|
||||
}
|
||||
}, [providerType])
|
||||
|
||||
async function activateConnector(connectorType: 'None' | 'MetaMask' | 'WalletConnect') {
|
||||
let connector: Connector
|
||||
switch (connectorType) {
|
||||
case META_MASK:
|
||||
await metaMask.activate()
|
||||
connector = metaMask
|
||||
break
|
||||
case WALLET_CONNECT:
|
||||
await walletConnect.activate()
|
||||
connector = walletConnect
|
||||
}
|
||||
if (!stale) {
|
||||
setConnector((oldConnector) => {
|
||||
oldConnector?.deactivate?.()
|
||||
return connector
|
||||
})
|
||||
}
|
||||
}
|
||||
}, [connectorType])
|
||||
|
||||
return (
|
||||
<Widget
|
||||
@@ -49,7 +73,7 @@ export default function Wrapper({ children }: { children: ReactNode }) {
|
||||
theme={theme}
|
||||
locale={locale}
|
||||
jsonRpcEndpoint={jsonRpcEndpoint === NO_JSON_RPC ? undefined : jsonRpcEndpoint}
|
||||
provider={provider}
|
||||
provider={connector?.provider}
|
||||
>
|
||||
{children}
|
||||
</Widget>
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
import { css } from 'lib/theme'
|
||||
|
||||
// need to use $loading as `loading` is a reserved prop
|
||||
export const loadingOpacityCss = css<{ $loading: boolean }>`
|
||||
filter: ${({ $loading }) => ($loading ? 'grayscale(1)' : 'none')};
|
||||
opacity: ${({ $loading }) => ($loading ? '0.4' : '1')};
|
||||
transition: opacity 0.2s ease-in-out;
|
||||
export const loadingOpacity = 0.6
|
||||
|
||||
export const loadingCss = css`
|
||||
filter: grayscale(1);
|
||||
opacity: ${loadingOpacity};
|
||||
`
|
||||
|
||||
// need to use isLoading as `loading` is a reserved prop
|
||||
export const loadingTransitionCss = css<{ isLoading: boolean }>`
|
||||
${({ isLoading }) => isLoading && loadingCss};
|
||||
transition: opacity ${({ isLoading }) => (isLoading ? 0 : 0.2)}s ease-in-out;
|
||||
`
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
import { BigintIsh, CurrencyAmount, Token, TradeType } from '@uniswap/sdk-core'
|
||||
import { AlphaRouter, AlphaRouterConfig, AlphaRouterParams, ChainId } from '@uniswap/smart-order-router'
|
||||
import JSBI from 'jsbi'
|
||||
import useBlockNumber from 'lib/hooks/useBlockNumber'
|
||||
import { GetQuoteResult } from 'state/routing/types'
|
||||
import { transformSwapRouteToGetQuoteResult } from 'utils/transformSwapRouteToGetQuoteResult'
|
||||
|
||||
export const AUTO_ROUTER_SUPPORTED_CHAINS: ChainId[] = Object.values(ChainId) as number[]
|
||||
export const AUTO_ROUTER_SUPPORTED_CHAINS: ChainId[] = Object.values(ChainId).filter((chainId): chainId is ChainId =>
|
||||
Number.isInteger(chainId)
|
||||
)
|
||||
|
||||
async function getQuote(
|
||||
{
|
||||
@@ -96,3 +99,14 @@ export async function getClientSideQuote(
|
||||
routerConfig
|
||||
)
|
||||
}
|
||||
|
||||
/** Used to keep quotes up to date given a certain block age. Returns undefined if past limit. */
|
||||
export function useFilterFreshQuote(
|
||||
quoteResult: GetQuoteResult | undefined,
|
||||
maxBlockAge = 10
|
||||
): GetQuoteResult | undefined {
|
||||
const block = useBlockNumber()
|
||||
if (!block || !quoteResult) return undefined
|
||||
if (block - (Number(quoteResult.blockNumber) || 0) > maxBlockAge) return undefined
|
||||
return quoteResult
|
||||
}
|
||||
|
||||
@@ -2,13 +2,16 @@ import { Protocol } from '@uniswap/router-sdk'
|
||||
import { Currency, CurrencyAmount, TradeType } from '@uniswap/sdk-core'
|
||||
import { ChainId } from '@uniswap/smart-order-router'
|
||||
import useDebounce from 'hooks/useDebounce'
|
||||
import useLast from 'hooks/useLast'
|
||||
import { useStablecoinAmountFromFiatValue } from 'hooks/useUSDCPrice'
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import { useCallback, useMemo } from 'react'
|
||||
import { GetQuoteResult, InterfaceTrade, TradeState } from 'state/routing/types'
|
||||
import { computeRoutes, transformRoutesToTrade } from 'state/routing/utils'
|
||||
|
||||
import useWrapCallback, { WrapType } from '../swap/useWrapCallback'
|
||||
import useActiveWeb3React from '../useActiveWeb3React'
|
||||
import { getClientSideQuote } from './clientSideSmartOrderRouter'
|
||||
import usePoll from '../usePoll'
|
||||
import { getClientSideQuote, useFilterFreshQuote } from './clientSideSmartOrderRouter'
|
||||
import { useRoutingAPIArguments } from './useRoutingAPIArguments'
|
||||
|
||||
/**
|
||||
@@ -22,14 +25,11 @@ const DistributionPercents: { [key: number]: number } = {
|
||||
[ChainId.ARBITRUM_ONE]: 25,
|
||||
[ChainId.ARBITRUM_RINKEBY]: 25,
|
||||
}
|
||||
|
||||
const DEFAULT_DISTRIBUTION_PERCENT = 10
|
||||
|
||||
function getConfig(chainId: ChainId | undefined) {
|
||||
return {
|
||||
// Limit to only V2 and V3.
|
||||
protocols: [Protocol.V2, Protocol.V3],
|
||||
|
||||
distributionPercent: (chainId && DistributionPercents[chainId]) ?? DEFAULT_DISTRIBUTION_PERCENT,
|
||||
}
|
||||
}
|
||||
@@ -42,22 +42,20 @@ export default function useClientSideSmartOrderRouterTrade<TTradeType extends Tr
|
||||
state: TradeState
|
||||
trade: InterfaceTrade<Currency, Currency, TTradeType> | undefined
|
||||
} {
|
||||
const amount = useMemo(() => amountSpecified?.asFraction, [amountSpecified])
|
||||
const [currencyIn, currencyOut] =
|
||||
tradeType === TradeType.EXACT_INPUT
|
||||
? [amountSpecified?.currency, otherCurrency]
|
||||
: [otherCurrency, amountSpecified?.currency]
|
||||
|
||||
// Debounce is used to prevent excessive requests to SOR, as it is data intensive.
|
||||
// This helps provide a "syncing" state the UI can reference for loading animations.
|
||||
const inputs = useMemo(() => [tradeType, amountSpecified, otherCurrency], [tradeType, amountSpecified, otherCurrency])
|
||||
const debouncedInputs = useDebounce(inputs, 200)
|
||||
const isDebouncing = inputs !== debouncedInputs
|
||||
|
||||
const chainId = amountSpecified?.currency.chainId
|
||||
const { library } = useActiveWeb3React()
|
||||
|
||||
const [currencyIn, currencyOut]: [Currency | undefined, Currency | undefined] = useMemo(
|
||||
() =>
|
||||
tradeType === TradeType.EXACT_INPUT
|
||||
? [amountSpecified?.currency, otherCurrency]
|
||||
: [otherCurrency, amountSpecified?.currency],
|
||||
[amountSpecified, otherCurrency, tradeType]
|
||||
// Fast user actions (ie updating the input) should be debounced, but currency changes should not.
|
||||
const [debouncedAmount, debouncedCurrencyIn, debouncedCurrencyOut] = useDebounce(
|
||||
useMemo(() => [amount, currencyIn, currencyOut], [amount, currencyIn, currencyOut]),
|
||||
200
|
||||
)
|
||||
const isDebouncing =
|
||||
amount !== debouncedAmount && currencyIn === debouncedCurrencyIn && currencyOut === debouncedCurrencyOut
|
||||
|
||||
const queryArgs = useRoutingAPIArguments({
|
||||
tokenIn: currencyIn,
|
||||
@@ -66,58 +64,53 @@ export default function useClientSideSmartOrderRouterTrade<TTradeType extends Tr
|
||||
tradeType,
|
||||
useClientSideRouter: true,
|
||||
})
|
||||
const chainId = amountSpecified?.currency.chainId
|
||||
const { library } = useActiveWeb3React()
|
||||
const params = useMemo(() => chainId && library && { chainId, provider: library }, [chainId, library])
|
||||
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [{ data: quoteResult, error }, setResult] = useState<{
|
||||
data?: GetQuoteResult
|
||||
error?: unknown
|
||||
}>({ error: undefined })
|
||||
const config = useMemo(() => getConfig(chainId), [chainId])
|
||||
const { type: wrapType } = useWrapCallback()
|
||||
|
||||
// When arguments update, make a new call to SOR for updated quote
|
||||
useEffect(() => {
|
||||
setLoading(true)
|
||||
if (isDebouncing) return
|
||||
|
||||
let stale = false
|
||||
fetchQuote()
|
||||
return () => {
|
||||
stale = true
|
||||
setLoading(false)
|
||||
const getQuoteResult = useCallback(async (): Promise<{ data?: GetQuoteResult; error?: unknown }> => {
|
||||
if (wrapType !== WrapType.NOT_APPLICABLE) return { error: undefined }
|
||||
if (!queryArgs || !params) return { error: undefined }
|
||||
try {
|
||||
return await getClientSideQuote(queryArgs, params, config)
|
||||
} catch {
|
||||
return { error: true }
|
||||
}
|
||||
}, [config, params, queryArgs, wrapType])
|
||||
|
||||
async function fetchQuote() {
|
||||
if (queryArgs && params) {
|
||||
let result
|
||||
try {
|
||||
result = await getClientSideQuote(queryArgs, params, config)
|
||||
} catch {
|
||||
result = { error: true }
|
||||
}
|
||||
if (!stale) {
|
||||
setResult(result)
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [queryArgs, params, config, isDebouncing])
|
||||
const { data, error } = usePoll(getQuoteResult, JSON.stringify(queryArgs), isDebouncing) ?? {
|
||||
error: undefined,
|
||||
}
|
||||
|
||||
const quoteResult = useFilterFreshQuote(data)
|
||||
const isLoading = !quoteResult
|
||||
|
||||
const route = useMemo(
|
||||
() => computeRoutes(currencyIn, currencyOut, tradeType, quoteResult),
|
||||
[currencyIn, currencyOut, quoteResult, tradeType]
|
||||
)
|
||||
const gasUseEstimateUSD = useStablecoinAmountFromFiatValue(quoteResult?.gasUseEstimateUSD) ?? null
|
||||
const trade = useMemo(() => {
|
||||
if (route) {
|
||||
try {
|
||||
return route && transformRoutesToTrade(route, tradeType, gasUseEstimateUSD)
|
||||
} catch (e: unknown) {
|
||||
console.debug('transformRoutesToTrade failed: ', e)
|
||||
}
|
||||
}
|
||||
return
|
||||
}, [gasUseEstimateUSD, route, tradeType])
|
||||
const trade =
|
||||
useLast(
|
||||
useMemo(() => {
|
||||
if (route) {
|
||||
try {
|
||||
return route && transformRoutesToTrade(route, tradeType, gasUseEstimateUSD)
|
||||
} catch (e: unknown) {
|
||||
console.debug('transformRoutesToTrade failed: ', e)
|
||||
}
|
||||
}
|
||||
return
|
||||
}, [gasUseEstimateUSD, route, tradeType]),
|
||||
Boolean
|
||||
) ?? undefined
|
||||
|
||||
// Dont return old trade if currencies dont match.
|
||||
const isStale =
|
||||
(currencyIn && !trade?.inputAmount?.currency.equals(currencyIn)) ||
|
||||
(currencyOut && !trade?.outputAmount?.currency.equals(currencyOut))
|
||||
|
||||
return useMemo(() => {
|
||||
if (!currencyIn || !currencyOut) {
|
||||
@@ -125,10 +118,14 @@ export default function useClientSideSmartOrderRouterTrade<TTradeType extends Tr
|
||||
}
|
||||
|
||||
// Returns the last trade state while syncing/loading to avoid jank from clearing the last trade while loading.
|
||||
if (isDebouncing) {
|
||||
return { state: TradeState.SYNCING, trade }
|
||||
} else if (loading) {
|
||||
return { state: TradeState.LOADING, trade }
|
||||
if (!quoteResult && !error) {
|
||||
if (isStale) {
|
||||
return { state: TradeState.LOADING, trade: undefined }
|
||||
} else if (isDebouncing) {
|
||||
return { state: TradeState.SYNCING, trade }
|
||||
} else if (isLoading) {
|
||||
return { state: TradeState.LOADING, trade }
|
||||
}
|
||||
}
|
||||
|
||||
let otherAmount = undefined
|
||||
@@ -151,5 +148,17 @@ export default function useClientSideSmartOrderRouterTrade<TTradeType extends Tr
|
||||
return { state: TradeState.VALID, trade }
|
||||
}
|
||||
return { state: TradeState.INVALID, trade: undefined }
|
||||
}, [currencyIn, currencyOut, isDebouncing, loading, quoteResult, error, route, queryArgs, trade, tradeType])
|
||||
}, [
|
||||
currencyIn,
|
||||
currencyOut,
|
||||
error,
|
||||
quoteResult,
|
||||
route,
|
||||
queryArgs,
|
||||
trade,
|
||||
isStale,
|
||||
isDebouncing,
|
||||
isLoading,
|
||||
tradeType,
|
||||
])
|
||||
}
|
||||
|
||||
@@ -83,7 +83,7 @@ export function useIsAmountPopulated() {
|
||||
export function useSwapAmount(field: Field): [string | undefined, (amount: string) => void] {
|
||||
const amount = useAtomValue(amountAtom)
|
||||
const isFieldIndependent = useIsSwapFieldIndependent(field)
|
||||
const value = useMemo(() => (isFieldIndependent ? amount : undefined), [amount, isFieldIndependent])
|
||||
const value = isFieldIndependent ? amount : undefined
|
||||
const updateSwap = useUpdateAtom(swapAtom)
|
||||
const updateAmount = useCallback(
|
||||
(amount: string) =>
|
||||
@@ -101,8 +101,9 @@ export function useSwapCurrencyAmount(field: Field): CurrencyAmount<Currency> |
|
||||
const isAmountPopulated = useIsAmountPopulated()
|
||||
const [swapAmount] = useSwapAmount(field)
|
||||
const [swapCurrency] = useSwapCurrency(field)
|
||||
const currencyAmount = useMemo(() => tryParseCurrencyAmount(swapAmount, swapCurrency), [swapAmount, swapCurrency])
|
||||
if (isFieldIndependent && isAmountPopulated) {
|
||||
return tryParseCurrencyAmount(swapAmount, swapCurrency)
|
||||
return currencyAmount
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@@ -4,7 +4,9 @@ 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 { SWAP_ROUTER_ADDRESSES, V2_ROUTER_ADDRESS, V3_ROUTER_ADDRESS } from 'constants/addresses'
|
||||
import useActiveWeb3React from 'hooks/useActiveWeb3React'
|
||||
import { useMemo } from 'react'
|
||||
import { useERC20PermitFromTrade, UseERC20PermitState } from 'hooks/useERC20Permit'
|
||||
import useTransactionDeadline from 'lib/hooks/useTransactionDeadline'
|
||||
import { useCallback, useMemo } from 'react'
|
||||
import invariant from 'tiny-invariant'
|
||||
import { getTxOptimizedSwapRouter, SwapRouterVersion } from 'utils/getTxOptimizedSwapRouter'
|
||||
|
||||
@@ -134,3 +136,78 @@ export function useSwapApprovalOptimizedTrade(
|
||||
}
|
||||
}, [trade, optimizedSwapRouter])
|
||||
}
|
||||
|
||||
export enum ApproveOrPermitState {
|
||||
REQUIRES_APPROVAL,
|
||||
PENDING_APPROVAL,
|
||||
REQUIRES_SIGNATURE,
|
||||
PENDING_SIGNATURE,
|
||||
APPROVED,
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all relevant statuses and callback functions for approvals.
|
||||
* Considers both standard approval and ERC20 permit.
|
||||
*/
|
||||
export const useApproveOrPermit = (
|
||||
trade:
|
||||
| V2Trade<Currency, Currency, TradeType>
|
||||
| V3Trade<Currency, Currency, TradeType>
|
||||
| Trade<Currency, Currency, TradeType>
|
||||
| undefined,
|
||||
allowedSlippage: Percent,
|
||||
useIsPendingApproval: (token?: Token, spender?: string) => boolean,
|
||||
amount?: CurrencyAmount<Currency> // defaults to trade.maximumAmountIn(allowedSlippage)
|
||||
) => {
|
||||
const deadline = useTransactionDeadline()
|
||||
|
||||
// Check approvals on ERC20 contract based on amount.
|
||||
const [approval, getApproval] = useSwapApproval(trade, allowedSlippage, useIsPendingApproval, amount)
|
||||
|
||||
// Check status of permit and whether token supports it.
|
||||
const {
|
||||
state: signatureState,
|
||||
signatureData,
|
||||
gatherPermitSignature,
|
||||
} = useERC20PermitFromTrade(trade, allowedSlippage, deadline)
|
||||
|
||||
const notApproved = approval === ApprovalState.NOT_APPROVED && !(signatureState === UseERC20PermitState.SIGNED)
|
||||
|
||||
// If permit is supported, trigger a signature, if not create approval transaction.
|
||||
const handleApproveOrPermit = useCallback(async () => {
|
||||
if (signatureState === UseERC20PermitState.NOT_SIGNED && gatherPermitSignature) {
|
||||
try {
|
||||
return await gatherPermitSignature()
|
||||
} catch (error) {
|
||||
// Try to approve if gatherPermitSignature failed for any reason other than the user rejecting it.
|
||||
if (error?.code !== 4001) {
|
||||
return getApproval()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return getApproval()
|
||||
}
|
||||
}, [signatureState, gatherPermitSignature, getApproval])
|
||||
|
||||
const approvalState = useMemo(() => {
|
||||
if (approval === ApprovalState.PENDING) {
|
||||
return ApproveOrPermitState.PENDING_APPROVAL
|
||||
}
|
||||
if (signatureState === UseERC20PermitState.LOADING) {
|
||||
return ApproveOrPermitState.PENDING_SIGNATURE
|
||||
}
|
||||
if (notApproved && Boolean(gatherPermitSignature)) {
|
||||
return ApproveOrPermitState.REQUIRES_SIGNATURE
|
||||
}
|
||||
if (notApproved) {
|
||||
return ApproveOrPermitState.REQUIRES_APPROVAL
|
||||
}
|
||||
return ApproveOrPermitState.APPROVED
|
||||
}, [approval, gatherPermitSignature, notApproved, signatureState])
|
||||
|
||||
return {
|
||||
approvalState,
|
||||
signatureData,
|
||||
handleApproveOrPermit,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,151 +1,104 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import { Currency, CurrencyAmount, Percent, TradeType } from '@uniswap/sdk-core'
|
||||
import { FeeOptions } from '@uniswap/v3-sdk'
|
||||
import { atom } from 'jotai'
|
||||
import { useAtomValue, useUpdateAtom } from 'jotai/utils'
|
||||
import { useCurrencyBalances } from 'lib/hooks/useCurrencyBalance'
|
||||
import { feeOptionsAtom, Field, swapAtom } from 'lib/state/swap'
|
||||
import { Field, swapAtom } from 'lib/state/swap'
|
||||
import tryParseCurrencyAmount from 'lib/utils/tryParseCurrencyAmount'
|
||||
import { ReactNode, useEffect, useMemo } from 'react'
|
||||
import { useEffect, useMemo } from 'react'
|
||||
import { InterfaceTrade, TradeState } from 'state/routing/types'
|
||||
|
||||
import { isAddress } from '../../../utils'
|
||||
import useActiveWeb3React from '../useActiveWeb3React'
|
||||
import useAllowedSlippage from '../useAllowedSlippage'
|
||||
import useSlippage, { Slippage } from '../useSlippage'
|
||||
import useUSDCPriceImpact, { PriceImpact } from '../useUSDCPriceImpact'
|
||||
import { useBestTrade } from './useBestTrade'
|
||||
import useWrapCallback, { WrapType } from './useWrapCallback'
|
||||
|
||||
interface SwapField {
|
||||
currency?: Currency
|
||||
amount?: CurrencyAmount<Currency>
|
||||
balance?: CurrencyAmount<Currency>
|
||||
usdc?: CurrencyAmount<Currency>
|
||||
}
|
||||
|
||||
interface SwapInfo {
|
||||
currencies: { [field in Field]?: Currency }
|
||||
currencyBalances: { [field in Field]?: CurrencyAmount<Currency> }
|
||||
currencyAmounts: { [field in Field]?: CurrencyAmount<Currency> }
|
||||
[Field.INPUT]: SwapField
|
||||
[Field.OUTPUT]: SwapField
|
||||
trade: {
|
||||
trade?: InterfaceTrade<Currency, Currency, TradeType>
|
||||
state: TradeState
|
||||
}
|
||||
allowedSlippage: Percent
|
||||
feeOptions: FeeOptions | undefined
|
||||
}
|
||||
|
||||
const BAD_RECIPIENT_ADDRESSES: { [address: string]: true } = {
|
||||
'0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f': true, // v2 factory
|
||||
'0xf164fC0Ec4E93095b804a4795bBe1e041497b92a': true, // v2 router 01
|
||||
'0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D': true, // v2 router 02
|
||||
slippage: Slippage
|
||||
impact: PriceImpact
|
||||
}
|
||||
|
||||
// from the current swap inputs, compute the best trade and return it.
|
||||
function useComputeSwapInfo(): SwapInfo {
|
||||
const { account } = useActiveWeb3React()
|
||||
const { type: wrapType } = useWrapCallback()
|
||||
const isWrapping = wrapType === WrapType.WRAP || wrapType === WrapType.UNWRAP
|
||||
const { independentField, amount, [Field.INPUT]: currencyIn, [Field.OUTPUT]: currencyOut } = useAtomValue(swapAtom)
|
||||
const isExactIn = independentField === Field.INPUT
|
||||
|
||||
const {
|
||||
independentField,
|
||||
amount,
|
||||
[Field.INPUT]: inputCurrency,
|
||||
[Field.OUTPUT]: outputCurrency,
|
||||
} = useAtomValue(swapAtom)
|
||||
|
||||
const feeOptions = useAtomValue(feeOptionsAtom)
|
||||
|
||||
const to = account
|
||||
|
||||
const relevantTokenBalances = useCurrencyBalances(
|
||||
account,
|
||||
useMemo(() => [inputCurrency ?? undefined, outputCurrency ?? undefined], [inputCurrency, outputCurrency])
|
||||
)
|
||||
|
||||
const isExactIn: boolean = independentField === Field.INPUT
|
||||
const parsedAmount = useMemo(
|
||||
() => tryParseCurrencyAmount(amount, (isExactIn ? inputCurrency : outputCurrency) ?? undefined),
|
||||
[inputCurrency, isExactIn, outputCurrency, amount]
|
||||
() => tryParseCurrencyAmount(amount, (isExactIn ? currencyIn : currencyOut) ?? undefined),
|
||||
[amount, isExactIn, currencyIn, currencyOut]
|
||||
)
|
||||
|
||||
//@TODO(ianlapham): this would eventually be replaced with routing api logic.
|
||||
const trade = useBestTrade(
|
||||
isExactIn ? TradeType.EXACT_INPUT : TradeType.EXACT_OUTPUT,
|
||||
parsedAmount,
|
||||
(isExactIn ? outputCurrency : inputCurrency) ?? undefined
|
||||
(isExactIn ? currencyOut : currencyIn) ?? undefined
|
||||
)
|
||||
|
||||
const currencies = useMemo(
|
||||
() => ({
|
||||
[Field.INPUT]: inputCurrency ?? undefined,
|
||||
[Field.OUTPUT]: outputCurrency ?? undefined,
|
||||
}),
|
||||
[inputCurrency, outputCurrency]
|
||||
const amountIn = useMemo(
|
||||
() => (isWrapping || isExactIn ? parsedAmount : trade.trade?.inputAmount),
|
||||
[isExactIn, isWrapping, parsedAmount, trade.trade?.inputAmount]
|
||||
)
|
||||
const amountOut = useMemo(
|
||||
() => (isWrapping || !isExactIn ? parsedAmount : trade.trade?.outputAmount),
|
||||
[isExactIn, isWrapping, parsedAmount, trade.trade?.outputAmount]
|
||||
)
|
||||
const [balanceIn, balanceOut] = useCurrencyBalances(
|
||||
account,
|
||||
useMemo(() => [currencyIn, currencyOut], [currencyIn, currencyOut])
|
||||
)
|
||||
|
||||
const currencyBalances = useMemo(
|
||||
() => ({
|
||||
[Field.INPUT]: relevantTokenBalances[0],
|
||||
[Field.OUTPUT]: relevantTokenBalances[1],
|
||||
}),
|
||||
[relevantTokenBalances]
|
||||
)
|
||||
|
||||
const currencyAmounts = useMemo(
|
||||
() => ({
|
||||
[Field.INPUT]: trade.trade?.inputAmount,
|
||||
[Field.OUTPUT]: trade.trade?.outputAmount,
|
||||
}),
|
||||
[trade.trade?.inputAmount, trade.trade?.outputAmount]
|
||||
)
|
||||
|
||||
const allowedSlippage = useAllowedSlippage(trade.trade)
|
||||
|
||||
const inputError = useMemo(() => {
|
||||
let inputError: ReactNode | undefined
|
||||
|
||||
if (!account) {
|
||||
inputError = <Trans>Connect Wallet</Trans>
|
||||
}
|
||||
|
||||
if (!currencies[Field.INPUT] || !currencies[Field.OUTPUT]) {
|
||||
inputError = inputError ?? <Trans>Select a token</Trans>
|
||||
}
|
||||
|
||||
if (!parsedAmount) {
|
||||
inputError = inputError ?? <Trans>Enter an amount</Trans>
|
||||
}
|
||||
|
||||
const formattedTo = isAddress(to)
|
||||
if (!to || !formattedTo) {
|
||||
inputError = inputError ?? <Trans>Enter a recipient</Trans>
|
||||
} else {
|
||||
if (BAD_RECIPIENT_ADDRESSES[formattedTo]) {
|
||||
inputError = inputError ?? <Trans>Invalid recipient</Trans>
|
||||
}
|
||||
}
|
||||
|
||||
// compare input balance to max input based on version
|
||||
const [balanceIn, amountIn] = [currencyBalances[Field.INPUT], trade.trade?.maximumAmountIn(allowedSlippage)]
|
||||
|
||||
if (balanceIn && amountIn && balanceIn.lessThan(amountIn)) {
|
||||
inputError = <Trans>Insufficient {amountIn.currency.symbol} balance</Trans>
|
||||
}
|
||||
|
||||
return inputError
|
||||
}, [account, allowedSlippage, currencies, currencyBalances, parsedAmount, to, trade.trade])
|
||||
// Compute slippage and impact off of the trade so that it refreshes with the trade.
|
||||
// (Using amountIn/amountOut would show (incorrect) intermediate values.)
|
||||
const slippage = useSlippage(trade.trade)
|
||||
const {
|
||||
inputUSDC: usdcIn,
|
||||
outputUSDC: usdcOut,
|
||||
priceImpact: impact,
|
||||
} = useUSDCPriceImpact(trade.trade?.inputAmount, trade.trade?.outputAmount)
|
||||
|
||||
return useMemo(
|
||||
() => ({
|
||||
currencies,
|
||||
currencyBalances,
|
||||
currencyAmounts,
|
||||
inputError,
|
||||
[Field.INPUT]: {
|
||||
currency: currencyIn,
|
||||
amount: amountIn,
|
||||
balance: balanceIn,
|
||||
usdc: usdcIn,
|
||||
},
|
||||
[Field.OUTPUT]: {
|
||||
currency: currencyOut,
|
||||
amount: amountOut,
|
||||
balance: balanceOut,
|
||||
usdc: usdcOut,
|
||||
},
|
||||
trade,
|
||||
allowedSlippage,
|
||||
feeOptions,
|
||||
slippage,
|
||||
impact,
|
||||
}),
|
||||
[currencies, currencyBalances, currencyAmounts, inputError, trade, allowedSlippage, feeOptions]
|
||||
[amountIn, amountOut, balanceIn, balanceOut, currencyIn, currencyOut, impact, slippage, trade, usdcIn, usdcOut]
|
||||
)
|
||||
}
|
||||
|
||||
const swapInfoAtom = atom<SwapInfo>({
|
||||
currencies: {},
|
||||
currencyBalances: {},
|
||||
currencyAmounts: {},
|
||||
[Field.INPUT]: {},
|
||||
[Field.OUTPUT]: {},
|
||||
trade: { state: TradeState.INVALID },
|
||||
allowedSlippage: new Percent(0),
|
||||
feeOptions: undefined,
|
||||
slippage: { auto: true, allowed: new Percent(0) },
|
||||
impact: {},
|
||||
})
|
||||
|
||||
export function SwapInfoUpdater() {
|
||||
|
||||
@@ -4,12 +4,12 @@ import { useUpdateAtom } from 'jotai/utils'
|
||||
import { feeOptionsAtom } from 'lib/state/swap'
|
||||
import { useEffect } from 'react'
|
||||
|
||||
interface FeeOptionsArgs {
|
||||
export interface FeeOptions {
|
||||
convenienceFee?: number
|
||||
convenienceFeeRecipient?: string | string | { [chainId: number]: string }
|
||||
}
|
||||
|
||||
export default function useSyncConvenienceFee({ convenienceFee, convenienceFeeRecipient }: FeeOptionsArgs) {
|
||||
export default function useSyncConvenienceFee({ convenienceFee, convenienceFeeRecipient }: FeeOptions) {
|
||||
const { chainId } = useActiveWeb3React()
|
||||
const updateFeeOptions = useUpdateAtom(feeOptionsAtom)
|
||||
|
||||
|
||||
@@ -1,70 +0,0 @@
|
||||
import { Currency } from '@uniswap/sdk-core'
|
||||
import { nativeOnChain } from 'constants/tokens'
|
||||
import { useUpdateAtom } from 'jotai/utils'
|
||||
import { DefaultAddress } from 'lib/components/Swap'
|
||||
import useActiveWeb3React from 'lib/hooks/useActiveWeb3React'
|
||||
import { useToken } from 'lib/hooks/useCurrency'
|
||||
import { Field, Swap, swapAtom } from 'lib/state/swap'
|
||||
import { useCallback, useLayoutEffect, useState } from 'react'
|
||||
|
||||
function useDefaultToken(
|
||||
defaultAddress: DefaultAddress | undefined,
|
||||
chainId: number | undefined
|
||||
): Currency | null | undefined {
|
||||
let address = undefined
|
||||
if (typeof defaultAddress === 'string') {
|
||||
address = defaultAddress
|
||||
} else if (typeof defaultAddress === 'object' && chainId) {
|
||||
address = defaultAddress[chainId]
|
||||
}
|
||||
const token = useToken(address)
|
||||
if (chainId && address === 'NATIVE') {
|
||||
return nativeOnChain(chainId)
|
||||
}
|
||||
return token
|
||||
}
|
||||
|
||||
interface UseSwapDefaultsArgs {
|
||||
defaultInputAddress?: DefaultAddress
|
||||
defaultInputAmount?: string
|
||||
defaultOutputAddress?: DefaultAddress
|
||||
defaultOutputAmount?: string
|
||||
}
|
||||
|
||||
export default function useSyncSwapDefaults({
|
||||
defaultInputAddress,
|
||||
defaultInputAmount,
|
||||
defaultOutputAddress,
|
||||
defaultOutputAmount,
|
||||
}: UseSwapDefaultsArgs) {
|
||||
const updateSwap = useUpdateAtom(swapAtom)
|
||||
const { chainId } = useActiveWeb3React()
|
||||
const defaultInputToken = useDefaultToken(defaultInputAddress, chainId)
|
||||
const defaultOutputToken = useDefaultToken(defaultOutputAddress, chainId)
|
||||
|
||||
const setToDefaults = useCallback(() => {
|
||||
const defaultSwapState: Swap = {
|
||||
amount: '',
|
||||
[Field.INPUT]: defaultInputToken || undefined,
|
||||
[Field.OUTPUT]: defaultOutputToken || undefined,
|
||||
independentField: Field.INPUT,
|
||||
}
|
||||
if (defaultInputAmount && defaultInputToken) {
|
||||
defaultSwapState.amount = defaultInputAmount
|
||||
} else if (defaultOutputAmount && defaultOutputToken) {
|
||||
defaultSwapState.independentField = Field.OUTPUT
|
||||
defaultSwapState.amount = defaultOutputAmount
|
||||
}
|
||||
updateSwap((swap) => ({ ...swap, ...defaultSwapState }))
|
||||
}, [defaultInputToken, defaultOutputToken, defaultInputAmount, defaultOutputAmount, updateSwap])
|
||||
|
||||
const [previousChainId, setPreviousChainId] = useState(chainId)
|
||||
useLayoutEffect(() => {
|
||||
setPreviousChainId(chainId)
|
||||
}, [chainId])
|
||||
useLayoutEffect(() => {
|
||||
if (chainId && chainId !== previousChainId) {
|
||||
setToDefaults()
|
||||
}
|
||||
}, [chainId, previousChainId, setToDefaults])
|
||||
}
|
||||
83
src/lib/hooks/swap/useSyncTokenDefaults.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
import { Currency } from '@uniswap/sdk-core'
|
||||
import { nativeOnChain } from 'constants/tokens'
|
||||
import { useUpdateAtom } from 'jotai/utils'
|
||||
import useActiveWeb3React from 'lib/hooks/useActiveWeb3React'
|
||||
import { useToken } from 'lib/hooks/useCurrency'
|
||||
import useNativeCurrency from 'lib/hooks/useNativeCurrency'
|
||||
import { Field, Swap, swapAtom } from 'lib/state/swap'
|
||||
import { useCallback, useLayoutEffect, useState } from 'react'
|
||||
|
||||
import useOnSupportedNetwork from '../useOnSupportedNetwork'
|
||||
|
||||
export type DefaultAddress = string | { [chainId: number]: string | 'NATIVE' } | 'NATIVE'
|
||||
|
||||
export interface TokenDefaults {
|
||||
defaultInputTokenAddress?: DefaultAddress
|
||||
defaultInputAmount?: number | string
|
||||
defaultOutputTokenAddress?: DefaultAddress
|
||||
defaultOutputAmount?: number | string
|
||||
}
|
||||
|
||||
function useDefaultToken(
|
||||
defaultAddress: DefaultAddress | undefined,
|
||||
chainId: number | undefined
|
||||
): Currency | undefined {
|
||||
let address = undefined
|
||||
if (typeof defaultAddress === 'string') {
|
||||
address = defaultAddress
|
||||
} else if (typeof defaultAddress === 'object' && chainId) {
|
||||
address = defaultAddress[chainId]
|
||||
}
|
||||
const token = useToken(address)
|
||||
|
||||
const onSupportedNetwork = useOnSupportedNetwork()
|
||||
|
||||
// Only use native currency if chain ID is in supported chains. ExtendedEther will error otherwise.
|
||||
if (chainId && address === 'NATIVE' && onSupportedNetwork) {
|
||||
return nativeOnChain(chainId)
|
||||
}
|
||||
return token ?? undefined
|
||||
}
|
||||
|
||||
export default function useSyncTokenDefaults({
|
||||
defaultInputTokenAddress,
|
||||
defaultInputAmount,
|
||||
defaultOutputTokenAddress,
|
||||
defaultOutputAmount,
|
||||
}: TokenDefaults) {
|
||||
const updateSwap = useUpdateAtom(swapAtom)
|
||||
const { chainId } = useActiveWeb3React()
|
||||
const onSupportedNetwork = useOnSupportedNetwork()
|
||||
const nativeCurrency = useNativeCurrency()
|
||||
const defaultOutputToken = useDefaultToken(defaultOutputTokenAddress, chainId)
|
||||
const defaultInputToken =
|
||||
useDefaultToken(defaultInputTokenAddress, chainId) ??
|
||||
// Default the input token to the native currency if it is not the output token.
|
||||
(defaultOutputToken !== nativeCurrency && onSupportedNetwork ? nativeCurrency : undefined)
|
||||
|
||||
const setToDefaults = useCallback(() => {
|
||||
const defaultSwapState: Swap = {
|
||||
amount: '',
|
||||
[Field.INPUT]: defaultInputToken,
|
||||
[Field.OUTPUT]: defaultOutputToken,
|
||||
independentField: Field.INPUT,
|
||||
}
|
||||
if (defaultInputToken && defaultInputAmount) {
|
||||
defaultSwapState.amount = defaultInputAmount.toString()
|
||||
} else if (defaultOutputToken && defaultOutputAmount) {
|
||||
defaultSwapState.independentField = Field.OUTPUT
|
||||
defaultSwapState.amount = defaultOutputAmount.toString()
|
||||
}
|
||||
updateSwap((swap) => ({ ...swap, ...defaultSwapState }))
|
||||
}, [defaultInputAmount, defaultInputToken, defaultOutputAmount, defaultOutputToken, updateSwap])
|
||||
|
||||
const [previousChainId, setPreviousChainId] = useState(chainId)
|
||||
useLayoutEffect(() => {
|
||||
setPreviousChainId(chainId)
|
||||
}, [chainId])
|
||||
useLayoutEffect(() => {
|
||||
if (chainId && chainId !== previousChainId) {
|
||||
setToDefaults()
|
||||
}
|
||||
}, [chainId, previousChainId, setToDefaults])
|
||||
}
|
||||
133
src/lib/hooks/swap/useWrapCallback.tsx
Normal file
@@ -0,0 +1,133 @@
|
||||
import { ContractTransaction } from '@ethersproject/contracts'
|
||||
import { useWETHContract } from 'hooks/useContract'
|
||||
import { atom, useAtom } from 'jotai'
|
||||
import { useAtomValue } from 'jotai/utils'
|
||||
import { Field, swapAtom } from 'lib/state/swap'
|
||||
import tryParseCurrencyAmount from 'lib/utils/tryParseCurrencyAmount'
|
||||
import { useCallback, useEffect, useMemo } from 'react'
|
||||
|
||||
import { WRAPPED_NATIVE_CURRENCY } from '../../../constants/tokens'
|
||||
import useActiveWeb3React from '../useActiveWeb3React'
|
||||
import { useCurrencyBalances } from '../useCurrencyBalance'
|
||||
|
||||
export enum WrapType {
|
||||
NOT_APPLICABLE,
|
||||
WRAP,
|
||||
UNWRAP,
|
||||
}
|
||||
interface UseWrapCallbackReturns {
|
||||
callback: () => Promise<ContractTransaction>
|
||||
error: WrapError
|
||||
loading: boolean
|
||||
type: WrapType
|
||||
}
|
||||
|
||||
export enum WrapError {
|
||||
NO_ERROR = 0, // must be equal to 0 so all other errors are truthy
|
||||
ENTER_NATIVE_AMOUNT,
|
||||
ENTER_WRAPPED_AMOUNT,
|
||||
INSUFFICIENT_NATIVE_BALANCE,
|
||||
INSUFFICIENT_WRAPPED_BALANCE,
|
||||
}
|
||||
|
||||
interface WrapState {
|
||||
loading: boolean
|
||||
error: WrapError
|
||||
}
|
||||
|
||||
const wrapState = atom<WrapState>({
|
||||
loading: false,
|
||||
error: WrapError.NO_ERROR,
|
||||
})
|
||||
|
||||
export default function useWrapCallback(): UseWrapCallbackReturns {
|
||||
const { account, chainId } = useActiveWeb3React()
|
||||
const [{ loading, error }, setWrapState] = useAtom(wrapState)
|
||||
const wrappedNativeCurrencyContract = useWETHContract()
|
||||
const { amount, [Field.INPUT]: inputCurrency, [Field.OUTPUT]: outputCurrency } = useAtomValue(swapAtom)
|
||||
|
||||
const wrapType = useMemo(() => {
|
||||
if (!inputCurrency || !outputCurrency || !chainId) {
|
||||
return WrapType.NOT_APPLICABLE
|
||||
}
|
||||
if (inputCurrency.isNative && WRAPPED_NATIVE_CURRENCY[chainId]?.equals(outputCurrency)) {
|
||||
return WrapType.WRAP
|
||||
}
|
||||
if (WRAPPED_NATIVE_CURRENCY[chainId]?.equals(inputCurrency) && outputCurrency.isNative) {
|
||||
return WrapType.UNWRAP
|
||||
}
|
||||
return WrapType.NOT_APPLICABLE
|
||||
}, [chainId, inputCurrency, outputCurrency])
|
||||
|
||||
const parsedAmountIn = useMemo(
|
||||
() => tryParseCurrencyAmount(amount, inputCurrency ?? undefined),
|
||||
[inputCurrency, amount]
|
||||
)
|
||||
|
||||
const relevantTokenBalances = useCurrencyBalances(
|
||||
account,
|
||||
useMemo(() => [inputCurrency ?? undefined, outputCurrency ?? undefined], [inputCurrency, outputCurrency])
|
||||
)
|
||||
const currencyBalances = useMemo(
|
||||
() => ({
|
||||
[Field.INPUT]: relevantTokenBalances[0],
|
||||
[Field.OUTPUT]: relevantTokenBalances[1],
|
||||
}),
|
||||
[relevantTokenBalances]
|
||||
)
|
||||
|
||||
const hasInputAmount = Boolean(parsedAmountIn?.greaterThan('0'))
|
||||
const sufficientBalance = parsedAmountIn && !currencyBalances[Field.INPUT]?.lessThan(parsedAmountIn)
|
||||
|
||||
useEffect(() => {
|
||||
if (sufficientBalance) {
|
||||
setWrapState((state) => ({ ...state, error: WrapError.NO_ERROR }))
|
||||
} else if (wrapType === WrapType.WRAP) {
|
||||
setWrapState((state) => ({
|
||||
...state,
|
||||
error: hasInputAmount ? WrapError.INSUFFICIENT_NATIVE_BALANCE : WrapError.ENTER_NATIVE_AMOUNT,
|
||||
}))
|
||||
} else if (wrapType === WrapType.UNWRAP) {
|
||||
setWrapState((state) => ({
|
||||
...state,
|
||||
error: hasInputAmount ? WrapError.INSUFFICIENT_WRAPPED_BALANCE : WrapError.ENTER_WRAPPED_AMOUNT,
|
||||
}))
|
||||
}
|
||||
}, [hasInputAmount, setWrapState, sufficientBalance, wrapType])
|
||||
|
||||
const callback = useCallback(async () => {
|
||||
if (!parsedAmountIn) {
|
||||
return Promise.reject('Must provide an input amount to wrap.')
|
||||
}
|
||||
if (wrapType === WrapType.NOT_APPLICABLE) {
|
||||
return Promise.reject('Wrapping not applicable to this asset.')
|
||||
}
|
||||
if (!sufficientBalance) {
|
||||
return Promise.reject('Insufficient balance to wrap desired amount.')
|
||||
}
|
||||
if (!wrappedNativeCurrencyContract) {
|
||||
return Promise.reject('Wrap contract not found.')
|
||||
}
|
||||
setWrapState((state) => ({ ...state, loading: true }))
|
||||
const result = await (wrapType === WrapType.WRAP
|
||||
? wrappedNativeCurrencyContract.deposit({ value: `0x${parsedAmountIn.quotient.toString(16)}` })
|
||||
: wrappedNativeCurrencyContract.withdraw(`0x${parsedAmountIn.quotient.toString(16)}`)
|
||||
).catch((e: unknown) => {
|
||||
setWrapState((state) => ({ ...state, loading: false }))
|
||||
throw e
|
||||
})
|
||||
// resolve loading state after one confirmation
|
||||
result.wait(1).finally(() => setWrapState((state) => ({ ...state, loading: false })))
|
||||
return Promise.resolve(result)
|
||||
}, [wrappedNativeCurrencyContract, sufficientBalance, parsedAmountIn, wrapType, setWrapState])
|
||||
|
||||
return useMemo(
|
||||
() => ({
|
||||
callback,
|
||||
error,
|
||||
loading,
|
||||
type: wrapType,
|
||||
}),
|
||||
[callback, error, loading, wrapType]
|
||||
)
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
import { Web3ReactHooks } from '@web3-react/core'
|
||||
import { useAtomValue } from 'jotai/utils'
|
||||
import { injectedAtom, urlAtom, Web3ReactState } from 'lib/state/web3'
|
||||
|
||||
export function useActiveWeb3ReactState(): Web3ReactState {
|
||||
const injected = useAtomValue(injectedAtom)
|
||||
const url = useAtomValue(urlAtom)
|
||||
return injected[1].useIsActive() ? injected : url
|
||||
}
|
||||
|
||||
export function useActiveWeb3ReactHooks(): Web3ReactHooks {
|
||||
const [, hooks] = useActiveWeb3ReactState()
|
||||
return hooks
|
||||
}
|
||||
|
||||
export default function useActiveWeb3React() {
|
||||
const { useProvider, useWeb3React } = useActiveWeb3ReactHooks()
|
||||
return useWeb3React(useProvider())
|
||||
}
|
||||
63
src/lib/hooks/useActiveWeb3React.tsx
Normal file
@@ -0,0 +1,63 @@
|
||||
import { getPriorityConnector, initializeConnector, Web3ReactHooks } from '@web3-react/core'
|
||||
import { EIP1193 } from '@web3-react/eip1193'
|
||||
import { EMPTY } from '@web3-react/empty'
|
||||
import { Actions, Connector, Provider as Eip1193Provider } from '@web3-react/types'
|
||||
import { Url } from '@web3-react/url'
|
||||
import { useAtom, WritableAtom } from 'jotai'
|
||||
import { useAtomValue } from 'jotai/utils'
|
||||
import { atomWithDefault, RESET, useUpdateAtom } from 'jotai/utils'
|
||||
import { PropsWithChildren, useEffect } from 'react'
|
||||
|
||||
const [connector, hooks] = initializeConnector(() => EMPTY)
|
||||
const EMPTY_CONNECTOR: [Connector, Web3ReactHooks] = [connector, hooks]
|
||||
const urlConnectorAtom = atomWithDefault<[Connector, Web3ReactHooks]>(() => EMPTY_CONNECTOR)
|
||||
const injectedConnectorAtom = atomWithDefault<[Connector, Web3ReactHooks]>(() => EMPTY_CONNECTOR)
|
||||
const web3Atom = atomWithDefault<ReturnType<typeof hooks.useWeb3React>>(() => ({
|
||||
connector: EMPTY_CONNECTOR[0],
|
||||
library: undefined,
|
||||
chainId: undefined,
|
||||
account: undefined,
|
||||
active: false,
|
||||
error: undefined,
|
||||
}))
|
||||
|
||||
export default function useActiveWeb3React() {
|
||||
return useAtomValue(web3Atom)
|
||||
}
|
||||
|
||||
function useConnector<T extends { new (actions: Actions, initializer: I): Connector }, I>(
|
||||
connectorAtom: WritableAtom<[Connector, Web3ReactHooks], typeof RESET | [Connector, Web3ReactHooks]>,
|
||||
Connector: T,
|
||||
initializer: I | undefined
|
||||
) {
|
||||
const [connector, setConnector] = useAtom(connectorAtom)
|
||||
useEffect(() => {
|
||||
if (initializer) {
|
||||
const [connector, hooks] = initializeConnector((actions) => new Connector(actions, initializer))
|
||||
connector.activate()
|
||||
setConnector([connector, hooks])
|
||||
} else {
|
||||
setConnector(RESET)
|
||||
}
|
||||
}, [Connector, initializer, setConnector])
|
||||
return connector
|
||||
}
|
||||
|
||||
interface Web3ProviderProps {
|
||||
provider?: Eip1193Provider
|
||||
jsonRpcEndpoint?: string
|
||||
}
|
||||
|
||||
export function Web3Provider({ provider, jsonRpcEndpoint, children }: PropsWithChildren<Web3ProviderProps>) {
|
||||
const injectedConnector = useConnector(injectedConnectorAtom, EIP1193, provider)
|
||||
const urlConnector = useConnector(urlConnectorAtom, Url, jsonRpcEndpoint)
|
||||
const priorityConnector = getPriorityConnector(injectedConnector, urlConnector)
|
||||
const priorityProvider = priorityConnector.usePriorityProvider()
|
||||
const priorityWeb3React = priorityConnector.usePriorityWeb3React(priorityProvider)
|
||||
const setWeb3 = useUpdateAtom(web3Atom)
|
||||
useEffect(() => {
|
||||
setWeb3(priorityWeb3React)
|
||||
}, [priorityWeb3React, setWeb3])
|
||||
|
||||
return <>{children}</>
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
import { Currency, Percent, TradeType } from '@uniswap/sdk-core'
|
||||
import useAutoSlippageTolerance from 'hooks/useAutoSlippageTolerance'
|
||||
import { useAtomValue } from 'jotai/utils'
|
||||
import { autoSlippageAtom, maxSlippageAtom } from 'lib/state/settings'
|
||||
import { InterfaceTrade } from 'state/routing/types'
|
||||
|
||||
export function toPercent(maxSlippage: number | undefined): Percent | undefined {
|
||||
if (!maxSlippage) return undefined
|
||||
const numerator = Math.floor(maxSlippage * 100)
|
||||
return new Percent(numerator, 10_000)
|
||||
}
|
||||
|
||||
/** Returns the user-inputted max slippage. */
|
||||
export default function useAllowedSlippage(trade: InterfaceTrade<Currency, Currency, TradeType> | undefined): Percent {
|
||||
const autoSlippage = useAutoSlippageTolerance(trade)
|
||||
const maxSlippage = toPercent(useAtomValue(maxSlippageAtom))
|
||||
return useAtomValue(autoSlippageAtom) ? autoSlippage : maxSlippage ?? autoSlippage
|
||||
}
|
||||
|
||||
export const MAX_VALID_SLIPPAGE = new Percent(1, 2)
|
||||
export const MIN_HIGH_SLIPPAGE = new Percent(1, 100)
|
||||
|
||||
export function getSlippageWarning(slippage?: Percent): 'warning' | 'error' | undefined {
|
||||
if (slippage?.greaterThan(MAX_VALID_SLIPPAGE)) return 'error'
|
||||
if (slippage?.greaterThan(MIN_HIGH_SLIPPAGE)) return 'warning'
|
||||
return
|
||||
}
|
||||
@@ -25,7 +25,8 @@ function useBlock() {
|
||||
|
||||
useEffect(() => {
|
||||
if (library && chainId && windowVisible) {
|
||||
setState({ chainId })
|
||||
// If chainId hasn't changed, don't clear the block. This prevents re-fetching still valid data.
|
||||
setState((state) => (state.chainId === chainId ? state : { chainId }))
|
||||
|
||||
library
|
||||
.getBlockNumber()
|
||||
|
||||
@@ -7,9 +7,10 @@ import { NEVER_RELOAD, useSingleCallResult } from 'lib/hooks/multicall'
|
||||
import useNativeCurrency from 'lib/hooks/useNativeCurrency'
|
||||
import { useMemo } from 'react'
|
||||
|
||||
import { TOKEN_SHORTHANDS } from '../../constants/tokens'
|
||||
import { isAddress } from '../../utils'
|
||||
import { useTokenMap } from './useTokenList'
|
||||
import { TokenMap } from './useTokenList'
|
||||
import { supportedChainId } from '../../utils/supportedChainId'
|
||||
import { TokenMap, useTokenMap } from './useTokenList'
|
||||
|
||||
// parse a name or symbol from a token response
|
||||
const BYTES32_REGEX = /^0x[a-fA-F0-9]{64}$/
|
||||
@@ -28,35 +29,27 @@ function parseStringOrBytes32(str: string | undefined, bytes32: string | undefin
|
||||
* Returns null if token is loading or null was passed.
|
||||
* Returns undefined if tokenAddress is invalid or token does not exist.
|
||||
*/
|
||||
export function useTokenFromMap(tokens: TokenMap, tokenAddress?: string | null): Token | null | undefined {
|
||||
export function useTokenFromNetwork(tokenAddress: string | null | undefined): Token | null | undefined {
|
||||
const { chainId } = useActiveWeb3React()
|
||||
|
||||
const address = isAddress(tokenAddress)
|
||||
const formattedAddress = 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 tokenContract = useTokenContract(formattedAddress ? formattedAddress : undefined, false)
|
||||
const tokenContractBytes32 = useBytes32TokenContract(formattedAddress ? formattedAddress : undefined, false)
|
||||
|
||||
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)
|
||||
const tokenName = useSingleCallResult(tokenContract, 'name', undefined, NEVER_RELOAD)
|
||||
const tokenNameBytes32 = useSingleCallResult(tokenContractBytes32, 'name', undefined, NEVER_RELOAD)
|
||||
const symbol = useSingleCallResult(tokenContract, 'symbol', undefined, NEVER_RELOAD)
|
||||
const symbolBytes32 = useSingleCallResult(tokenContractBytes32, 'symbol', undefined, NEVER_RELOAD)
|
||||
const decimals = useSingleCallResult(tokenContract, 'decimals', undefined, NEVER_RELOAD)
|
||||
|
||||
return useMemo(() => {
|
||||
if (token) return token
|
||||
if (tokenAddress === null) return null
|
||||
if (!chainId || !address) return undefined
|
||||
if (typeof tokenAddress !== 'string' || !chainId || !formattedAddress) return undefined
|
||||
if (decimals.loading || symbol.loading || tokenName.loading) return null
|
||||
if (decimals.result) {
|
||||
return new Token(
|
||||
chainId,
|
||||
address,
|
||||
formattedAddress,
|
||||
decimals.result[0],
|
||||
parseStringOrBytes32(symbol.result?.[0], symbolBytes32.result?.[0], 'UNKNOWN'),
|
||||
parseStringOrBytes32(tokenName.result?.[0], tokenNameBytes32.result?.[0], 'Unknown Token')
|
||||
@@ -64,14 +57,13 @@ export function useTokenFromMap(tokens: TokenMap, tokenAddress?: string | null):
|
||||
}
|
||||
return undefined
|
||||
}, [
|
||||
address,
|
||||
formattedAddress,
|
||||
chainId,
|
||||
decimals.loading,
|
||||
decimals.result,
|
||||
symbol.loading,
|
||||
symbol.result,
|
||||
symbolBytes32.result,
|
||||
token,
|
||||
tokenAddress,
|
||||
tokenName.loading,
|
||||
tokenName.result,
|
||||
@@ -79,6 +71,20 @@ export function useTokenFromMap(tokens: TokenMap, tokenAddress?: string | null):
|
||||
])
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a Token from the tokenAddress.
|
||||
* Returns null if token is loading or null was passed.
|
||||
* Returns undefined if tokenAddress is invalid or token does not exist.
|
||||
*/
|
||||
export function useTokenFromMapOrNetwork(tokens: TokenMap, tokenAddress?: string | null): Token | null | undefined {
|
||||
const address = isAddress(tokenAddress)
|
||||
const token: Token | undefined = address ? tokens[address] : undefined
|
||||
|
||||
const tokenFromNetwork = useTokenFromNetwork(token ? undefined : address ? address : undefined)
|
||||
|
||||
return tokenFromNetwork ?? token
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a Token from the tokenAddress.
|
||||
* Returns null if token is loading or null was passed.
|
||||
@@ -86,7 +92,7 @@ export function useTokenFromMap(tokens: TokenMap, tokenAddress?: string | null):
|
||||
*/
|
||||
export function useToken(tokenAddress?: string | null): Token | null | undefined {
|
||||
const tokens = useTokenMap()
|
||||
return useTokenFromMap(tokens, tokenAddress)
|
||||
return useTokenFromMapOrNetwork(tokens, tokenAddress)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -96,8 +102,14 @@ export function useToken(tokenAddress?: string | null): Token | null | undefined
|
||||
*/
|
||||
export function useCurrencyFromMap(tokens: TokenMap, currencyId?: string | null): Currency | null | undefined {
|
||||
const nativeCurrency = useNativeCurrency()
|
||||
const { chainId } = useActiveWeb3React()
|
||||
const isNative = Boolean(nativeCurrency && currencyId?.toUpperCase() === 'ETH')
|
||||
const token = useTokenFromMap(tokens, isNative ? undefined : currencyId)
|
||||
const shorthandMatchAddress = useMemo(() => {
|
||||
const chain = supportedChainId(chainId)
|
||||
return chain && currencyId ? TOKEN_SHORTHANDS[currencyId.toUpperCase()]?.[chain] : undefined
|
||||
}, [chainId, currencyId])
|
||||
|
||||
const token = useTokenFromMapOrNetwork(tokens, isNative ? undefined : shorthandMatchAddress ?? currencyId)
|
||||
|
||||
if (currencyId === null || currencyId === undefined) return currencyId
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Currency } from '@uniswap/sdk-core'
|
||||
import { useTheme } from 'lib/theme'
|
||||
import Vibrant from 'node-vibrant/lib/bundle'
|
||||
import Vibrant from 'node-vibrant/lib/bundle.js'
|
||||
import { useEffect, useLayoutEffect, useState } from 'react'
|
||||
|
||||
import useCurrencyLogoURIs from './useCurrencyLogoURIs'
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { Provider as EthersProvider } from '@ethersproject/abstract-provider'
|
||||
import { Signer as EthersSigner } from '@ethersproject/abstract-signer'
|
||||
import { VoidSigner } from '@ethersproject/abstract-signer'
|
||||
import { Eip1193Bridge as ExperimentalEip1193Bridge } from '@ethersproject/experimental'
|
||||
import { JsonRpcProvider } from '@ethersproject/providers'
|
||||
import { JsonRpcProvider, JsonRpcSigner } from '@ethersproject/providers'
|
||||
import { Provider as Eip1193Provider } from '@web3-react/types'
|
||||
import { ZERO_ADDRESS } from 'constants/misc'
|
||||
import { useMemo } from 'react'
|
||||
@@ -17,32 +16,48 @@ class Eip1193Bridge extends ExperimentalEip1193Bridge {
|
||||
const result = await this.provider.getNetwork()
|
||||
return '0x' + result.chainId.toString(16)
|
||||
}
|
||||
case 'eth_requestAccounts':
|
||||
try {
|
||||
return await super.send(method, params)
|
||||
} catch (e) {
|
||||
return this.send('eth_accounts')
|
||||
}
|
||||
case 'eth_sendTransaction': {
|
||||
if (!this.signer) break
|
||||
|
||||
// TODO(zzmp): JsonRpcProvider filters from/gas fields from the params.
|
||||
const req = JsonRpcProvider.hexlifyTransaction(params?.[0], { from: true, gas: true })
|
||||
const tx = await this.signer.sendTransaction(req)
|
||||
return tx.hash
|
||||
}
|
||||
default:
|
||||
return super.send(method, params)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface EthersSigningProvider extends EthersProvider {
|
||||
getSigner(): JsonRpcSigner
|
||||
}
|
||||
|
||||
export default function useEip1193Provider(
|
||||
provider?: Eip1193Provider | EthersProvider | JsonRpcProvider | { provider: EthersProvider; signer: EthersSigner }
|
||||
provider?: Eip1193Provider | EthersSigningProvider | EthersProvider
|
||||
): Eip1193Provider | undefined {
|
||||
return useMemo(() => {
|
||||
if (provider) {
|
||||
if (provider instanceof EthersProvider) {
|
||||
// A JsonRpcProvider includes its own Signer, otherwise use a VoidSigner.
|
||||
if (EthersProvider.isProvider(provider)) {
|
||||
const signer = 'getSigner' in provider ? provider.getSigner() : null ?? voidSigner
|
||||
return new Eip1193Bridge(signer, provider)
|
||||
}
|
||||
|
||||
if ('provider' in provider && 'signer' in provider) {
|
||||
return new Eip1193Bridge(provider.signer, provider.provider)
|
||||
}
|
||||
|
||||
// See https://github.com/ethereum/EIPs/blob/master/EIPS/eip-1193.md.
|
||||
if ('request' in provider && 'on' in provider && 'removeListener' in provider) {
|
||||
return provider
|
||||
} else if (EthersProvider.isProvider((provider as ExperimentalEip1193Bridge).provider)) {
|
||||
/*
|
||||
* Direct users to use our own wrapper to avoid any pitfalls:
|
||||
* - Eip1193Bridge is experimental
|
||||
* - signer is not straightforward
|
||||
* - bugs out if chainId>8
|
||||
*/
|
||||
throw new Error('Eip1193Bridge is experimental: pass your ethers Provider directly')
|
||||
}
|
||||
}
|
||||
return
|
||||
return provider
|
||||
}, [provider])
|
||||
}
|
||||
|
||||
@@ -1,7 +1,15 @@
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
|
||||
export default function useHasFocus(node: Node | null | undefined): boolean {
|
||||
const [hasFocus, setHasFocus] = useState(node?.contains(document.activeElement) ?? false)
|
||||
useEffect(() => {
|
||||
if (node instanceof HTMLElement) {
|
||||
// tabIndex is required to receive blur events from non-button elements.
|
||||
node.tabIndex = node.tabIndex || -1
|
||||
// Without explicitly omitting outline, Safari will now outline this node when focused.
|
||||
node.style.outline = node.style.outline || 'none'
|
||||
}
|
||||
}, [node])
|
||||
const [hasFocus, setHasFocus] = useState(node?.contains(document?.activeElement) ?? false)
|
||||
const onFocus = useCallback(() => setHasFocus(true), [])
|
||||
const onBlur = useCallback((e) => setHasFocus(node?.contains(e.relatedTarget) ?? false), [node])
|
||||
useEffect(() => {
|
||||
|
||||
11
src/lib/hooks/useOnSupportedNetwork.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { ALL_SUPPORTED_CHAIN_IDS } from 'constants/chains'
|
||||
import { useMemo } from 'react'
|
||||
|
||||
import useActiveWeb3React from './useActiveWeb3React'
|
||||
|
||||
function useOnSupportedNetwork() {
|
||||
const { chainId } = useActiveWeb3React()
|
||||
return useMemo(() => chainId && ALL_SUPPORTED_CHAIN_IDS.includes(chainId), [chainId])
|
||||
}
|
||||
|
||||
export default useOnSupportedNetwork
|
||||