Compare commits
102 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b0ff0f83b0 | ||
|
|
5cf9e84db5 | ||
|
|
c0bdb8db12 | ||
|
|
2d8f767d74 | ||
|
|
1303416eca | ||
|
|
124f6420a5 | ||
|
|
91f5fc0881 | ||
|
|
ec831f8433 | ||
|
|
56bd9b68d7 | ||
|
|
865d21f039 | ||
|
|
dceadf8472 | ||
|
|
cd3a91bca8 | ||
|
|
de1f5d1adc | ||
|
|
b5d403768f | ||
|
|
c4c811aeb3 | ||
|
|
33c24a3f05 | ||
|
|
9ef2b3a116 | ||
|
|
afe38a2d10 | ||
|
|
d28607a1c8 | ||
|
|
7fb363ac46 | ||
|
|
16b0b1530d | ||
|
|
abb2696f40 | ||
|
|
772178fc86 | ||
|
|
9f1378f635 | ||
|
|
84275dcce1 | ||
|
|
a76ece6ce3 | ||
|
|
334e137fb3 | ||
|
|
eb6c4d464a | ||
|
|
24734e6a34 | ||
|
|
f1bcee3c08 | ||
|
|
7a215ccdb4 | ||
|
|
c5c4f48d96 | ||
|
|
bdcf761ddd | ||
|
|
e876267d83 | ||
|
|
76cbd82cb7 | ||
|
|
6c4f7ab9a1 | ||
|
|
da20315724 | ||
|
|
963b910552 | ||
|
|
9e2dc9a435 | ||
|
|
6567f18bf5 | ||
|
|
ee96973212 | ||
|
|
ce6c783174 | ||
|
|
64e8c3ced9 | ||
|
|
46e6c2295d | ||
|
|
3626dbdeec | ||
|
|
eb75e0dc2e | ||
|
|
c26ecdfc88 | ||
|
|
f508788026 | ||
|
|
377026bca8 | ||
|
|
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 |
@@ -1,5 +1,5 @@
|
||||
REACT_APP_INFURA_KEY="099fc58e0de9451d80b18d7c74caa7c1"
|
||||
REACT_APP_PORTIS_ID="c0e2bf01-4b08-4fd5-ac7b-8e26b58cd236"
|
||||
REACT_APP_FORTMATIC_KEY="pk_live_F937DF033A1666BF"
|
||||
REACT_APP_GOOGLE_ANALYTICS_ID="UA-128182339-4"
|
||||
REACT_APP_GOOGLE_ANALYTICS_ID="G-KDP9B6W4H8"
|
||||
REACT_APP_FIREBASE_KEY="AIzaSyBcZWwTcTJHj_R6ipZcrJkXdq05PuX0Rs0"
|
||||
|
||||
45
INTERFACE_README.md
Normal file
45
INTERFACE_README.md
Normal file
@@ -0,0 +1,45 @@
|
||||
# Uniswap Interface
|
||||
|
||||
An open source interface for Uniswap -- a protocol for decentralized exchange of Ethereum tokens.
|
||||
|
||||
- Website: [uniswap.org](https://uniswap.org/)
|
||||
- Interface: [app.uniswap.org](https://app.uniswap.org)
|
||||
- Docs: [uniswap.org/docs/](https://docs.uniswap.org/)
|
||||
- Twitter: [@Uniswap](https://twitter.com/Uniswap)
|
||||
- Reddit: [/r/Uniswap](https://www.reddit.com/r/Uniswap/)
|
||||
- Email: [contact@uniswap.org](mailto:contact@uniswap.org)
|
||||
- Discord: [Uniswap](https://discord.gg/FCfyBSbCU5)
|
||||
- Whitepapers:
|
||||
- [V1](https://hackmd.io/C-DvwDSfSxuh-Gd4WKE_ig)
|
||||
- [V2](https://uniswap.org/whitepaper.pdf)
|
||||
- [V3](https://uniswap.org/whitepaper-v3.pdf)
|
||||
|
||||
## Accessing the Uniswap Interface
|
||||
|
||||
To access the Uniswap Interface, use an IPFS gateway link from the
|
||||
[latest release](https://github.com/Uniswap/uniswap-interface/releases/latest),
|
||||
or visit [app.uniswap.org](https://app.uniswap.org).
|
||||
|
||||
## Unsupported tokens
|
||||
|
||||
Check out `useUnsupportedTokenList()` in [src/state/lists/hooks.ts](./src/state/lists/hooks.ts) for blocking tokens in your instance of the interface.
|
||||
|
||||
You can block an entire list of tokens by passing in a tokenlist like [here](./src/constants/lists.ts) or you can block specific tokens by adding them to [unsupported.tokenlist.json](./src/constants/tokenLists/unsupported.tokenlist.json).
|
||||
|
||||
## Contributions
|
||||
|
||||
For steps on local deployment, development, and code contribution, please see [CONTRIBUTING](./CONTRIBUTING.md).
|
||||
|
||||
## Accessing Uniswap V2
|
||||
|
||||
The Uniswap Interface supports swapping, adding liquidity, removing liquidity and migrating liquidity for Uniswap protocol V2.
|
||||
|
||||
- Swap on Uniswap V2: https://app.uniswap.org/#/swap?use=v2
|
||||
- View V2 liquidity: https://app.uniswap.org/#/pool/v2
|
||||
- Add V2 liquidity: https://app.uniswap.org/#/add/v2
|
||||
- Migrate V2 liquidity to V3: https://app.uniswap.org/#/migrate/v2
|
||||
|
||||
## Accessing Uniswap V1
|
||||
|
||||
The Uniswap V1 interface for mainnet and testnets is accessible via IPFS gateways
|
||||
linked from the [v1.0.0 release](https://github.com/Uniswap/uniswap-interface/releases/tag/v1.0.0).
|
||||
56
README.md
56
README.md
@@ -1,51 +1,21 @@
|
||||
# Uniswap Interface
|
||||
This repo is home to the Uniswap Widgets package and the web app interface [app.uniswap.org](https://app.uniswap.org).
|
||||
|
||||
[](https://github.com/Uniswap/uniswap-interface/actions/workflows/unit-tests.yaml)
|
||||
[](https://github.com/Uniswap/uniswap-interface/actions/workflows/integration-tests.yaml)
|
||||
[](https://github.com/Uniswap/uniswap-interface/actions/workflows/lint.yml)
|
||||
[](https://github.com/Uniswap/uniswap-interface/actions/workflows/release.yaml)
|
||||
# Uniswap Labs Interface
|
||||
|
||||
[](https://github.com/Uniswap/interface/actions/workflows/unit-tests.yaml)
|
||||
[](https://github.com/Uniswap/interface/actions/workflows/integration-tests.yaml)
|
||||
[](https://github.com/Uniswap/interface/actions/workflows/lint.yml)
|
||||
[](https://github.com/Uniswap/interface/actions/workflows/release.yaml)
|
||||
[](https://crowdin.com/project/uniswap-interface)
|
||||
|
||||
An open source interface for Uniswap -- a protocol for decentralized exchange of Ethereum tokens.
|
||||
The web application hosted at https://app.uniswap.org is a convenient way to access the core functionality of the Uniswap Protocol.
|
||||
|
||||
- Website: [uniswap.org](https://uniswap.org/)
|
||||
- Interface: [app.uniswap.org](https://app.uniswap.org)
|
||||
- Docs: [uniswap.org/docs/](https://docs.uniswap.org/)
|
||||
- Twitter: [@Uniswap](https://twitter.com/Uniswap)
|
||||
- Reddit: [/r/Uniswap](https://www.reddit.com/r/Uniswap/)
|
||||
- Email: [contact@uniswap.org](mailto:contact@uniswap.org)
|
||||
- Discord: [Uniswap](https://discord.gg/FCfyBSbCU5)
|
||||
- Whitepapers:
|
||||
- [V1](https://hackmd.io/C-DvwDSfSxuh-Gd4WKE_ig)
|
||||
- [V2](https://uniswap.org/whitepaper.pdf)
|
||||
- [V3](https://uniswap.org/whitepaper-v3.pdf)
|
||||
For documentation of the interface including how to contribute or access prior builds, please view the README here: [INTERFACE_README.md](./INTERFACE_README.md)
|
||||
|
||||
## Accessing the Uniswap Interface
|
||||
# Uniswap Labs Widgets
|
||||
|
||||
To access the Uniswap Interface, use an IPFS gateway link from the
|
||||
[latest release](https://github.com/Uniswap/uniswap-interface/releases/latest),
|
||||
or visit [app.uniswap.org](https://app.uniswap.org).
|
||||
The `@uniswap/widgets` package is an npm package of React components used to provide subsets of the Uniswap Protocol functionality in a small and configurable user interface element.
|
||||
|
||||
## Unsupported tokens
|
||||
The npm package can be found here. [@uniswap/widgets](https://www.npmjs.com/package/@uniswap/widgets)
|
||||
|
||||
Check out `useUnsupportedTokenList()` in [src/state/lists/hooks.ts](./src/state/lists/hooks.ts) for blocking tokens in your instance of the interface.
|
||||
|
||||
You can block an entire list of tokens by passing in a tokenlist like [here](./src/constants/lists.ts) or you can block specific tokens by adding them to [unsupported.tokenlist.json](./src/constants/tokenLists/unsupported.tokenlist.json).
|
||||
|
||||
## Contributions
|
||||
|
||||
For steps on local deployment, development, and code contribution, please see [CONTRIBUTING](./CONTRIBUTING.md).
|
||||
|
||||
## Accessing Uniswap V2
|
||||
|
||||
The Uniswap Interface supports swapping, adding liquidity, removing liquidity and migrating liquidity for Uniswap protocol V2.
|
||||
|
||||
- Swap on Uniswap V2: https://app.uniswap.org/#/swap?use=v2
|
||||
- View V2 liquidity: https://app.uniswap.org/#/pool/v2
|
||||
- Add V2 liquidity: https://app.uniswap.org/#/add/v2
|
||||
- Migrate V2 liquidity to V3: https://app.uniswap.org/#/migrate/v2
|
||||
|
||||
## Accessing Uniswap V1
|
||||
|
||||
The Uniswap V1 interface for mainnet and testnets is accessible via IPFS gateways
|
||||
linked from the [v1.0.0 release](https://github.com/Uniswap/uniswap-interface/releases/tag/v1.0.0).
|
||||
For documentation of the widgets package, please view the README here: [WIDGETS_README.md](./WIDGETS_README.md).
|
||||
|
||||
40
WIDGETS_README.md
Normal file
40
WIDGETS_README.md
Normal file
@@ -0,0 +1,40 @@
|
||||
# Uniswap Labs Swap Widget
|
||||
|
||||
The Swap Widget bundles the whole swapping experience into a single React component that developers can easily embed in their app with one line of code.
|
||||
|
||||

|
||||
|
||||
You can customize the theme (colors, font, border radius, and more) to match the style of your application. You can also configure your own default token list and optionally set a convenience fee on swaps executed through the widget on your site.
|
||||
|
||||
## Installation
|
||||
|
||||
Install the widgets library via `npm` or `yarn`. If you do not already use the widget's peerDependencies `redux` and `react-redux`, then you'll need to add them as well.
|
||||
|
||||
```js
|
||||
yarn add @uniswap/widgets redux react-redux
|
||||
```
|
||||
```js
|
||||
npm i --save @uniswap/widgets redux react-redux
|
||||
```
|
||||
|
||||
## Documentation
|
||||
|
||||
- [overview](https://docs.uniswap.org/sdk/widgets/swap-widget)
|
||||
- [api reference](https://docs.uniswap.org/sdk/widgets/swap-widget/api)
|
||||
|
||||
## Example Apps
|
||||
|
||||
Uniswap Labs maintains two demo apps in branches of the [widgets-demo](https://github.com/Uniswap/widgets-demo) repo:
|
||||
|
||||
- [NextJS](https://github.com/Uniswap/widgets-demo/tree/nextjs)
|
||||
- [Create React App](https://github.com/Uniswap/widgets-demo/tree/cra)
|
||||
|
||||
Others have also also released the widget in production to their userbase:
|
||||
|
||||
- [OpenSea](https://opensea.io/)
|
||||
- [Friends With Benefits](https://www.fwb.help/)
|
||||
- [Oasis](https://oasis.app/)
|
||||
|
||||
## Legal notice
|
||||
|
||||
Uniswap Labs encourages integrators to evaluate their own regulatory obligations when integrating this widget into their products, including, but not limited to, those related to economic or trade sanctions compliance.
|
||||
39
package.json
39
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@uniswap/widgets",
|
||||
"version": "0.0.22-beta",
|
||||
"version": "0.0.26-beta",
|
||||
"description": "Uniswap Interface",
|
||||
"homepage": ".",
|
||||
"files": [
|
||||
@@ -30,6 +30,7 @@
|
||||
"@babel/preset-env": "^7.16.11",
|
||||
"@babel/preset-react": "^7.16.7",
|
||||
"@babel/preset-typescript": "^7.16.7",
|
||||
"@ethersproject/experimental": "^5.4.0",
|
||||
"@gnosis.pm/safe-apps-web3-react": "^0.6.0",
|
||||
"@graphql-codegen/cli": "1.21.5",
|
||||
"@graphql-codegen/typescript": "1.22.3",
|
||||
@@ -89,8 +90,8 @@
|
||||
"@uniswap/v2-periphery": "^1.1.0-beta.0",
|
||||
"@uniswap/v3-core": "1.0.0",
|
||||
"@uniswap/v3-periphery": "^1.1.1",
|
||||
"@web3-react/metamask": "8.0.13-beta.0",
|
||||
"@web3-react/walletconnect": "8.0.18-beta.0",
|
||||
"@web3-react/metamask": "^8.0.18-beta.0",
|
||||
"@web3-react/walletconnect": "^8.0.25-beta.0",
|
||||
"array.prototype.flat": "^1.2.4",
|
||||
"array.prototype.flatmap": "^1.2.4",
|
||||
"babel-plugin-macros": "^3.1.0",
|
||||
@@ -118,7 +119,7 @@
|
||||
"react-confetti": "^6.0.0",
|
||||
"react-cosmos": "^5.6.6",
|
||||
"react-dom": "^17.0.1",
|
||||
"react-ga": "^2.5.7",
|
||||
"react-ga4": "^1.4.1",
|
||||
"react-is": "^17.0.2",
|
||||
"react-markdown": "^4.3.1",
|
||||
"react-redux": "^7.2.2",
|
||||
@@ -193,39 +194,25 @@
|
||||
"license": "GPL-3.0-or-later",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.17.0",
|
||||
"@ethersproject/abi": "^5.4.1",
|
||||
"@ethersproject/abstract-provider": "^5.4.1",
|
||||
"@ethersproject/abstract-signer": "^5.4.1",
|
||||
"@ethersproject/address": "^5.4.0",
|
||||
"@ethersproject/bignumber": "^5.4.2",
|
||||
"@ethersproject/bytes": "^5.4.0",
|
||||
"@ethersproject/constants": "^5.4.0",
|
||||
"@ethersproject/contracts": "^5.4.1",
|
||||
"@ethersproject/experimental": "^5.4.0",
|
||||
"@ethersproject/hash": "^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",
|
||||
"@popperjs/core": "^2.4.4",
|
||||
"@reduxjs/toolkit": "^1.6.1",
|
||||
"@uniswap/redux-multicall": "^1.0.0",
|
||||
"@uniswap/redux-multicall": "^1.1.1",
|
||||
"@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.26",
|
||||
"@uniswap/token-lists": "^1.0.0-beta.27",
|
||||
"@uniswap/v2-sdk": "^3.0.1",
|
||||
"@uniswap/v3-sdk": "^3.8.2",
|
||||
"@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",
|
||||
"@web3-react/core": "^8.0.22-beta.0",
|
||||
"@web3-react/eip1193": "^8.0.17-beta.0",
|
||||
"@web3-react/empty": "^8.0.11-beta.0",
|
||||
"@web3-react/types": "^8.0.11-beta.0",
|
||||
"@web3-react/url": "^8.0.16-beta.0",
|
||||
"ajv": "^6.12.3",
|
||||
"cids": "^1.0.0",
|
||||
"ethers": "^5.1.4",
|
||||
"immer": "^9.0.6",
|
||||
"jotai": "^1.3.7",
|
||||
"jsbi": "^3.1.4",
|
||||
|
||||
@@ -37,6 +37,11 @@ function isAsset(source: string) {
|
||||
return extname && [...ASSET_EXTENSIONS, '.css', '.scss'].includes(extname)
|
||||
}
|
||||
|
||||
function isEthers(source: string) {
|
||||
// @ethersproject/* modules are provided by ethers, with the exception of experimental.
|
||||
return source.startsWith('@ethersproject/') && !source.endsWith('experimental')
|
||||
}
|
||||
|
||||
const TS_CONFIG = './tsconfig.lib.json'
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const { baseUrl, paths }: CompilerOptions = require(TS_CONFIG).compilerOptions
|
||||
@@ -60,7 +65,7 @@ const plugins = [
|
||||
const check = {
|
||||
input: 'src/lib/index.tsx',
|
||||
output: { file: 'dist/widgets.tsc', inlineDynamicImports: true },
|
||||
external: isAsset,
|
||||
external: (source: string) => isAsset(source) || isEthers(source),
|
||||
plugins: [
|
||||
externals({ exclude: ['constants'], deps: true, peerDeps: true }), // marks builtins, dependencies, and peerDependencies external
|
||||
...plugins,
|
||||
@@ -72,7 +77,7 @@ const check = {
|
||||
const type = {
|
||||
input: 'dist/dts/lib/index.d.ts',
|
||||
output: { file: 'dist/index.d.ts' },
|
||||
external: isAsset,
|
||||
external: (source: string) => isAsset(source) || isEthers(source),
|
||||
plugins: [
|
||||
externals({ exclude: ['constants'], deps: true, peerDeps: true }),
|
||||
dts({ compilerOptions: { baseUrl: 'dist/dts' } }),
|
||||
@@ -112,6 +117,7 @@ const transpile = {
|
||||
sourcemap: false,
|
||||
},
|
||||
],
|
||||
external: isEthers,
|
||||
plugins: [
|
||||
externals({
|
||||
exclude: [
|
||||
|
||||
BIN
src/assets/images/widget-screenshot.png
Normal file
BIN
src/assets/images/widget-screenshot.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 106 KiB |
@@ -1,10 +0,0 @@
|
||||
<svg width="23" height="20" viewBox="0 0 23 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<linearGradient id="gradient1" 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)" />
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 782 B |
@@ -1,6 +1,6 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import React, { ErrorInfo } from 'react'
|
||||
import ReactGA from 'react-ga'
|
||||
import ReactGA from 'react-ga4'
|
||||
import styled from 'styled-components/macro'
|
||||
|
||||
import store, { AppState } from '../../state'
|
||||
@@ -49,6 +49,11 @@ type ErrorBoundaryState = {
|
||||
|
||||
const IS_UNISWAP = window.location.hostname === 'app.uniswap.org'
|
||||
|
||||
async function updateServiceWorker(): Promise<void> {
|
||||
const ready = await navigator.serviceWorker.ready
|
||||
await ready.update()
|
||||
}
|
||||
|
||||
export default class ErrorBoundary extends React.Component<unknown, ErrorBoundaryState> {
|
||||
constructor(props: unknown) {
|
||||
super(props)
|
||||
@@ -56,15 +61,18 @@ export default class ErrorBoundary extends React.Component<unknown, ErrorBoundar
|
||||
}
|
||||
|
||||
static getDerivedStateFromError(error: Error): ErrorBoundaryState {
|
||||
updateServiceWorker()
|
||||
.then(() => {
|
||||
window.location.reload()
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to update service worker', error)
|
||||
})
|
||||
return { error }
|
||||
}
|
||||
|
||||
componentDidCatch(error: Error, errorInfo: ErrorInfo) {
|
||||
ReactGA.exception({
|
||||
...error,
|
||||
...errorInfo,
|
||||
fatal: true,
|
||||
})
|
||||
ReactGA.event('exception', { description: error.toString() + errorInfo.toString(), fatal: true })
|
||||
}
|
||||
|
||||
render() {
|
||||
|
||||
@@ -11,7 +11,7 @@ import { PoolState, usePools } from 'hooks/usePools'
|
||||
import usePrevious from 'hooks/usePrevious'
|
||||
import { DynamicSection } from 'pages/AddLiquidity/styled'
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import ReactGA from 'react-ga'
|
||||
import ReactGA from 'react-ga4'
|
||||
import { Box } from 'rebass'
|
||||
import styled, { keyframes } from 'styled-components/macro'
|
||||
import { ThemedText } from 'theme'
|
||||
|
||||
@@ -255,8 +255,8 @@ export default function NetworkSelector() {
|
||||
|
||||
const handleChainSwitch = useCallback(
|
||||
(targetChain: number, skipToggle?: boolean) => {
|
||||
if (!library) return
|
||||
switchToNetwork({ library, chainId: targetChain })
|
||||
if (!library?.provider) return
|
||||
switchToNetwork({ provider: library.provider, chainId: targetChain })
|
||||
.then(() => {
|
||||
if (!skipToggle) {
|
||||
toggle()
|
||||
|
||||
@@ -9,7 +9,7 @@ import useTheme from 'hooks/useTheme'
|
||||
import { saturate } from 'polished'
|
||||
import React, { ReactNode, useCallback, useMemo } from 'react'
|
||||
import { BarChart2, CloudOff, Inbox } from 'react-feather'
|
||||
import ReactGA from 'react-ga'
|
||||
import ReactGA from 'react-ga4'
|
||||
import { batch } from 'react-redux'
|
||||
import { Bound } from 'state/mint/v3/actions'
|
||||
import styled from 'styled-components/macro'
|
||||
@@ -158,11 +158,7 @@ export default function LiquidityChartRangeInput({
|
||||
)
|
||||
|
||||
if (isError) {
|
||||
ReactGA.exception({
|
||||
...error,
|
||||
category: 'Liquidity',
|
||||
fatal: false,
|
||||
})
|
||||
ReactGA.event('exception', { description: error.toString(), fatal: false })
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -3,7 +3,7 @@ import { CurrencyAmount, Token } from '@uniswap/sdk-core'
|
||||
import useActiveWeb3React from 'hooks/useActiveWeb3React'
|
||||
import { useCallback, useEffect } from 'react'
|
||||
import { Heart, X } from 'react-feather'
|
||||
import ReactGA from 'react-ga'
|
||||
import ReactGA from 'react-ga4'
|
||||
import styled, { keyframes } from 'styled-components/macro'
|
||||
|
||||
import tokenLogo from '../../assets/images/token-logo.png'
|
||||
|
||||
@@ -1,139 +0,0 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import FlagImage from 'assets/images/ukraine.png'
|
||||
import { AutoColumn } from 'components/Column'
|
||||
import { RowBetween, RowFixed } from 'components/Row'
|
||||
import { X } from 'react-feather'
|
||||
import ReactGA from 'react-ga'
|
||||
import { useDarkModeManager, useShowDonationLink } from 'state/user/hooks'
|
||||
import styled from 'styled-components/macro'
|
||||
import { ExternalLink, ThemedText, Z_INDEX } from 'theme'
|
||||
|
||||
const darkGradient = `radial-gradient(87.53% 3032.45% at 5.16% 10.13%, rgba(255, 255, 255, 0.2) 0%, rgba(255, 255, 255, 0) 100%),
|
||||
linear-gradient(0deg, rgba(0, 91, 187, 0.35), rgba(0, 91, 187, 0.35)), #000000;`
|
||||
const lightGradient = `radial-gradient(87.53% 3032.45% at 5.16% 10.13%, rgba(255, 255, 255, 0.2) 0%, rgba(255, 255, 255, 0) 100%), linear-gradient(0deg, #CBE4FF, #CBE4FF), linear-gradient(0deg, rgba(255, 255, 255, 0.09), rgba(255, 255, 255, 0.09)), radial-gradient(100% 93.36% at 0% 6.64%, #8BC4FF 0%, #FFF5BF 100%);`
|
||||
|
||||
const Wrapper = styled(AutoColumn)<{ darkMode: boolean }>`
|
||||
background: #edeef2;
|
||||
position: fixed;
|
||||
bottom: 40px;
|
||||
border-radius: 12px;
|
||||
padding: 18px;
|
||||
max-width: 360px;
|
||||
background: ${({ darkMode }) => (darkMode ? darkGradient : lightGradient)};
|
||||
color: ${({ theme }) => theme.text1};
|
||||
z-index: ${Z_INDEX.deprecated_content};
|
||||
|
||||
:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
& > * {
|
||||
z-index: ${Z_INDEX.fixed};
|
||||
}
|
||||
|
||||
overflow: hidden;
|
||||
|
||||
:before {
|
||||
background-image: url(${FlagImage});
|
||||
background-repeat: no-repeat;
|
||||
overflow: hidden;
|
||||
background-size: 300px;
|
||||
content: '';
|
||||
height: 1200px;
|
||||
width: 400px;
|
||||
opacity: 0.1;
|
||||
position: absolute;
|
||||
transform: rotate(25deg) translate(-140px, -60px);
|
||||
width: 300px;
|
||||
z-index: ${Z_INDEX.deprecated_zero};
|
||||
}
|
||||
|
||||
${({ theme }) => theme.mediaWidth.upToSmall`
|
||||
max-width: 100%;
|
||||
`}
|
||||
|
||||
${({ theme }) => theme.mediaWidth.upToMedium`
|
||||
position: relative;
|
||||
bottom: unset;
|
||||
`}
|
||||
`
|
||||
|
||||
const WrappedCloseIcon = styled(X)`
|
||||
stroke: ${({ theme }) => theme.text2};
|
||||
z-index: ${Z_INDEX.tooltip};
|
||||
:hover {
|
||||
cursor: pointer;
|
||||
opacity: 0.8;
|
||||
}
|
||||
`
|
||||
|
||||
export const StyledFlagImage = styled.div`
|
||||
margin-right: 12px;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
border-radius: 100%;
|
||||
&:before,
|
||||
&:after {
|
||||
content: '';
|
||||
width: 9px;
|
||||
height: 18px;
|
||||
}
|
||||
&:before {
|
||||
float: left;
|
||||
border-top-left-radius: 9px;
|
||||
border-bottom-left-radius: 9px;
|
||||
background: #005bbb;
|
||||
}
|
||||
&:after {
|
||||
float: right;
|
||||
border-top-right-radius: 9px;
|
||||
border-bottom-right-radius: 9px;
|
||||
background: #ffd500;
|
||||
}
|
||||
transform: rotate(90deg);
|
||||
`
|
||||
|
||||
const StyledLink = styled(ExternalLink)`
|
||||
text-decoration: none !important;
|
||||
`
|
||||
|
||||
export default function DonationLink() {
|
||||
const [darkMode] = useDarkModeManager()
|
||||
const [, setVisible] = useShowDonationLink()
|
||||
|
||||
return (
|
||||
<Wrapper
|
||||
gap="10px"
|
||||
darkMode={darkMode}
|
||||
as={StyledLink}
|
||||
target="https://donate.uniswap.org/#/swap"
|
||||
href="https://donate.uniswap.org/#/swap"
|
||||
onClickCapture={() => {
|
||||
ReactGA.event({
|
||||
category: 'Donate',
|
||||
action: 'Link to Ukraine site.',
|
||||
})
|
||||
}}
|
||||
>
|
||||
<RowBetween>
|
||||
<RowFixed>
|
||||
<StyledFlagImage />
|
||||
<ThemedText.Body fontWeight={600} fontSize={'18px'}>
|
||||
<Trans>Donate to Ukraine</Trans>
|
||||
</ThemedText.Body>
|
||||
</RowFixed>
|
||||
<WrappedCloseIcon
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
setVisible(false)
|
||||
return false
|
||||
}}
|
||||
/>
|
||||
</RowBetween>
|
||||
<ThemedText.Body fontWeight={400} fontSize="12px" color="text2">
|
||||
<Trans>Directly support the Ukrainian government by donating tokens.</Trans>
|
||||
</ThemedText.Body>
|
||||
</Wrapper>
|
||||
)
|
||||
}
|
||||
@@ -4,7 +4,7 @@ import { RowFixed } from 'components/Row'
|
||||
import useCurrentBlockTimestamp from 'hooks/useCurrentBlockTimestamp'
|
||||
import { useEffect } from 'react'
|
||||
import { MessageCircle, X } from 'react-feather'
|
||||
import ReactGA from 'react-ga'
|
||||
import ReactGA from 'react-ga4'
|
||||
import { useShowSurveyPopup } from 'state/user/hooks'
|
||||
import styled from 'styled-components/macro'
|
||||
import { ExternalLink, ThemedText, Z_INDEX } from 'theme'
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
import { SupportedChainId } from 'constants/chains'
|
||||
import useActiveWeb3React from 'hooks/useActiveWeb3React'
|
||||
import { useLocation } from 'react-router-dom'
|
||||
import styled from 'styled-components/macro'
|
||||
import { MEDIA_WIDTHS } from 'theme'
|
||||
|
||||
import { useActivePopups } from '../../state/application/hooks'
|
||||
import { useShowDonationLink, useURLWarningVisible } from '../../state/user/hooks'
|
||||
import { useURLWarningVisible } from '../../state/user/hooks'
|
||||
import { AutoColumn } from '../Column'
|
||||
import ClaimPopup from './ClaimPopup'
|
||||
import DonationLink from './DonationLink'
|
||||
import PopupItem from './PopupItem'
|
||||
|
||||
const MobilePopupWrapper = styled.div<{ height: string | number }>`
|
||||
@@ -68,11 +66,6 @@ export default function Popups() {
|
||||
const { chainId } = useActiveWeb3React()
|
||||
const isNotOnMainnet = Boolean(chainId && chainId !== SupportedChainId.MAINNET)
|
||||
|
||||
const location = useLocation()
|
||||
const isOnSwapPage = location.pathname.includes('swap')
|
||||
const [donationVisible] = useShowDonationLink()
|
||||
const showDonation = donationVisible && isOnSwapPage
|
||||
|
||||
return (
|
||||
<>
|
||||
<FixedPopupColumn gap="20px" extraPadding={urlWarningActive} xlPadding={isNotOnMainnet}>
|
||||
@@ -80,11 +73,9 @@ export default function Popups() {
|
||||
{activePopups.map((item) => (
|
||||
<PopupItem key={item.key} content={item.content} popKey={item.key} removeAfterMs={item.removeAfterMs} />
|
||||
))}
|
||||
{showDonation ? <DonationLink /> : null}
|
||||
</FixedPopupColumn>
|
||||
<MobilePopupWrapper height={activePopups?.length > 0 || showDonation ? 'fit-content' : 0}>
|
||||
<MobilePopupWrapper height={activePopups?.length > 0 ? 'fit-content' : 0}>
|
||||
<MobilePopupInner>
|
||||
{showDonation ? <DonationLink /> : null}
|
||||
{activePopups // reverse so new items up front
|
||||
.slice(0)
|
||||
.reverse()
|
||||
|
||||
@@ -3,7 +3,7 @@ import Card, { DarkGreyCard } from 'components/Card'
|
||||
import Row, { AutoRow, RowBetween } from 'components/Row'
|
||||
import { useEffect, useRef } from 'react'
|
||||
import { ArrowDown, Info, X } from 'react-feather'
|
||||
import ReactGA from 'react-ga'
|
||||
import ReactGA from 'react-ga4'
|
||||
import styled from 'styled-components/macro'
|
||||
import { ExternalLink, ThemedText } from 'theme'
|
||||
import { isMobile } from 'utils/userAgent'
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Trans } from '@lingui/macro'
|
||||
import { ButtonOutlined } from 'components/Button'
|
||||
import { AutoRow } from 'components/Row'
|
||||
import React from 'react'
|
||||
import ReactGA from 'react-ga'
|
||||
import ReactGA from 'react-ga4'
|
||||
import styled from 'styled-components/macro'
|
||||
import { ThemedText } from 'theme'
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ import { getTokenFilter } from 'lib/hooks/useTokenList/filtering'
|
||||
import { tokenComparator, useSortTokensByQuery } from 'lib/hooks/useTokenList/sorting'
|
||||
import { KeyboardEvent, RefObject, useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { Edit } from 'react-feather'
|
||||
import ReactGA from 'react-ga'
|
||||
import ReactGA from 'react-ga4'
|
||||
import AutoSizer from 'react-virtualized-auto-sizer'
|
||||
import { FixedSizeList } from 'react-window'
|
||||
import { Text } from 'rebass'
|
||||
|
||||
@@ -11,7 +11,7 @@ import useTheme from 'hooks/useTheme'
|
||||
import { transparentize } from 'polished'
|
||||
import { useCallback, useState } from 'react'
|
||||
import { AlertTriangle, ArrowLeft } from 'react-feather'
|
||||
import ReactGA from 'react-ga'
|
||||
import ReactGA from 'react-ga4'
|
||||
import { useAppDispatch } from 'state/hooks'
|
||||
import { enableList, removeList } from 'state/lists/actions'
|
||||
import { useAllLists } from 'state/lists/hooks'
|
||||
|
||||
@@ -9,7 +9,7 @@ import parseENSAddress from 'lib/utils/parseENSAddress'
|
||||
import uriToHttp from 'lib/utils/uriToHttp'
|
||||
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { CheckCircle, Settings } from 'react-feather'
|
||||
import ReactGA from 'react-ga'
|
||||
import ReactGA from 'react-ga4'
|
||||
import { usePopper } from 'react-popper'
|
||||
import { useAppDispatch, useAppSelector } from 'state/hooks'
|
||||
import styled from 'styled-components/macro'
|
||||
|
||||
@@ -5,7 +5,7 @@ import useActiveWeb3React from 'hooks/useActiveWeb3React'
|
||||
import { AUTO_ROUTER_SUPPORTED_CHAINS } from 'lib/hooks/routing/clientSideSmartOrderRouter'
|
||||
import { useContext, useRef, useState } from 'react'
|
||||
import { Settings, X } from 'react-feather'
|
||||
import ReactGA from 'react-ga'
|
||||
import ReactGA from 'react-ga4'
|
||||
import { Text } from 'rebass'
|
||||
import styled, { ThemeContext } from 'styled-components/macro'
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import Row, { AutoRow, RowBetween } from 'components/Row'
|
||||
import { useWalletConnectMonitoringEventCallback } from 'hooks/useMonitoringEventCallback'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { ArrowLeft, ArrowRight, Info } from 'react-feather'
|
||||
import ReactGA from 'react-ga'
|
||||
import ReactGA from 'react-ga4'
|
||||
import styled from 'styled-components/macro'
|
||||
import { AbstractConnector } from 'web3-react-abstract-connector'
|
||||
import { UnsupportedChainIdError, useWeb3React } from 'web3-react-core'
|
||||
|
||||
@@ -1,18 +1,13 @@
|
||||
import useActiveWeb3React from 'hooks/useActiveWeb3React'
|
||||
import { useEffect } from 'react'
|
||||
import ReactGA from 'react-ga'
|
||||
import ReactGA from 'react-ga4'
|
||||
import { RouteComponentProps } from 'react-router-dom'
|
||||
import { getCLS, getFCP, getFID, getLCP, Metric } from 'web-vitals'
|
||||
|
||||
import { GOOGLE_ANALYTICS_CLIENT_ID_STORAGE_KEY } from './index'
|
||||
|
||||
function reportWebVitals({ name, delta, id }: Metric) {
|
||||
ReactGA.timing({
|
||||
category: 'Web Vitals',
|
||||
variable: name,
|
||||
value: Math.round(name === 'CLS' ? delta * 1000 : delta),
|
||||
label: id,
|
||||
})
|
||||
ReactGA._gaCommandSendTiming('Web Vitals', name, Math.round(name === 'CLS' ? delta * 1000 : delta), id)
|
||||
}
|
||||
|
||||
// tracks web vitals and pageviews
|
||||
@@ -35,7 +30,7 @@ export default function GoogleAnalyticsReporter({ location: { pathname, search }
|
||||
}, [pathname, search])
|
||||
|
||||
useEffect(() => {
|
||||
// typed as 'any' in react-ga -.-
|
||||
// typed as 'any' in react-ga4 -.-
|
||||
ReactGA.ga((tracker: any) => {
|
||||
if (!tracker) return
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import ReactGA from 'react-ga'
|
||||
import ReactGA from 'react-ga4'
|
||||
import { isMobile } from 'utils/userAgent'
|
||||
|
||||
export const GOOGLE_ANALYTICS_CLIENT_ID_STORAGE_KEY = 'ga_client_id'
|
||||
@@ -23,5 +23,5 @@ if (typeof GOOGLE_ANALYTICS_ID === 'string') {
|
||||
: 'mobileRegular',
|
||||
})
|
||||
} else {
|
||||
ReactGA.initialize('test', { testMode: true, debug: true })
|
||||
ReactGA.initialize('test', { gtagOptions: { debug_mode: true } })
|
||||
}
|
||||
|
||||
35
src/components/swap/AutoRouterIcon.tsx
Normal file
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>
|
||||
)
|
||||
}
|
||||
@@ -4,7 +4,7 @@ import { AutoColumn } from 'components/Column'
|
||||
import { LoadingOpacityContainer } from 'components/Loader/styled'
|
||||
import { RowFixed } from 'components/Row'
|
||||
import { MouseoverTooltipContent } from 'components/Tooltip'
|
||||
import ReactGA from 'react-ga'
|
||||
import ReactGA from 'react-ga4'
|
||||
import { InterfaceTrade } from 'state/routing/types'
|
||||
import styled from 'styled-components/macro'
|
||||
import { ThemedText } from 'theme'
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -35,10 +35,7 @@ export const SUPPORTED_LOCALES = [
|
||||
]
|
||||
export type SupportedLocale = typeof SUPPORTED_LOCALES[number] | 'pseudo'
|
||||
|
||||
// eslint-disable-next-line import/first
|
||||
import * as enUS from 'locales/en-US'
|
||||
export const DEFAULT_LOCALE: SupportedLocale = 'en-US'
|
||||
export const DEFAULT_CATALOG = enUS
|
||||
|
||||
export const LOCALE_LABEL: { [locale in SupportedLocale]: string } = {
|
||||
'af-ZA': 'Afrikaans',
|
||||
|
||||
@@ -1108,6 +1108,13 @@
|
||||
"name": "Ukraine DAO",
|
||||
"symbol": "Ukraine",
|
||||
"decimals": 18
|
||||
},
|
||||
{
|
||||
"chainId": 1,
|
||||
"address": "0x8290D7a64F25e6b5002d98367E8367c1b532b534",
|
||||
"name": "oneUNI",
|
||||
"symbol": "oneUNI",
|
||||
"decimals": 18
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,23 +1,86 @@
|
||||
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 USDC_MAINNET = new Token(
|
||||
SupportedChainId.MAINNET,
|
||||
'0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48',
|
||||
6,
|
||||
'USDC',
|
||||
'USD//C'
|
||||
)
|
||||
export const USDC_ROPSTEN = new Token(
|
||||
SupportedChainId.ROPSTEN,
|
||||
'0x07865c6e87b9f70255377e024ace6630c1eaa37f',
|
||||
6,
|
||||
'USDC',
|
||||
'USD//C'
|
||||
)
|
||||
export const USDC_RINKEBY = new Token(
|
||||
SupportedChainId.RINKEBY,
|
||||
'0x4DBCdF9B62e891a7cec5A2568C3F4FAF9E8Abe2b',
|
||||
6,
|
||||
'tUSDC',
|
||||
'test USD//C'
|
||||
)
|
||||
export const USDC_GOERLI = new Token(
|
||||
SupportedChainId.GOERLI,
|
||||
'0x07865c6e87b9f70255377e024ace6630c1eaa37f',
|
||||
6,
|
||||
'USDC',
|
||||
'USD//C'
|
||||
)
|
||||
export const USDC_KOVAN = new Token(
|
||||
SupportedChainId.KOVAN,
|
||||
'0x31eeb2d0f9b6fd8642914ab10f4dd473677d80df',
|
||||
6,
|
||||
'USDC',
|
||||
'USD//C'
|
||||
)
|
||||
export const USDC_OPTIMISM = new Token(
|
||||
SupportedChainId.OPTIMISM,
|
||||
'0x7F5c764cBc14f9669B88837ca1490cCa17c31607',
|
||||
6,
|
||||
'USDC',
|
||||
'USD//C'
|
||||
)
|
||||
export const USDC_OPTIMISTIC_KOVAN = new Token(
|
||||
SupportedChainId.OPTIMISTIC_KOVAN,
|
||||
'0x3b8e53b3ab8e01fb57d0c9e893bc4d655aa67d84',
|
||||
6,
|
||||
'USDC',
|
||||
'USD//C'
|
||||
)
|
||||
export const USDC_ARBITRUM = new Token(
|
||||
SupportedChainId.ARBITRUM_ONE,
|
||||
'0xFF970A61A04b1cA14834A43f5dE4533eBDDB5CC8',
|
||||
6,
|
||||
'USDC',
|
||||
'USD//C'
|
||||
)
|
||||
export const USDC_ARBITRUM_RINKEBY = new Token(
|
||||
SupportedChainId.ARBITRUM_RINKEBY,
|
||||
'0x09b98f8b2395d076514037ff7d39a091a536206c',
|
||||
6,
|
||||
'USDC',
|
||||
'USD//C'
|
||||
)
|
||||
export const USDC_POLYGON = new Token(
|
||||
SupportedChainId.POLYGON,
|
||||
'0x2791bca1f2de4661ed88a30c99a7a9449aa84174',
|
||||
6,
|
||||
'USDC',
|
||||
'USD//C'
|
||||
)
|
||||
export const USDC_POLYGON_MUMBAI = new Token(
|
||||
SupportedChainId.POLYGON_MUMBAI,
|
||||
'0xe11a86849d99f524cac3e7a0ec1241828e332c62',
|
||||
6,
|
||||
'USDC',
|
||||
'USD//C'
|
||||
)
|
||||
|
||||
export const AMPL = new Token(
|
||||
SupportedChainId.MAINNET,
|
||||
@@ -55,7 +118,7 @@ export const USDC: { [chainId in SupportedChainId]: Token } = {
|
||||
[SupportedChainId.OPTIMISTIC_KOVAN]: USDC_OPTIMISTIC_KOVAN,
|
||||
[SupportedChainId.POLYGON]: USDC_POLYGON,
|
||||
[SupportedChainId.POLYGON_MUMBAI]: USDC_POLYGON_MUMBAI,
|
||||
[SupportedChainId.GOERLI]: USDC_GÖRLI,
|
||||
[SupportedChainId.GOERLI]: USDC_GOERLI,
|
||||
[SupportedChainId.RINKEBY]: USDC_RINKEBY,
|
||||
[SupportedChainId.KOVAN]: USDC_KOVAN,
|
||||
[SupportedChainId.ROPSTEN]: USDC_ROPSTEN,
|
||||
@@ -310,7 +373,7 @@ export const TOKEN_SHORTHANDS: { [shorthand: string]: { [chainId in SupportedCha
|
||||
[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.GOERLI]: USDC_GOERLI.address,
|
||||
[SupportedChainId.RINKEBY]: USDC_RINKEBY.address,
|
||||
[SupportedChainId.KOVAN]: USDC_KOVAN.address,
|
||||
[SupportedChainId.ROPSTEN]: USDC_ROPSTEN.address,
|
||||
|
||||
@@ -15,7 +15,7 @@ export default function useAddTokenToMetamask(currencyToAdd: Currency | undefine
|
||||
const logoURL = useCurrencyLogoURIs(token)[0]
|
||||
|
||||
const addToken = useCallback(() => {
|
||||
if (library && library.provider.isMetaMask && library.provider.request && token) {
|
||||
if (library && library?.provider?.isMetaMask && library.provider.request && token) {
|
||||
library.provider
|
||||
.request({
|
||||
method: 'wallet_watchAsset',
|
||||
|
||||
@@ -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]
|
||||
)
|
||||
}
|
||||
|
||||
@@ -28,26 +28,24 @@ export function useClientSideV3Trade<TTradeType extends TradeType>(
|
||||
amountSpecified?: CurrencyAmount<Currency>,
|
||||
otherCurrency?: Currency
|
||||
): { state: TradeState; trade: InterfaceTrade<Currency, Currency, TTradeType> | undefined } {
|
||||
const [currencyIn, currencyOut] = useMemo(
|
||||
() =>
|
||||
tradeType === TradeType.EXACT_INPUT
|
||||
? [amountSpecified?.currency, otherCurrency]
|
||||
: [otherCurrency, amountSpecified?.currency],
|
||||
[tradeType, amountSpecified, otherCurrency]
|
||||
)
|
||||
const [currencyIn, currencyOut] =
|
||||
tradeType === TradeType.EXACT_INPUT
|
||||
? [amountSpecified?.currency, otherCurrency]
|
||||
: [otherCurrency, amountSpecified?.currency]
|
||||
const { routes, loading: routesLoading } = useAllV3Routes(currencyIn, currencyOut)
|
||||
|
||||
const quoter = useV3Quoter()
|
||||
const { chainId } = useActiveWeb3React()
|
||||
const quotesResults = useSingleContractWithCallData(
|
||||
quoter,
|
||||
amountSpecified
|
||||
? routes.map((route) => SwapQuoter.quoteCallParameters(route, amountSpecified, tradeType).calldata)
|
||||
: [],
|
||||
{
|
||||
gasRequired: chainId ? QUOTE_GAS_OVERRIDES[chainId] ?? DEFAULT_GAS_QUOTE : undefined,
|
||||
}
|
||||
const callData = useMemo(
|
||||
() =>
|
||||
amountSpecified
|
||||
? routes.map((route) => SwapQuoter.quoteCallParameters(route, amountSpecified, tradeType).calldata)
|
||||
: [],
|
||||
[amountSpecified, routes, tradeType]
|
||||
)
|
||||
const quotesResults = useSingleContractWithCallData(quoter, callData, {
|
||||
gasRequired: chainId ? QUOTE_GAS_OVERRIDES[chainId] ?? DEFAULT_GAS_QUOTE : undefined,
|
||||
})
|
||||
|
||||
return useMemo(() => {
|
||||
if (
|
||||
|
||||
@@ -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])
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import { FeeAmount } from '@uniswap/v3-sdk'
|
||||
import useBlockNumber from 'lib/hooks/useBlockNumber'
|
||||
import ms from 'ms.macro'
|
||||
import { useMemo } from 'react'
|
||||
import ReactGA from 'react-ga'
|
||||
import ReactGA from 'react-ga4'
|
||||
import { useFeeTierDistributionQuery } from 'state/data/enhanced'
|
||||
import { FeeTierDistributionQuery } from 'state/data/generated'
|
||||
|
||||
@@ -112,9 +112,7 @@ function usePoolTVL(token0: Token | undefined, token1: Token | undefined) {
|
||||
}
|
||||
|
||||
if (latestBlock - (_meta?.block?.number ?? 0) > MAX_DATA_BLOCK_AGE) {
|
||||
ReactGA.exception({
|
||||
description: `Graph stale (latest block: ${latestBlock})`,
|
||||
})
|
||||
ReactGA.event('exception', { description: `Graph stale (latest block: ${latestBlock})` })
|
||||
|
||||
return {
|
||||
isLoading,
|
||||
|
||||
@@ -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])
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import { LocationDescriptor } from 'history'
|
||||
import useParsedQueryString from 'hooks/useParsedQueryString'
|
||||
import { stringify } from 'qs'
|
||||
import { useMemo } from 'react'
|
||||
import ReactGA from 'react-ga'
|
||||
import ReactGA from 'react-ga4'
|
||||
import { useLocation } from 'react-router-dom'
|
||||
|
||||
import { useActiveLocale } from './useActiveLocale'
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { skipToken } from '@reduxjs/toolkit/query/react'
|
||||
import { Currency } from '@uniswap/sdk-core'
|
||||
import { ChainId } from '@uniswap/smart-order-router'
|
||||
import { FeeAmount, nearestUsableTick, Pool, TICK_SPACINGS, tickToPrice } from '@uniswap/v3-sdk'
|
||||
import { SupportedChainId } from 'constants/chains'
|
||||
import { ZERO_ADDRESS } from 'constants/misc'
|
||||
import JSBI from 'jsbi'
|
||||
import { useSingleContractMultipleData } from 'lib/hooks/multicall'
|
||||
@@ -14,7 +14,7 @@ import { useTickLens } from './useContract'
|
||||
import { PoolState, usePool } from './usePools'
|
||||
|
||||
const PRICE_FIXED_DIGITS = 8
|
||||
const CHAIN_IDS_MISSING_SUBGRAPH_DATA = [ChainId.ARBITRUM_ONE, ChainId.ARBITRUM_RINKEBY]
|
||||
const CHAIN_IDS_MISSING_SUBGRAPH_DATA = [SupportedChainId.ARBITRUM_ONE, SupportedChainId.ARBITRUM_RINKEBY]
|
||||
|
||||
export interface TickData {
|
||||
tick: number
|
||||
|
||||
@@ -1,19 +1,81 @@
|
||||
import { Interface } from '@ethersproject/abi'
|
||||
import { Currency, Token } from '@uniswap/sdk-core'
|
||||
import IUniswapV3PoolStateJson from '@uniswap/v3-core/artifacts/contracts/interfaces/pool/IUniswapV3PoolState.sol/IUniswapV3PoolState.json'
|
||||
import { BigintIsh, Currency, Token } from '@uniswap/sdk-core'
|
||||
import { abi as IUniswapV3PoolStateABI } 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'
|
||||
|
||||
import { V3_CORE_FACTORY_ADDRESSES } from '../constants/addresses'
|
||||
import { IUniswapV3PoolStateInterface } from '../types/v3/IUniswapV3PoolState'
|
||||
|
||||
const { abi: IUniswapV3PoolStateABI } = IUniswapV3PoolStateJson
|
||||
|
||||
const POOL_STATE_INTERFACE = new Interface(IUniswapV3PoolStateABI) as IUniswapV3PoolStateInterface
|
||||
|
||||
// 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 +88,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]
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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])
|
||||
}
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
import { Interface } from '@ethersproject/abi'
|
||||
import { Currency, CurrencyAmount } from '@uniswap/sdk-core'
|
||||
import IUniswapV2PairJson from '@uniswap/v2-core/build/IUniswapV2Pair.json'
|
||||
import { abi as IUniswapV2PairABI } from '@uniswap/v2-core/build/IUniswapV2Pair.json'
|
||||
import { computePairAddress, Pair } from '@uniswap/v2-sdk'
|
||||
import { useMultipleContractSingleData } from 'lib/hooks/multicall'
|
||||
import { useMemo } from 'react'
|
||||
|
||||
import { V2_FACTORY_ADDRESSES } from '../constants/addresses'
|
||||
|
||||
const { abi: IUniswapV2PairABI } = IUniswapV2PairJson
|
||||
|
||||
const PAIR_INTERFACE = new Interface(IUniswapV2PairABI)
|
||||
|
||||
export enum PairState {
|
||||
|
||||
@@ -10,6 +10,10 @@
|
||||
{
|
||||
"name": "react-feather",
|
||||
"message": "Please import from lib/icons to ensure performant usage."
|
||||
},
|
||||
{
|
||||
"name": "@uniswap/smart-order-router",
|
||||
"message": "Forbidden import; smart-order-router is lazy-loaded."
|
||||
}
|
||||
],
|
||||
"patterns": [
|
||||
|
||||
@@ -5,10 +5,20 @@ import { ReactNode, useMemo } from 'react'
|
||||
import Button from './Button'
|
||||
import Row from './Row'
|
||||
|
||||
const fadeIn = keyframes`
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
`
|
||||
|
||||
const StyledButton = styled(Button)`
|
||||
border-radius: ${({ theme }) => theme.borderRadius}em;
|
||||
animation: ${fadeIn} 0.25s ease-in;
|
||||
border-radius: ${({ theme }) => theme.borderRadius * 0.75}em;
|
||||
flex-grow: 1;
|
||||
transition: background-color 0.25s ease-out, flex-grow 0.25s ease-out, padding 0.25s ease-out;
|
||||
transition: background-color 0.25s ease-out, border-radius 0.25s ease-out, flex-grow 0.25s ease-out;
|
||||
|
||||
:disabled {
|
||||
margin: -1px;
|
||||
@@ -35,11 +45,13 @@ const actionCss = css`
|
||||
|
||||
${ActionRow} {
|
||||
animation: ${grow} 0.25s ease-in;
|
||||
flex-grow: 1;
|
||||
justify-content: flex-start;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
${StyledButton} {
|
||||
border-radius: ${({ theme }) => theme.borderRadius * 0.75}em;
|
||||
border-radius: ${({ theme }) => theme.borderRadius}em;
|
||||
flex-grow: 0;
|
||||
padding: 1em;
|
||||
}
|
||||
@@ -57,8 +69,8 @@ export const Overlay = styled(Row)<{ hasAction: boolean }>`
|
||||
export interface Action {
|
||||
message: ReactNode
|
||||
icon?: Icon
|
||||
onClick: () => void
|
||||
children: ReactNode
|
||||
onClick?: () => void
|
||||
children?: ReactNode
|
||||
}
|
||||
|
||||
export interface BaseProps {
|
||||
@@ -72,11 +84,13 @@ export default function ActionButton({ color = 'accent', disabled, action, onCli
|
||||
const textColor = useMemo(() => (color === 'accent' && !disabled ? 'onAccent' : 'currentColor'), [color, disabled])
|
||||
return (
|
||||
<Overlay hasAction={Boolean(action)} flex align="stretch">
|
||||
<StyledButton color={color} disabled={disabled} onClick={action ? action.onClick : onClick}>
|
||||
<ThemedText.TransitionButton buttonSize={action ? 'medium' : 'large'} color={textColor}>
|
||||
{action ? action.children : children}
|
||||
</ThemedText.TransitionButton>
|
||||
</StyledButton>
|
||||
{(action ? action.onClick : true) && (
|
||||
<StyledButton color={color} disabled={disabled} onClick={action?.onClick || onClick}>
|
||||
<ThemedText.TransitionButton buttonSize={action ? 'medium' : 'large'} color={textColor}>
|
||||
{action?.children || children}
|
||||
</ThemedText.TransitionButton>
|
||||
</StyledButton>
|
||||
)}
|
||||
{action && (
|
||||
<ActionRow gap={0.5}>
|
||||
<LargeIcon color="currentColor" icon={action.icon || AlertTriangle} />
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import 'wicg-inert'
|
||||
|
||||
import useUnmount from 'lib/hooks/useUnmount'
|
||||
import { X } from 'lib/icons'
|
||||
import styled, { Color, Layer, ThemeProvider } from 'lib/theme'
|
||||
import { delayUnmountForAnimation } from 'lib/utils/animations'
|
||||
import { createContext, ReactElement, ReactNode, useContext, useEffect, useRef, useState } from 'react'
|
||||
import { createPortal } from 'react-dom'
|
||||
|
||||
@@ -97,8 +97,10 @@ export default function Dialog({ color, children, onClose = () => void 0 }: Dial
|
||||
context.setActive(true)
|
||||
return () => context.setActive(false)
|
||||
}, [context])
|
||||
const dialog = useRef<HTMLDivElement>(null)
|
||||
useUnmount(dialog)
|
||||
|
||||
const modal = useRef<HTMLDivElement>(null)
|
||||
useEffect(() => delayUnmountForAnimation(modal), [])
|
||||
|
||||
useEffect(() => {
|
||||
const close = (e: KeyboardEvent) => e.key === 'Escape' && onClose?.()
|
||||
document.addEventListener('keydown', close, true)
|
||||
@@ -108,9 +110,11 @@ export default function Dialog({ color, children, onClose = () => void 0 }: Dial
|
||||
context.element &&
|
||||
createPortal(
|
||||
<ThemeProvider>
|
||||
<Modal color={color} ref={dialog}>
|
||||
<OnCloseContext.Provider value={onClose}>{children}</OnCloseContext.Provider>
|
||||
</Modal>
|
||||
<OnCloseContext.Provider value={onClose}>
|
||||
<Modal color={color} ref={modal}>
|
||||
{children}
|
||||
</Modal>
|
||||
</OnCloseContext.Provider>
|
||||
</ThemeProvider>,
|
||||
context.element
|
||||
)
|
||||
|
||||
@@ -1,14 +1,10 @@
|
||||
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;
|
||||
@@ -35,7 +31,6 @@ export function StatusHeader({ icon: Icon, iconColor, iconSize = 4, children }:
|
||||
{children}
|
||||
</Column>
|
||||
</Column>
|
||||
<Rule />
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -49,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: 6.825em;
|
||||
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
|
||||
@@ -93,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}>
|
||||
@@ -105,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 userSelect>
|
||||
{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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -26,5 +26,5 @@ export default function WidgetsPropsValidator(props: PropsWithChildren<WidgetPro
|
||||
}
|
||||
}, [locale])
|
||||
|
||||
return <>{props.children}</>
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -26,10 +26,12 @@ export default function EtherscanLink({ data, type, color = 'currentColor', chil
|
||||
() => data && getExplorerLink(chainId || SupportedChainId.MAINNET, data, type),
|
||||
[chainId, data, type]
|
||||
)
|
||||
|
||||
return (
|
||||
<StyledExternalLink href={url} color={color} target="_blank">
|
||||
<Row gap={0.25}>
|
||||
{children} <Link />
|
||||
{children}
|
||||
{url && <Link />}
|
||||
</Row>
|
||||
</StyledExternalLink>
|
||||
)
|
||||
|
||||
66
src/lib/components/Expando.tsx
Normal file
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(transparent, ${({ theme }) => theme.dialog});
|
||||
bottom: 0;
|
||||
content: '';
|
||||
height: 0.75em;
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
width: calc(100% - 1em);
|
||||
}
|
||||
`
|
||||
|
||||
const InnerColumn = styled(Column)<{ height: number }>`
|
||||
height: ${({ height }) => height}em;
|
||||
padding: 0.5em 0;
|
||||
`
|
||||
|
||||
interface ExpandoProps {
|
||||
title: ReactNode
|
||||
open: boolean
|
||||
onExpand: () => void
|
||||
// The absolute height of the expanded container, in em.
|
||||
height: number
|
||||
}
|
||||
|
||||
/** A scrollable Expando with an absolute height. */
|
||||
export default function Expando({ title, open, onExpand, height, children }: PropsWithChildren<ExpandoProps>) {
|
||||
const [scrollingEl, setScrollingEl] = useState<HTMLDivElement | null>(null)
|
||||
const scrollbar = useScrollbar(scrollingEl)
|
||||
return (
|
||||
<Column>
|
||||
<HeaderColumn gap={open ? 0.5 : 0.75}>
|
||||
<Rule />
|
||||
<Row>
|
||||
{title}
|
||||
<IconButton color="secondary" onClick={onExpand} icon={ExpandoIcon} iconProps={{ open }} />
|
||||
</Row>
|
||||
<Rule />
|
||||
</HeaderColumn>
|
||||
<ExpandoColumn open={open} height={height}>
|
||||
<InnerColumn flex align="stretch" height={height} ref={setScrollingEl} css={scrollbar}>
|
||||
{children}
|
||||
</InnerColumn>
|
||||
</ExpandoColumn>
|
||||
</Column>
|
||||
)
|
||||
return null
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
import JSBI from 'jsbi'
|
||||
import styled, { css } from 'lib/theme'
|
||||
import { forwardRef, HTMLProps, useCallback, useEffect, useState } from 'react'
|
||||
import { ChangeEvent, forwardRef, HTMLProps, useCallback } from 'react'
|
||||
|
||||
const Input = styled.input`
|
||||
-webkit-appearance: textfield;
|
||||
@@ -77,42 +76,23 @@ interface EnforcedNumericInputProps extends NumericInputProps {
|
||||
enforcer: (nextUserInput: string) => string | null
|
||||
}
|
||||
|
||||
function isNumericallyEqual(a: string, b: string) {
|
||||
const [aInteger, aDecimal] = a.split('.')
|
||||
const [bInteger, bDecimal] = b.split('.')
|
||||
return (
|
||||
JSBI.equal(JSBI.BigInt(aInteger ?? 0), JSBI.BigInt(bInteger ?? 0)) &&
|
||||
JSBI.equal(JSBI.BigInt(aDecimal ?? 0), JSBI.BigInt(bDecimal ?? 0))
|
||||
)
|
||||
}
|
||||
|
||||
const NumericInput = forwardRef<HTMLInputElement, EnforcedNumericInputProps>(function NumericInput(
|
||||
{ value, onChange, enforcer, pattern, ...props }: EnforcedNumericInputProps,
|
||||
ref
|
||||
) {
|
||||
const [state, setState] = useState(value ?? '')
|
||||
useEffect(() => {
|
||||
if (!isNumericallyEqual(state, value)) {
|
||||
setState(value ?? '')
|
||||
}
|
||||
}, [value, state, setState])
|
||||
|
||||
const validateChange = useCallback(
|
||||
(event) => {
|
||||
const nextInput = enforcer(event.target.value.replace(/,/g, '.'))
|
||||
if (nextInput !== null) {
|
||||
setState(nextInput ?? '')
|
||||
if (!isNumericallyEqual(nextInput, value)) {
|
||||
onChange(nextInput)
|
||||
}
|
||||
(event: ChangeEvent<HTMLInputElement>) => {
|
||||
const nextInput = enforcer(event.target.value.replace(/,/g, '.'))?.replace(/^0+$/, '0')
|
||||
if (nextInput !== undefined) {
|
||||
onChange(nextInput)
|
||||
}
|
||||
},
|
||||
[value, onChange, enforcer]
|
||||
[enforcer, onChange]
|
||||
)
|
||||
|
||||
return (
|
||||
<Input
|
||||
value={state}
|
||||
value={value}
|
||||
onChange={validateChange}
|
||||
// universal input options
|
||||
inputMode="decimal"
|
||||
|
||||
@@ -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,5 @@
|
||||
import { useLingui } from '@lingui/react'
|
||||
import { Currency, CurrencyAmount } from '@uniswap/sdk-core'
|
||||
import { useUSDCValue } from 'hooks/useUSDCPrice'
|
||||
import { loadingTransitionCss } from 'lib/css/loading'
|
||||
import {
|
||||
useIsSwapFieldIndependent,
|
||||
@@ -70,18 +69,16 @@ export function useFormattedFieldAmount({ disabled, currencyAmount, fieldAmount
|
||||
export default function Input({ disabled, focused }: InputProps) {
|
||||
const { i18n } = useLingui()
|
||||
const {
|
||||
currencyBalances: { [Field.INPUT]: balance },
|
||||
[Field.INPUT]: { balance, amount: tradeCurrencyAmount, usdc },
|
||||
trade: { state: tradeState },
|
||||
tradeCurrencyAmounts: { [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)
|
||||
@@ -99,30 +96,30 @@ export default function Input({ disabled, focused }: InputProps) {
|
||||
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: swapInputCurrencyAmount,
|
||||
fieldAmount: swapInputAmount,
|
||||
currencyAmount: tradeCurrencyAmount,
|
||||
fieldAmount: inputAmount,
|
||||
})
|
||||
|
||||
return (
|
||||
<InputColumn gap={0.5} approved={mockApproved}>
|
||||
<TokenInput
|
||||
currency={swapInputCurrency}
|
||||
currency={inputCurrency}
|
||||
amount={amount}
|
||||
max={max}
|
||||
disabled={disabled}
|
||||
onChangeInput={updateSwapInputAmount}
|
||||
onChangeCurrency={updateSwapInputCurrency}
|
||||
onChangeInput={updateInputAmount}
|
||||
onChangeCurrency={updateInputCurrency}
|
||||
loading={isLoading}
|
||||
>
|
||||
<ThemedText.Body2 color="secondary" userSelect>
|
||||
<Row>
|
||||
<USDC isLoading={isRouteLoading}>{inputUSDC ? `$${inputUSDC.toFixed(2)}` : '-'}</USDC>
|
||||
<USDC isLoading={isRouteLoading}>{usdc ? `$${formatCurrencyAmount(usdc, 6, 'en', 2)}` : '-'}</USDC>
|
||||
{balance && (
|
||||
<Balance color={balanceColor} focused={focused}>
|
||||
Balance: <span>{formatCurrencyAmount(balance, 4, i18n.locale)}</span>
|
||||
|
||||
@@ -5,13 +5,11 @@ import { useAtomValue } from 'jotai/utils'
|
||||
import BrandedFooter from 'lib/components/BrandedFooter'
|
||||
import { useIsSwapFieldIndependent, useSwapAmount, useSwapCurrency, useSwapInfo } from 'lib/hooks/swap'
|
||||
import useCurrencyColor from 'lib/hooks/useCurrencyColor'
|
||||
import useUSDCPriceImpact, { toHumanReadablePriceImpact } from 'lib/hooks/useUSDCPriceImpact'
|
||||
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 { formatCurrencyAmount } from 'utils/formatCurrencyAmount'
|
||||
import { getPriceImpactWarning } from 'utils/prices'
|
||||
|
||||
import Column from '../Column'
|
||||
import Row from '../Row'
|
||||
@@ -40,9 +38,9 @@ export default function Output({ disabled, focused, children }: PropsWithChildre
|
||||
const { i18n } = useLingui()
|
||||
|
||||
const {
|
||||
currencyBalances: { [Field.OUTPUT]: balance },
|
||||
[Field.OUTPUT]: { balance, amount: outputCurrencyAmount, usdc: outputUSDC },
|
||||
trade: { state: tradeState },
|
||||
tradeCurrencyAmounts: { [Field.INPUT]: inputCurrencyAmount, [Field.OUTPUT]: outputCurrencyAmount },
|
||||
impact,
|
||||
} = useSwapInfo()
|
||||
|
||||
const [swapOutputAmount, updateSwapOutputAmount] = useSwapAmount(Field.OUTPUT)
|
||||
@@ -59,9 +57,6 @@ 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 { outputUSDC, priceImpact } = useUSDCPriceImpact(inputCurrencyAmount, outputCurrencyAmount)
|
||||
const priceImpactWarning = useMemo(() => getPriceImpactWarning(priceImpact), [priceImpact])
|
||||
|
||||
const amount = useFormattedFieldAmount({
|
||||
disabled,
|
||||
currencyAmount: outputCurrencyAmount,
|
||||
@@ -87,12 +82,8 @@ export default function Output({ disabled, focused, children }: PropsWithChildre
|
||||
<ThemedText.Body2 color="secondary" userSelect>
|
||||
<Row>
|
||||
<USDC gap={0.5} isLoading={isRouteLoading}>
|
||||
{outputUSDC ? `$${outputUSDC.toFixed(2)}` : '-'}{' '}
|
||||
{priceImpact && (
|
||||
<ThemedText.Body2 color={priceImpactWarning}>
|
||||
({toHumanReadablePriceImpact(priceImpact)})
|
||||
</ThemedText.Body2>
|
||||
)}
|
||||
{outputUSDC ? `$${formatCurrencyAmount(outputUSDC, 6, 'en', 2)}` : '-'}{' '}
|
||||
{impact && <ThemedText.Body2 color={impact.warning}>({impact.toString()})</ThemedText.Body2>}
|
||||
</USDC>
|
||||
{balance && (
|
||||
<Balance focused={focused}>
|
||||
|
||||
54
src/lib/components/Swap/Price.tsx
Normal file
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>
|
||||
)
|
||||
}
|
||||
@@ -2,7 +2,7 @@ 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'
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import ErrorDialog, { StatusHeader } from 'lib/components/Error/ErrorDialog'
|
||||
import EtherscanLink from 'lib/components/EtherscanLink'
|
||||
import Rule from 'lib/components/Rule'
|
||||
import SwapSummary from 'lib/components/Swap/Summary'
|
||||
import useInterval from 'lib/hooks/useInterval'
|
||||
import { CheckCircle, Clock, Spinner } from 'lib/icons'
|
||||
@@ -84,6 +85,7 @@ function TransactionStatus({ tx, onClose }: TransactionStatusProps) {
|
||||
<SwapSummary input={tx.info.inputCurrencyAmount} output={tx.info.outputCurrencyAmount} />
|
||||
) : null}
|
||||
</StatusHeader>
|
||||
<Rule />
|
||||
<TransactionRow flex>
|
||||
<ThemedText.ButtonSmall>
|
||||
<EtherscanLink type={ExplorerDataType.TRANSACTION} data={tx.info.response.hash}>
|
||||
|
||||
@@ -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={async () => void 0}
|
||||
trade={trade}
|
||||
slippage={slippage}
|
||||
inputUSDC={inputUSDC}
|
||||
outputUSDC={outputUSDC}
|
||||
impact={impact}
|
||||
/>
|
||||
</Modal>
|
||||
) : null
|
||||
}
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
import { t } from '@lingui/macro'
|
||||
import { useLingui } from '@lingui/react'
|
||||
import { Trade } from '@uniswap/router-sdk'
|
||||
import { Currency, Percent, TradeType } from '@uniswap/sdk-core'
|
||||
import { Currency, 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 { Slippage } from 'lib/hooks/useSlippage'
|
||||
import { PriceImpact } from 'lib/hooks/useUSDCPriceImpact'
|
||||
import { feeOptionsAtom } from 'lib/state/swap'
|
||||
import styled, { Color, ThemedText } from 'lib/theme'
|
||||
import { useMemo } from 'react'
|
||||
import { currencyId } from 'utils/currencyId'
|
||||
import { formatCurrencyAmount } from 'utils/formatCurrencyAmount'
|
||||
import { computeRealizedLPFeeAmount, 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]};
|
||||
@@ -37,16 +38,16 @@ function Detail({ label, value, color }: DetailProps) {
|
||||
|
||||
interface DetailsProps {
|
||||
trade: Trade<Currency, Currency, TradeType>
|
||||
allowedSlippage: Percent
|
||||
slippage: Slippage
|
||||
impact?: PriceImpact
|
||||
}
|
||||
|
||||
export default function Details({ trade, allowedSlippage }: DetailsProps) {
|
||||
export default function Details({ trade, slippage, impact }: DetailsProps) {
|
||||
const { inputAmount, outputAmount } = trade
|
||||
const inputCurrency = inputAmount.currency
|
||||
const outputCurrency = outputAmount.currency
|
||||
const integrator = window.location.hostname
|
||||
const feeOptions = useAtomValue(feeOptionsAtom)
|
||||
const priceImpact = useMemo(() => computeRealizedPriceImpact(trade), [trade])
|
||||
const lpFeeAmount = useMemo(() => computeRealizedLPFeeAmount(trade), [trade])
|
||||
const { i18n } = useLingui()
|
||||
|
||||
@@ -62,7 +63,9 @@ export default function Details({ trade, allowedSlippage }: DetailsProps) {
|
||||
}
|
||||
}
|
||||
|
||||
rows.push([t`Price impact`, `${priceImpact.toFixed(2)}%`, getPriceImpactWarning(priceImpact)])
|
||||
if (impact) {
|
||||
rows.push([t`Price impact`, impact.toString(), impact.warning])
|
||||
}
|
||||
|
||||
if (lpFeeAmount) {
|
||||
const parsedLpFee = formatCurrencyAmount(lpFeeAmount, 6, i18n.locale)
|
||||
@@ -70,36 +73,36 @@ 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
|
||||
}, [
|
||||
feeOptions,
|
||||
priceImpact,
|
||||
lpFeeAmount,
|
||||
trade,
|
||||
allowedSlippage,
|
||||
outputAmount,
|
||||
i18n.locale,
|
||||
integrator,
|
||||
outputCurrency,
|
||||
impact,
|
||||
inputCurrency,
|
||||
integrator,
|
||||
lpFeeAmount,
|
||||
outputAmount,
|
||||
outputCurrency,
|
||||
slippage,
|
||||
trade,
|
||||
])
|
||||
|
||||
return (
|
||||
<>
|
||||
<Column gap={0.5}>
|
||||
{details.map(([label, detail, color]) => (
|
||||
<Detail key={label} label={label} value={detail} color={color} />
|
||||
))}
|
||||
</>
|
||||
</Column>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import { useLingui } from '@lingui/react'
|
||||
import { Currency, CurrencyAmount, Percent, Token } from '@uniswap/sdk-core'
|
||||
import useUSDCPriceImpact, { toHumanReadablePriceImpact } from 'lib/hooks/useUSDCPriceImpact'
|
||||
import { Currency, CurrencyAmount } from '@uniswap/sdk-core'
|
||||
import { PriceImpact } from 'lib/hooks/useUSDCPriceImpact'
|
||||
import { ArrowRight } from 'lib/icons'
|
||||
import { ThemedText } from 'lib/theme'
|
||||
import { useMemo } from 'react'
|
||||
import { PropsWithChildren } from 'react'
|
||||
import { formatCurrencyAmount } from 'utils/formatCurrencyAmount'
|
||||
import { getPriceImpactWarning } from 'utils/prices'
|
||||
|
||||
import Column from '../../Column'
|
||||
import Row from '../../Row'
|
||||
@@ -13,13 +12,11 @@ import TokenImg from '../../TokenImg'
|
||||
|
||||
interface TokenValueProps {
|
||||
input: CurrencyAmount<Currency>
|
||||
usdc?: CurrencyAmount<Token>
|
||||
priceImpact?: Percent
|
||||
usdc?: CurrencyAmount<Currency>
|
||||
}
|
||||
|
||||
function TokenValue({ input, usdc, priceImpact }: TokenValueProps) {
|
||||
function TokenValue({ input, usdc, children }: PropsWithChildren<TokenValueProps>) {
|
||||
const { i18n } = useLingui()
|
||||
const priceImpactWarning = useMemo(() => getPriceImpactWarning(priceImpact), [priceImpact])
|
||||
return (
|
||||
<Column justify="flex-start">
|
||||
<Row gap={0.375} justify="flex-start">
|
||||
@@ -29,16 +26,12 @@ function TokenValue({ input, usdc, priceImpact }: TokenValueProps) {
|
||||
</ThemedText.Body2>
|
||||
</Row>
|
||||
{usdc && (
|
||||
<Row justify="flex-start">
|
||||
<ThemedText.Caption color="secondary" userSelect>
|
||||
${formatCurrencyAmount(usdc, 2, i18n.locale)}
|
||||
{priceImpact && (
|
||||
<ThemedText.Caption color={priceImpactWarning}>
|
||||
({toHumanReadablePriceImpact(priceImpact)})
|
||||
</ThemedText.Caption>
|
||||
)}
|
||||
</ThemedText.Caption>
|
||||
</Row>
|
||||
<ThemedText.Caption color="secondary" userSelect>
|
||||
<Row justify="flex-start" gap={0.25}>
|
||||
${formatCurrencyAmount(usdc, 6, 'en', 2)}
|
||||
{children}
|
||||
</Row>
|
||||
</ThemedText.Caption>
|
||||
)}
|
||||
</Column>
|
||||
)
|
||||
@@ -47,17 +40,19 @@ function TokenValue({ input, usdc, priceImpact }: TokenValueProps) {
|
||||
interface SummaryProps {
|
||||
input: CurrencyAmount<Currency>
|
||||
output: CurrencyAmount<Currency>
|
||||
showUSDC?: true
|
||||
inputUSDC?: CurrencyAmount<Currency>
|
||||
outputUSDC?: CurrencyAmount<Currency>
|
||||
impact?: PriceImpact
|
||||
}
|
||||
|
||||
export default function Summary({ input, output, showUSDC }: SummaryProps) {
|
||||
const { inputUSDC, outputUSDC, priceImpact } = useUSDCPriceImpact(input, output)
|
||||
|
||||
export default function Summary({ input, output, inputUSDC, outputUSDC, impact }: SummaryProps) {
|
||||
return (
|
||||
<Row gap={showUSDC ? 1 : 0.25}>
|
||||
<TokenValue input={input} usdc={showUSDC && inputUSDC} />
|
||||
<Row gap={impact ? 1 : 0.25}>
|
||||
<TokenValue input={input} usdc={inputUSDC} />
|
||||
<ArrowRight />
|
||||
<TokenValue input={output} usdc={showUSDC && outputUSDC} priceImpact={priceImpact} />
|
||||
<TokenValue input={output} usdc={outputUSDC}>
|
||||
{impact && <ThemedText.Caption color={impact.warning}>({impact.toString()})</ThemedText.Caption>}
|
||||
</TokenValue>
|
||||
</Row>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,121 +1,127 @@
|
||||
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 { Currency, CurrencyAmount, TradeType } from '@uniswap/sdk-core'
|
||||
import ActionButton, { Action } from 'lib/components/ActionButton'
|
||||
import Column from 'lib/components/Column'
|
||||
import { Header } from 'lib/components/Dialog'
|
||||
import Expando from 'lib/components/Expando'
|
||||
import Row from 'lib/components/Row'
|
||||
import { Slippage } from 'lib/hooks/useSlippage'
|
||||
import { PriceImpact } from 'lib/hooks/useUSDCPriceImpact'
|
||||
import { AlertTriangle, BarChart, Info, Spinner } from 'lib/icons'
|
||||
import styled, { ThemedText } from 'lib/theme'
|
||||
import formatLocaleNumber from 'lib/utils/formatLocaleNumber'
|
||||
import { useMemo, useState } from 'react'
|
||||
import { formatCurrencyAmount, formatPrice } from 'utils/formatCurrencyAmount'
|
||||
import { computeRealizedPriceImpact, getPriceImpactWarning } from 'utils/prices'
|
||||
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({ impact, slippage }: { impact?: PriceImpact; slippage: Slippage }) {
|
||||
return (
|
||||
<Row gap={0.5}>
|
||||
{impact?.warning || slippage.warning ? (
|
||||
<AlertTriangle color={impact?.warning || slippage.warning} />
|
||||
) : (
|
||||
<Info color="secondary" />
|
||||
)}
|
||||
<ThemedText.Subhead2 color={impact?.warning || slippage.warning || 'secondary'}>
|
||||
{impact?.warning ? (
|
||||
<Trans>High price impact</Trans>
|
||||
) : slippage.warning ? (
|
||||
<Trans>High slippage</Trans>
|
||||
) : (
|
||||
<Trans>Swap details</Trans>
|
||||
)}
|
||||
</ThemedText.Subhead2>
|
||||
</Row>
|
||||
)
|
||||
}
|
||||
|
||||
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: () => Promise<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 [isPending, setIsPending] = useState(false)
|
||||
const onClick = useCallback(async () => {
|
||||
setIsPending(true)
|
||||
await onConfirm()
|
||||
setIsPending(false)
|
||||
}, [onConfirm])
|
||||
|
||||
const action = useMemo((): Action | undefined => {
|
||||
if (doesTradeDiffer) {
|
||||
if (isPending) {
|
||||
return { message: <Trans>Confirm in your wallet</Trans>, icon: Spinner }
|
||||
} else 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,62 +129,53 @@ export function SummaryDialog({ trade, allowedSlippage, onConfirm }: SummaryDial
|
||||
}
|
||||
}
|
||||
return
|
||||
}, [ackPriceImpact, doesTradeDiffer, priceImpact, trade])
|
||||
}, [ackPriceImpact, doesTradeDiffer, highPriceImpact, isPending, trade])
|
||||
|
||||
if (!(inputAmount && outputAmount && inputCurrency && outputCurrency)) {
|
||||
return null
|
||||
}
|
||||
return (
|
||||
<ActionButton onClick={onClick} action={action}>
|
||||
<Trans>Confirm swap</Trans>
|
||||
</ActionButton>
|
||||
)
|
||||
}
|
||||
|
||||
interface SummaryDialogProps {
|
||||
trade: Trade<Currency, Currency, TradeType>
|
||||
slippage: Slippage
|
||||
inputUSDC?: CurrencyAmount<Currency>
|
||||
outputUSDC?: CurrencyAmount<Currency>
|
||||
impact?: PriceImpact
|
||||
onConfirm: () => Promise<void>
|
||||
}
|
||||
|
||||
export function SummaryDialog({ trade, slippage, inputUSDC, outputUSDC, impact, onConfirm }: SummaryDialogProps) {
|
||||
const { inputAmount, outputAmount } = trade
|
||||
|
||||
const [open, setOpen] = useState(false)
|
||||
const onExpand = useCallback(() => setOpen((open) => !open), [])
|
||||
|
||||
return (
|
||||
<>
|
||||
<Header title={<Trans>Swap summary</Trans>} ruled />
|
||||
<Body flex align="stretch" gap={0.75} padded open={open}>
|
||||
<SummaryColumn gap={0.75} flex justify="center">
|
||||
<Summary input={inputAmount} output={outputAmount} showUSDC />
|
||||
<Row>
|
||||
<ThemedText.Caption userSelect>
|
||||
{formatLocaleNumber({ number: 1, sigFigs: 1, locale: i18n.locale })} {inputCurrency.symbol} ={' '}
|
||||
{formatPrice(executionPrice, 6, i18n.locale)} {outputCurrency.symbol}
|
||||
</ThemedText.Caption>
|
||||
</Row>
|
||||
</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}
|
||||
impact={impact}
|
||||
/>
|
||||
<Price trade={trade} />
|
||||
</Heading>
|
||||
<Column gap={open ? 0 : 0.75} style={{ transition: 'gap 0.25s' }}>
|
||||
<Expando title={<Subhead impact={impact} slippage={slippage} />} open={open} onExpand={onExpand} height={7}>
|
||||
<Details trade={trade} slippage={slippage} impact={impact} />
|
||||
</Expando>
|
||||
<Footing>
|
||||
<Estimate trade={trade} slippage={slippage} />
|
||||
</Footing>
|
||||
<ConfirmButton trade={trade} highPriceImpact={impact?.warning === 'error'} onConfirm={onConfirm} />
|
||||
</Column>
|
||||
</Body>
|
||||
</>
|
||||
)
|
||||
|
||||
@@ -1,237 +0,0 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import { Token } from '@uniswap/sdk-core'
|
||||
import { useUpdateAtom } from 'jotai/utils'
|
||||
import { WrapErrorText } from 'lib/components/Swap/WrapErrorText'
|
||||
import { useSwapCurrencyAmount, useSwapInfo, useSwapTradeType } from 'lib/hooks/swap'
|
||||
import {
|
||||
ApproveOrPermitState,
|
||||
useApproveOrPermit,
|
||||
useSwapApprovalOptimizedTrade,
|
||||
useSwapRouterAddress,
|
||||
} from 'lib/hooks/swap/useSwapApproval'
|
||||
import { useSwapCallback } from 'lib/hooks/swap/useSwapCallback'
|
||||
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 { Spinner } from 'lib/icons'
|
||||
import { displayTxHashAtom, Field } from 'lib/state/swap'
|
||||
import { TransactionType } from 'lib/state/transactions'
|
||||
import { useTheme } from 'lib/theme'
|
||||
import { 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 { SummaryDialog } from './Summary'
|
||||
|
||||
interface SwapButtonProps {
|
||||
disabled?: boolean
|
||||
}
|
||||
|
||||
function useIsPendingApproval(token?: Token, spender?: string): boolean {
|
||||
return Boolean(usePendingApproval(token, spender))
|
||||
}
|
||||
|
||||
export default function SwapButton({ disabled }: SwapButtonProps) {
|
||||
const { account, chainId } = useActiveWeb3React()
|
||||
|
||||
const { tokenColorExtraction } = useTheme()
|
||||
|
||||
const {
|
||||
allowedSlippage,
|
||||
currencies: { [Field.INPUT]: inputCurrency },
|
||||
currencyBalances: { [Field.INPUT]: inputCurrencyBalance },
|
||||
feeOptions,
|
||||
trade,
|
||||
tradeCurrencyAmounts: { [Field.INPUT]: inputTradeCurrencyAmount, [Field.OUTPUT]: outputTradeCurrencyAmount },
|
||||
} = useSwapInfo()
|
||||
|
||||
const tradeType = useSwapTradeType()
|
||||
|
||||
const [activeTrade, setActiveTrade] = useState<typeof trade.trade | undefined>()
|
||||
useEffect(() => {
|
||||
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
|
||||
|
||||
const approvalCurrencyAmount = useSwapCurrencyAmount(Field.INPUT)
|
||||
|
||||
const { approvalState, signatureData, handleApproveOrPermit } = useApproveOrPermit(
|
||||
optimizedTrade,
|
||||
allowedSlippage,
|
||||
useIsPendingApproval,
|
||||
approvalCurrencyAmount
|
||||
)
|
||||
|
||||
const approvalHash = usePendingApproval(
|
||||
inputCurrency?.isToken ? inputCurrency : undefined,
|
||||
useSwapRouterAddress(optimizedTrade)
|
||||
)
|
||||
|
||||
const addTransaction = useAddTransaction()
|
||||
const onApprove = useCallback(() => {
|
||||
handleApproveOrPermit().then((transaction) => {
|
||||
if (transaction) {
|
||||
addTransaction({ type: TransactionType.APPROVAL, ...transaction })
|
||||
}
|
||||
})
|
||||
}, [addTransaction, handleApproveOrPermit])
|
||||
|
||||
const { type: wrapType, callback: wrapCallback, error: wrapError, loading: wrapLoading } = useWrapCallback()
|
||||
|
||||
const disableSwap = useMemo(
|
||||
() =>
|
||||
disabled ||
|
||||
!chainId ||
|
||||
wrapLoading ||
|
||||
(wrapType !== WrapType.NOT_APPLICABLE && wrapError) ||
|
||||
approvalState === ApproveOrPermitState.PENDING_SIGNATURE ||
|
||||
!(inputTradeCurrencyAmount && inputCurrencyBalance) ||
|
||||
inputCurrencyBalance.lessThan(inputTradeCurrencyAmount),
|
||||
[disabled, chainId, wrapLoading, wrapType, wrapError, approvalState, inputTradeCurrencyAmount, inputCurrencyBalance]
|
||||
)
|
||||
|
||||
const actionProps = useMemo((): Partial<ActionButtonProps> | undefined => {
|
||||
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>
|
||||
),
|
||||
onClick: onApprove,
|
||||
children:
|
||||
approvalState === ApproveOrPermitState.REQUIRES_SIGNATURE ? <Trans>Allow</Trans> : <Trans>Approve</Trans>,
|
||||
},
|
||||
}
|
||||
}
|
||||
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()
|
||||
|
||||
// the callback to execute the swap
|
||||
const { callback: swapCallback } = useSwapCallback({
|
||||
trade: optimizedTrade,
|
||||
allowedSlippage,
|
||||
recipientAddressOrName: account ?? null,
|
||||
signatureData,
|
||||
deadline,
|
||||
feeOptions,
|
||||
})
|
||||
|
||||
//@TODO(ianlapham): add a loading state, process errors
|
||||
const setDisplayTxHash = useUpdateAtom(displayTxHashAtom)
|
||||
|
||||
const onConfirm = useCallback(() => {
|
||||
swapCallback?.()
|
||||
.then((response) => {
|
||||
setDisplayTxHash(response.hash)
|
||||
invariant(inputTradeCurrencyAmount && outputTradeCurrencyAmount)
|
||||
addTransaction({
|
||||
response,
|
||||
type: TransactionType.SWAP,
|
||||
tradeType,
|
||||
inputCurrencyAmount: inputTradeCurrencyAmount,
|
||||
outputCurrencyAmount: outputTradeCurrencyAmount,
|
||||
})
|
||||
})
|
||||
.catch((error) => {
|
||||
//@TODO(ianlapham): add error handling
|
||||
console.log(error)
|
||||
})
|
||||
.finally(() => {
|
||||
setActiveTrade(undefined)
|
||||
})
|
||||
}, [addTransaction, inputTradeCurrencyAmount, outputTradeCurrencyAmount, setDisplayTxHash, swapCallback, tradeType])
|
||||
|
||||
const ButtonText = useCallback(() => {
|
||||
if (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={handleActionButtonClick}
|
||||
{...actionProps}
|
||||
>
|
||||
<ButtonText />
|
||||
</ActionButton>
|
||||
{activeTrade && (
|
||||
<Dialog color="dialog" onClose={handleDialogClose}>
|
||||
<SummaryDialog trade={activeTrade} allowedSlippage={allowedSlippage} onConfirm={onConfirm} />
|
||||
</Dialog>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
195
src/lib/components/Swap/SwapButton/index.tsx
Normal file
195
src/lib/components/Swap/SwapButton/index.tsx
Normal file
@@ -0,0 +1,195 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import { useAtomValue, useUpdateAtom } from 'jotai/utils'
|
||||
import { useSwapInfo } from 'lib/hooks/swap'
|
||||
import { useSwapApprovalOptimizedTrade } from 'lib/hooks/swap/useSwapApproval'
|
||||
import { useSwapCallback } from 'lib/hooks/swap/useSwapCallback'
|
||||
import useWrapCallback, { WrapType } from 'lib/hooks/swap/useWrapCallback'
|
||||
import { useAddTransaction } from 'lib/hooks/transactions'
|
||||
import useActiveWeb3React from 'lib/hooks/useActiveWeb3React'
|
||||
import { useSetOldestValidBlock } from 'lib/hooks/useIsValidBlock'
|
||||
import useTransactionDeadline from 'lib/hooks/useTransactionDeadline'
|
||||
import { Spinner } from 'lib/icons'
|
||||
import { displayTxHashAtom, feeOptionsAtom, Field } from 'lib/state/swap'
|
||||
import { TransactionType } from 'lib/state/transactions'
|
||||
import { useTheme } from 'lib/theme'
|
||||
import { isAnimating } from 'lib/utils/animations'
|
||||
import { memo, useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import invariant from 'tiny-invariant'
|
||||
|
||||
import ActionButton, { ActionButtonProps } from '../../ActionButton'
|
||||
import Dialog from '../../Dialog'
|
||||
import { SummaryDialog } from '../Summary'
|
||||
import useApprovalData, { useIsPendingApproval } from './useApprovalData'
|
||||
|
||||
interface SwapButtonProps {
|
||||
disabled?: boolean
|
||||
}
|
||||
|
||||
export default memo(function SwapButton({ disabled }: SwapButtonProps) {
|
||||
const { account, chainId } = useActiveWeb3React()
|
||||
const {
|
||||
[Field.INPUT]: {
|
||||
currency: inputCurrency,
|
||||
amount: inputCurrencyAmount,
|
||||
balance: inputCurrencyBalance,
|
||||
usdc: inputUSDC,
|
||||
},
|
||||
[Field.OUTPUT]: { usdc: outputUSDC },
|
||||
trade,
|
||||
slippage,
|
||||
impact,
|
||||
} = useSwapInfo()
|
||||
const feeOptions = useAtomValue(feeOptionsAtom)
|
||||
|
||||
// TODO(zzmp): Return an optimized trade directly from useSwapInfo.
|
||||
const optimizedTrade =
|
||||
// Use trade.trade if there is no swap optimized trade. This occurs if approvals are still pending.
|
||||
useSwapApprovalOptimizedTrade(trade.trade, slippage.allowed, useIsPendingApproval) || trade.trade
|
||||
const deadline = useTransactionDeadline()
|
||||
|
||||
const { type: wrapType, callback: wrapCallback } = useWrapCallback()
|
||||
const { approvalAction, signatureData } = useApprovalData(optimizedTrade, slippage, inputCurrencyAmount)
|
||||
const { callback: swapCallback } = useSwapCallback({
|
||||
trade: optimizedTrade,
|
||||
allowedSlippage: slippage.allowed,
|
||||
recipientAddressOrName: account ?? null,
|
||||
signatureData,
|
||||
deadline,
|
||||
feeOptions,
|
||||
})
|
||||
|
||||
const [open, setOpen] = useState(false)
|
||||
// Close the review modal if there is no available trade.
|
||||
useEffect(() => setOpen((open) => (trade.trade ? open : false)), [trade.trade])
|
||||
// Close the review modal on chain change.
|
||||
useEffect(() => setOpen(false), [chainId])
|
||||
|
||||
const addTransaction = useAddTransaction()
|
||||
const setDisplayTxHash = useUpdateAtom(displayTxHashAtom)
|
||||
const setOldestValidBlock = useSetOldestValidBlock()
|
||||
|
||||
const [isPending, setIsPending] = useState(false)
|
||||
const onWrap = useCallback(async () => {
|
||||
setIsPending(true)
|
||||
try {
|
||||
const transaction = await wrapCallback?.()
|
||||
if (!transaction) return
|
||||
addTransaction({
|
||||
response: transaction,
|
||||
type: TransactionType.WRAP,
|
||||
unwrapped: wrapType === WrapType.UNWRAP,
|
||||
currencyAmountRaw: transaction.value?.toString() ?? '0',
|
||||
chainId,
|
||||
})
|
||||
setDisplayTxHash(transaction.hash)
|
||||
} catch (e) {
|
||||
// TODO(zzmp): Surface errors from wrap.
|
||||
console.log(e)
|
||||
}
|
||||
|
||||
// Only reset pending after any queued animations to avoid layout thrashing, because a
|
||||
// successful wrap will open the status dialog and immediately cover the button.
|
||||
const postWrap = () => {
|
||||
setIsPending(false)
|
||||
document.removeEventListener('animationend', postWrap)
|
||||
}
|
||||
if (isAnimating(document)) {
|
||||
document.addEventListener('animationend', postWrap)
|
||||
} else {
|
||||
postWrap()
|
||||
}
|
||||
}, [addTransaction, chainId, setDisplayTxHash, wrapCallback, wrapType])
|
||||
// Reset the pending state if user updates the swap.
|
||||
useEffect(() => setIsPending(false), [inputCurrencyAmount, trade])
|
||||
|
||||
const onSwap = useCallback(async () => {
|
||||
try {
|
||||
const transaction = await swapCallback?.()
|
||||
if (!transaction) return
|
||||
invariant(trade.trade)
|
||||
addTransaction({
|
||||
response: transaction,
|
||||
type: TransactionType.SWAP,
|
||||
tradeType: trade.trade.tradeType,
|
||||
inputCurrencyAmount: trade.trade.inputAmount,
|
||||
outputCurrencyAmount: trade.trade.outputAmount,
|
||||
})
|
||||
setDisplayTxHash(transaction.hash)
|
||||
|
||||
// Set the block containing the response to the oldest valid block to ensure that the
|
||||
// completed trade's impact is reflected in future fetched trades.
|
||||
transaction.wait(1).then((receipt) => {
|
||||
setOldestValidBlock(receipt.blockNumber)
|
||||
})
|
||||
|
||||
// Only reset open after any queued animations to avoid layout thrashing, because a
|
||||
// successful swap will open the status dialog and immediately cover the summary dialog.
|
||||
const postSwap = () => {
|
||||
setOpen(false)
|
||||
document.removeEventListener('animationend', postSwap)
|
||||
}
|
||||
if (isAnimating(document)) {
|
||||
document.addEventListener('animationend', postSwap)
|
||||
} else {
|
||||
postSwap()
|
||||
}
|
||||
} catch (e) {
|
||||
// TODO(zzmp): Surface errors from swap.
|
||||
console.log(e)
|
||||
}
|
||||
}, [addTransaction, setDisplayTxHash, setOldestValidBlock, swapCallback, trade.trade])
|
||||
|
||||
const disableSwap = useMemo(
|
||||
() =>
|
||||
disabled ||
|
||||
!chainId ||
|
||||
(wrapType === WrapType.NONE && !optimizedTrade) ||
|
||||
!(inputCurrencyAmount && inputCurrencyBalance) ||
|
||||
inputCurrencyBalance.lessThan(inputCurrencyAmount),
|
||||
[disabled, wrapType, optimizedTrade, chainId, inputCurrencyAmount, inputCurrencyBalance]
|
||||
)
|
||||
const actionProps = useMemo((): Partial<ActionButtonProps> | undefined => {
|
||||
if (disableSwap) {
|
||||
return { disabled: true }
|
||||
} else if (wrapType === WrapType.NONE) {
|
||||
return approvalAction ? { action: approvalAction } : { onClick: () => setOpen(true) }
|
||||
} else {
|
||||
return isPending
|
||||
? { action: { message: <Trans>Confirm in your wallet</Trans>, icon: Spinner } }
|
||||
: { onClick: onWrap }
|
||||
}
|
||||
}, [approvalAction, disableSwap, isPending, onWrap, wrapType])
|
||||
const Label = useCallback(() => {
|
||||
switch (wrapType) {
|
||||
case WrapType.UNWRAP:
|
||||
return <Trans>Unwrap {inputCurrency?.symbol}</Trans>
|
||||
case WrapType.WRAP:
|
||||
return <Trans>Wrap {inputCurrency?.symbol}</Trans>
|
||||
case WrapType.NONE:
|
||||
default:
|
||||
return <Trans>Review swap</Trans>
|
||||
}
|
||||
}, [inputCurrency?.symbol, wrapType])
|
||||
const onClose = useCallback(() => setOpen(false), [])
|
||||
|
||||
const { tokenColorExtraction } = useTheme()
|
||||
return (
|
||||
<>
|
||||
<ActionButton color={tokenColorExtraction ? 'interactive' : 'accent'} {...actionProps}>
|
||||
<Label />
|
||||
</ActionButton>
|
||||
{open && trade.trade && (
|
||||
<Dialog color="dialog" onClose={onClose}>
|
||||
<SummaryDialog
|
||||
trade={trade.trade}
|
||||
slippage={slippage}
|
||||
inputUSDC={inputUSDC}
|
||||
outputUSDC={outputUSDC}
|
||||
impact={impact}
|
||||
onConfirm={onSwap}
|
||||
/>
|
||||
</Dialog>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
})
|
||||
88
src/lib/components/Swap/SwapButton/useApprovalData.tsx
Normal file
88
src/lib/components/Swap/SwapButton/useApprovalData.tsx
Normal file
@@ -0,0 +1,88 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import { Currency, CurrencyAmount, Token } from '@uniswap/sdk-core'
|
||||
import { Action } from 'lib/components/ActionButton'
|
||||
import EtherscanLink from 'lib/components/EtherscanLink'
|
||||
import {
|
||||
ApproveOrPermitState,
|
||||
useApproveOrPermit,
|
||||
useSwapApprovalOptimizedTrade,
|
||||
useSwapRouterAddress,
|
||||
} from 'lib/hooks/swap/useSwapApproval'
|
||||
import { useAddTransaction, usePendingApproval } from 'lib/hooks/transactions'
|
||||
import { Slippage } from 'lib/hooks/useSlippage'
|
||||
import { Spinner } from 'lib/icons'
|
||||
import { TransactionType } from 'lib/state/transactions'
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import { ExplorerDataType } from 'utils/getExplorerLink'
|
||||
|
||||
export function useIsPendingApproval(token?: Token, spender?: string): boolean {
|
||||
return Boolean(usePendingApproval(token, spender))
|
||||
}
|
||||
|
||||
export default function useApprovalData(
|
||||
trade: ReturnType<typeof useSwapApprovalOptimizedTrade>,
|
||||
slippage: Slippage,
|
||||
currencyAmount?: CurrencyAmount<Currency>
|
||||
) {
|
||||
const currency = currencyAmount?.currency
|
||||
const { approvalState, signatureData, handleApproveOrPermit } = useApproveOrPermit(
|
||||
trade,
|
||||
slippage.allowed,
|
||||
useIsPendingApproval,
|
||||
currencyAmount
|
||||
)
|
||||
|
||||
const [isPending, setIsPending] = useState(false)
|
||||
const addTransaction = useAddTransaction()
|
||||
const onApprove = useCallback(async () => {
|
||||
setIsPending(true)
|
||||
const transaction = await handleApproveOrPermit()
|
||||
if (transaction) {
|
||||
addTransaction({ type: TransactionType.APPROVAL, ...transaction })
|
||||
}
|
||||
setIsPending(false)
|
||||
}, [addTransaction, handleApproveOrPermit])
|
||||
// Reset the pending state if currency changes.
|
||||
useEffect(() => setIsPending(false), [currency])
|
||||
|
||||
const approvalHash = usePendingApproval(currency?.isToken ? currency : undefined, useSwapRouterAddress(trade))
|
||||
const approvalAction = useMemo((): Action | undefined => {
|
||||
if (!trade || !currency) return
|
||||
|
||||
switch (approvalState) {
|
||||
case ApproveOrPermitState.REQUIRES_APPROVAL:
|
||||
if (isPending) {
|
||||
return { message: <Trans>Approve in your wallet</Trans>, icon: Spinner }
|
||||
}
|
||||
return {
|
||||
message: <Trans>Approve {currency.symbol} first</Trans>,
|
||||
onClick: onApprove,
|
||||
children: <Trans>Approve</Trans>,
|
||||
}
|
||||
case ApproveOrPermitState.REQUIRES_SIGNATURE:
|
||||
if (isPending) {
|
||||
return { message: <Trans>Allow in your wallet</Trans>, icon: Spinner }
|
||||
}
|
||||
return {
|
||||
message: <Trans>Allow {currency.symbol} first</Trans>,
|
||||
onClick: onApprove,
|
||||
children: <Trans>Allow</Trans>,
|
||||
}
|
||||
case ApproveOrPermitState.PENDING_APPROVAL:
|
||||
return {
|
||||
message: (
|
||||
<EtherscanLink type={ExplorerDataType.TRANSACTION} data={approvalHash}>
|
||||
<Trans>Approval pending</Trans>
|
||||
</EtherscanLink>
|
||||
),
|
||||
icon: Spinner,
|
||||
}
|
||||
case ApproveOrPermitState.PENDING_SIGNATURE:
|
||||
return { message: <Trans>Allowance pending</Trans>, icon: Spinner }
|
||||
case ApproveOrPermitState.APPROVED:
|
||||
return
|
||||
}
|
||||
}, [approvalHash, approvalState, currency, isPending, onApprove, trade])
|
||||
|
||||
return { approvalAction, signatureData: signatureData ?? undefined }
|
||||
}
|
||||
@@ -81,5 +81,5 @@ export default function SwapPropValidator(props: ValidatorProps) {
|
||||
}
|
||||
}, [defaultInputTokenAddress, defaultInputAmount, defaultOutputTokenAddress, defaultOutputAmount])
|
||||
|
||||
return <>{props.children}</>
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -1,19 +1,16 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import { Currency, TradeType } from '@uniswap/sdk-core'
|
||||
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 useUSDCPriceImpact, { toHumanReadablePriceImpact } from 'lib/hooks/useUSDCPriceImpact'
|
||||
import { PriceImpact } from 'lib/hooks/useUSDCPriceImpact'
|
||||
import { AlertTriangle, Icon, Info, InlineSpinner } from 'lib/icons'
|
||||
import styled, { ThemedText } from 'lib/theme'
|
||||
import { ReactNode, useCallback, useMemo, useState } from 'react'
|
||||
import { ReactNode, useCallback } from 'react'
|
||||
import { InterfaceTrade } from 'state/routing/types'
|
||||
import { getPriceImpactWarning } from 'utils/prices'
|
||||
|
||||
import { TextButton } from '../../Button'
|
||||
import Row from '../../Row'
|
||||
import Price from '../Price'
|
||||
import RoutingDiagram from '../RoutingDiagram'
|
||||
|
||||
const Loading = styled.span`
|
||||
@@ -40,7 +37,7 @@ export function ConnectWallet() {
|
||||
}
|
||||
|
||||
export function UnsupportedNetwork() {
|
||||
return <Caption caption={<Trans>Unsupported network - switch to another to trade.</Trans>} />
|
||||
return <Caption caption={<Trans>Unsupported network - switch to another to trade</Trans>} />
|
||||
}
|
||||
|
||||
export function InsufficientBalance({ currency }: { currency: Currency }) {
|
||||
@@ -51,6 +48,10 @@ export function InsufficientLiquidity() {
|
||||
return <Caption caption={<Trans>Insufficient liquidity in the pool for your trade</Trans>} />
|
||||
}
|
||||
|
||||
export function Error() {
|
||||
return <Caption caption={<Trans>Error fetching trade</Trans>} />
|
||||
}
|
||||
|
||||
export function Empty() {
|
||||
return <Caption icon={Info} caption={<Trans>Enter an amount</Trans>} />
|
||||
}
|
||||
@@ -68,55 +69,37 @@ export function LoadingTrade() {
|
||||
)
|
||||
}
|
||||
|
||||
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])
|
||||
export function WrapCurrency({ inputCurrency, outputCurrency }: { inputCurrency: Currency; outputCurrency: Currency }) {
|
||||
const Text = useCallback(
|
||||
() => (
|
||||
<Trans>
|
||||
Convert {inputCurrency.symbol} to {outputCurrency.symbol} with no slippage
|
||||
</Trans>
|
||||
),
|
||||
[inputCurrency.symbol, outputCurrency.symbol]
|
||||
)
|
||||
|
||||
return <Caption icon={Info} caption={<WrapText />} />
|
||||
return <Caption icon={Info} caption={<Text />} />
|
||||
}
|
||||
|
||||
export function Trade({ trade }: { trade: InterfaceTrade<Currency, Currency, TradeType> }) {
|
||||
const [flip, setFlip] = useState(true)
|
||||
const { inputAmount: input, outputAmount: output, executionPrice } = trade
|
||||
const { inputUSDC, outputUSDC, priceImpact } = useUSDCPriceImpact(input, output)
|
||||
const isPriceImpactHigh = priceImpact && getPriceImpactWarning(priceImpact)
|
||||
|
||||
const ratio = useMemo(() => {
|
||||
const [a, b] = flip ? [output, input] : [input, output]
|
||||
const priceString = (!flip ? executionPrice : executionPrice?.invert())?.toSignificant(6)
|
||||
|
||||
const ratio = `1 ${a.currency.symbol} = ${priceString} ${b.currency.symbol}`
|
||||
const usdc = !flip
|
||||
? inputUSDC
|
||||
? ` ($${inputUSDC.toSignificant(6)})`
|
||||
: null
|
||||
: outputUSDC
|
||||
? ` ($${outputUSDC.toSignificant(6)})`
|
||||
: null
|
||||
|
||||
return (
|
||||
<ThemedText.Caption userSelect>
|
||||
<Row gap={0.25}>
|
||||
{ratio}
|
||||
{usdc && <ThemedText.Caption color="secondary">{usdc}</ThemedText.Caption>}
|
||||
</Row>
|
||||
</ThemedText.Caption>
|
||||
)
|
||||
}, [executionPrice, inputUSDC, outputUSDC, flip, input, output])
|
||||
|
||||
export function Trade({
|
||||
trade,
|
||||
outputUSDC,
|
||||
impact,
|
||||
}: {
|
||||
trade: InterfaceTrade<Currency, Currency, TradeType>
|
||||
outputUSDC?: CurrencyAmount<Currency>
|
||||
impact?: PriceImpact
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
<Tooltip placement="bottom" icon={isPriceImpactHigh ? AlertTriangle : Info}>
|
||||
<Tooltip placement="bottom" icon={impact?.warning ? AlertTriangle : Info}>
|
||||
<Column gap={0.75}>
|
||||
{isPriceImpactHigh && (
|
||||
{impact?.warning && (
|
||||
<>
|
||||
<ThemedText.Caption>
|
||||
The output amount is estimated at {toHumanReadablePriceImpact(priceImpact)} less than the input amount
|
||||
due to high price impact
|
||||
The output amount is estimated at {impact.toString()} less than the input amount due to high price
|
||||
impact
|
||||
</ThemedText.Caption>
|
||||
<Rule />
|
||||
</>
|
||||
@@ -124,9 +107,7 @@ export function Trade({ trade }: { trade: InterfaceTrade<Currency, Currency, Tra
|
||||
<RoutingDiagram trade={trade} />
|
||||
</Column>
|
||||
</Tooltip>
|
||||
<TextButton color="primary" onClick={() => setFlip(!flip)}>
|
||||
{ratio}
|
||||
</TextButton>
|
||||
<Price trade={trade} outputUSDC={outputUSDC} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ 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'
|
||||
@@ -17,16 +17,16 @@ 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: inputBalance, amount: inputAmount },
|
||||
[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 { type: wrapType } = useWrapCallback()
|
||||
const caption = useMemo(() => {
|
||||
if (disabled) {
|
||||
return <Caption.ConnectWallet />
|
||||
@@ -37,34 +37,39 @@ 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) {
|
||||
if (state === TradeState.SYNCING || state === TradeState.LOADING) {
|
||||
return <Caption.LoadingTrade />
|
||||
}
|
||||
if (!trade?.swaps) {
|
||||
if (inputBalance && inputAmount?.greaterThan(inputBalance)) {
|
||||
return <Caption.InsufficientBalance currency={inputCurrency} />
|
||||
}
|
||||
if (wrapType !== WrapType.NONE) {
|
||||
return <Caption.WrapCurrency inputCurrency={inputCurrency} outputCurrency={outputCurrency} />
|
||||
}
|
||||
if (state === TradeState.NO_ROUTE_FOUND || (trade && !trade.swaps)) {
|
||||
return <Caption.InsufficientLiquidity />
|
||||
}
|
||||
if (balance && trade?.inputAmount.greaterThan(balance)) {
|
||||
return <Caption.InsufficientBalance currency={trade.inputAmount.currency} />
|
||||
if (trade?.inputAmount && trade.outputAmount) {
|
||||
return <Caption.Trade trade={trade} outputUSDC={outputUSDC} impact={impact} />
|
||||
}
|
||||
if (trade.inputAmount && trade.outputAmount) {
|
||||
return <Caption.Trade trade={trade} />
|
||||
if (state === TradeState.INVALID) {
|
||||
return <Caption.Error />
|
||||
}
|
||||
}
|
||||
|
||||
return <Caption.Empty />
|
||||
}, [
|
||||
balance,
|
||||
chainId,
|
||||
disabled,
|
||||
impact,
|
||||
inputAmount,
|
||||
inputBalance,
|
||||
inputCurrency,
|
||||
isAmountPopulated,
|
||||
isRouteLoading,
|
||||
outputCurrency,
|
||||
outputUSDC,
|
||||
state,
|
||||
trade,
|
||||
wrapLoading,
|
||||
wrapType,
|
||||
])
|
||||
|
||||
@@ -78,4 +83,4 @@ export default function Toolbar({ disabled }: { disabled?: boolean }) {
|
||||
</ThemedText.Caption>
|
||||
</>
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -47,11 +47,15 @@ export interface SwapProps extends TokenDefaults, FeeOptions {
|
||||
onConnectWallet?: () => void
|
||||
}
|
||||
|
||||
export default function Swap(props: SwapProps) {
|
||||
function Updaters(props: SwapProps & { disabled: boolean }) {
|
||||
useSyncTokenList(props.tokenList)
|
||||
useSyncTokenDefaults(props)
|
||||
useSyncConvenienceFee(props)
|
||||
|
||||
return props.disabled ? null : <SwapInfoUpdater />
|
||||
}
|
||||
|
||||
export default function Swap(props: SwapProps) {
|
||||
const { active, account } = useActiveWeb3React()
|
||||
const [wrapper, setWrapper] = useState<HTMLDivElement | null>(null)
|
||||
|
||||
@@ -71,8 +75,9 @@ export default function Swap(props: SwapProps) {
|
||||
const isInteractive = Boolean(active && onSupportedNetwork)
|
||||
|
||||
return (
|
||||
<SwapPropValidator {...props}>
|
||||
{isSwapSupported && <SwapInfoUpdater />}
|
||||
<>
|
||||
<SwapPropValidator {...props} />
|
||||
<Updaters {...props} disabled={!isSwapSupported} />
|
||||
<Header title={<Trans>Swap</Trans>}>
|
||||
{active && <Wallet disabled={!account} onClick={props.onConnectWallet} />}
|
||||
<Settings disabled={!isInteractive} />
|
||||
@@ -92,6 +97,6 @@ export default function Swap(props: SwapProps) {
|
||||
<StatusDialog tx={displayTx} onClose={() => setDisplayTxHash()} />
|
||||
</Dialog>
|
||||
)}
|
||||
</SwapPropValidator>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { Currency } from '@uniswap/sdk-core'
|
||||
import { useToken } from 'lib/hooks/useCurrency'
|
||||
import useCurrencyLogoURIs from 'lib/hooks/useCurrencyLogoURIs'
|
||||
import { MissingToken } from 'lib/icons'
|
||||
import styled from 'lib/theme'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { useCallback, useMemo, useState } from 'react'
|
||||
|
||||
const badSrcs = new Set<string>()
|
||||
|
||||
@@ -13,20 +14,28 @@ interface BaseProps {
|
||||
type TokenImgProps = BaseProps & Omit<React.ImgHTMLAttributes<HTMLImageElement>, keyof BaseProps>
|
||||
|
||||
function TokenImg({ token, ...rest }: TokenImgProps) {
|
||||
const srcs = useCurrencyLogoURIs(token)
|
||||
const [src, setSrc] = useState<string | undefined>()
|
||||
useEffect(() => {
|
||||
setSrc(srcs.find((src) => !badSrcs.has(src)))
|
||||
}, [srcs])
|
||||
const onError = useCallback(() => {
|
||||
if (src) badSrcs.add(src)
|
||||
setSrc(srcs.find((src) => !badSrcs.has(src)))
|
||||
}, [src, srcs])
|
||||
// Use the wrapped token info so that it includes the logoURI.
|
||||
const tokenInfo = useToken(token.isToken ? token.wrapped.address : undefined) ?? token
|
||||
|
||||
if (src) {
|
||||
return <img src={src} alt={token.name || token.symbol} onError={onError} {...rest} />
|
||||
}
|
||||
return <MissingToken color="secondary" {...rest} />
|
||||
// TODO(zzmp): TokenImg takes a frame to switch.
|
||||
const srcs = useCurrencyLogoURIs(tokenInfo)
|
||||
|
||||
const [attempt, setAttempt] = useState(0)
|
||||
const onError = useCallback((e) => {
|
||||
if (e.target.src) badSrcs.add(e.target.src)
|
||||
setAttempt((attempt) => ++attempt)
|
||||
}, [])
|
||||
|
||||
return useMemo(() => {
|
||||
// Trigger a re-render when an error occurs.
|
||||
void attempt
|
||||
|
||||
const src = srcs.find((src) => !badSrcs.has(src))
|
||||
if (!src) return <MissingToken color="secondary" {...rest} />
|
||||
|
||||
const alt = tokenInfo.name || tokenInfo.symbol
|
||||
return <img src={src} alt={alt} key={alt} onError={onError} {...rest} />
|
||||
}, [attempt, onError, rest, srcs, tokenInfo.name, tokenInfo.symbol])
|
||||
}
|
||||
|
||||
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 {
|
||||
|
||||
@@ -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'
|
||||
@@ -68,7 +68,7 @@ export function TokenSelectDialog({ value, onSelect }: TokenSelectDialogProps) {
|
||||
const baseTokens: Currency[] = [] // TODO(zzmp): Add base tokens to token list functionality
|
||||
|
||||
const input = useRef<HTMLInputElement>(null)
|
||||
useEffect(() => input.current?.focus(), [input])
|
||||
useEffect(() => input.current?.focus({ preventScroll: true }), [input])
|
||||
|
||||
const [options, setOptions] = useState<ElementRef<typeof TokenOptions> | null>(null)
|
||||
|
||||
@@ -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,40 +0,0 @@
|
||||
import { initializeConnector } 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, Web3ReactState } 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<Web3ReactState>) => void
|
||||
) {
|
||||
return useEffect(() => {
|
||||
if (initializer) {
|
||||
const state = initializeConnector((actions) => new Connector(actions, initializer))
|
||||
state[0].activate()
|
||||
setContext(state)
|
||||
} 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,21 +1,20 @@
|
||||
import { Provider as EthersProvider } from '@ethersproject/abstract-provider'
|
||||
import { JsonRpcProvider } from '@ethersproject/providers'
|
||||
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 { ActiveWeb3Provider } from 'lib/hooks/useActiveWeb3React'
|
||||
import { BlockUpdater } from 'lib/hooks/useBlockNumber'
|
||||
import useEip1193Provider from 'lib/hooks/useEip1193Provider'
|
||||
import { UNMOUNTING } from 'lib/hooks/useUnmount'
|
||||
import { Provider as I18nProvider } from 'lib/i18n'
|
||||
import { MulticallUpdater, store as multicallStore } from 'lib/state/multicall'
|
||||
import styled, { keyframes, Theme, ThemeProvider } from 'lib/theme'
|
||||
import { UNMOUNTING } from 'lib/utils/animations'
|
||||
import { PropsWithChildren, StrictMode, useState } from 'react'
|
||||
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;
|
||||
@@ -40,26 +39,27 @@ const WidgetWrapper = styled.div<{ width?: number | string }>`
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
font-family: ${({ theme }) => theme.fontFamily};
|
||||
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)};
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const slideDown = keyframes`
|
||||
to {
|
||||
const slideIn = keyframes`
|
||||
from {
|
||||
transform: translateY(calc(100% - 0.25em));
|
||||
}
|
||||
`
|
||||
const slideUp = keyframes`
|
||||
from {
|
||||
const slideOut = keyframes`
|
||||
to {
|
||||
transform: translateY(calc(100% - 0.25em));
|
||||
}
|
||||
`
|
||||
|
||||
const DialogWrapper = styled.div`
|
||||
border-radius: ${({ theme }) => theme.borderRadius * 0.75}em;
|
||||
height: calc(100% - 0.5em);
|
||||
left: 0;
|
||||
margin: 0.25em;
|
||||
@@ -73,11 +73,11 @@ const DialogWrapper = styled.div`
|
||||
}
|
||||
|
||||
${Modal} {
|
||||
animation: ${slideUp} 0.25s ease-in-out;
|
||||
}
|
||||
animation: ${slideIn} 0.25s ease-in;
|
||||
|
||||
${Modal}.${UNMOUNTING} {
|
||||
animation: ${slideDown} 0.25s ease-in-out;
|
||||
&.${UNMOUNTING} {
|
||||
animation: ${slideOut} 0.25s ease-out;
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
@@ -94,8 +94,8 @@ function Updaters() {
|
||||
export type WidgetProps = {
|
||||
theme?: Theme
|
||||
locale?: SupportedLocale
|
||||
provider?: Eip1193Provider | EthersProvider
|
||||
jsonRpcEndpoint?: string
|
||||
provider?: Eip1193Provider | JsonRpcProvider
|
||||
jsonRpcEndpoint?: string | JsonRpcProvider
|
||||
width?: string | number
|
||||
dialog?: HTMLElement | null
|
||||
className?: string
|
||||
@@ -114,31 +114,29 @@ export default function Widget(props: PropsWithChildren<WidgetProps>) {
|
||||
className,
|
||||
onError,
|
||||
} = props
|
||||
const eip1193 = useEip1193Provider(provider)
|
||||
const [dialog, setDialog] = useState<HTMLDivElement | null>(null)
|
||||
return (
|
||||
<StrictMode>
|
||||
<I18nProvider locale={locale}>
|
||||
<ThemeProvider theme={theme}>
|
||||
<WidgetWrapper width={width} className={className}>
|
||||
<ThemeProvider theme={theme}>
|
||||
<WidgetWrapper width={width} className={className}>
|
||||
<I18nProvider locale={locale}>
|
||||
<DialogWrapper ref={setDialog} />
|
||||
<DialogProvider value={userDialog || dialog}>
|
||||
<ErrorBoundary onError={onError}>
|
||||
<WidgetPropValidator {...props}>
|
||||
<ReduxProvider store={multicallStore}>
|
||||
<AtomProvider>
|
||||
<Web3Provider provider={eip1193} jsonRpcEndpoint={jsonRpcEndpoint}>
|
||||
<Updaters />
|
||||
{children}
|
||||
</Web3Provider>
|
||||
</AtomProvider>
|
||||
</ReduxProvider>
|
||||
</WidgetPropValidator>
|
||||
<WidgetPropValidator {...props} />
|
||||
<ReduxProvider store={multicallStore}>
|
||||
<AtomProvider>
|
||||
<ActiveWeb3Provider provider={provider} jsonRpcEndpoint={jsonRpcEndpoint}>
|
||||
<Updaters />
|
||||
{children}
|
||||
</ActiveWeb3Provider>
|
||||
</AtomProvider>
|
||||
</ReduxProvider>
|
||||
</ErrorBoundary>
|
||||
</DialogProvider>
|
||||
</WidgetWrapper>
|
||||
</ThemeProvider>
|
||||
</I18nProvider>
|
||||
</I18nProvider>
|
||||
</WidgetWrapper>
|
||||
</ThemeProvider>
|
||||
</StrictMode>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -10,5 +10,5 @@ export const loadingCss = css`
|
||||
// need to use isLoading as `loading` is a reserved prop
|
||||
export const loadingTransitionCss = css<{ isLoading: boolean }>`
|
||||
${({ isLoading }) => isLoading && loadingCss};
|
||||
transition: opacity 0.2s ease-in-out;
|
||||
transition: opacity ${({ isLoading }) => (isLoading ? 0 : 0.2)}s ease-in-out;
|
||||
`
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { BigintIsh, CurrencyAmount, Token, TradeType } from '@uniswap/sdk-core'
|
||||
// This file is lazy-loaded, so the import of smart-order-router is intentional.
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { AlphaRouter, AlphaRouterConfig, AlphaRouterParams, ChainId } from '@uniswap/smart-order-router'
|
||||
import JSBI from 'jsbi'
|
||||
import { GetQuoteResult } from 'state/routing/types'
|
||||
|
||||
@@ -1,15 +1,18 @@
|
||||
import 'setimmediate'
|
||||
|
||||
import { Protocol } from '@uniswap/router-sdk'
|
||||
import { Currency, CurrencyAmount, TradeType } from '@uniswap/sdk-core'
|
||||
import { ChainId } from '@uniswap/smart-order-router'
|
||||
import { SupportedChainId } from 'constants/chains'
|
||||
import useDebounce from 'hooks/useDebounce'
|
||||
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 { useGetIsValidBlock } from '../useIsValidBlock'
|
||||
import usePoll from '../usePoll'
|
||||
import { useRoutingAPIArguments } from './useRoutingAPIArguments'
|
||||
|
||||
/**
|
||||
@@ -17,20 +20,17 @@ import { useRoutingAPIArguments } from './useRoutingAPIArguments'
|
||||
* Defaults are defined in https://github.com/Uniswap/smart-order-router/blob/309e6f6603984d3b5aef0733b0cfaf129c29f602/src/routers/alpha-router/config.ts#L83.
|
||||
*/
|
||||
const DistributionPercents: { [key: number]: number } = {
|
||||
[ChainId.MAINNET]: 10,
|
||||
[ChainId.OPTIMISM]: 10,
|
||||
[ChainId.OPTIMISTIC_KOVAN]: 10,
|
||||
[ChainId.ARBITRUM_ONE]: 25,
|
||||
[ChainId.ARBITRUM_RINKEBY]: 25,
|
||||
[SupportedChainId.MAINNET]: 10,
|
||||
[SupportedChainId.OPTIMISM]: 10,
|
||||
[SupportedChainId.OPTIMISTIC_KOVAN]: 10,
|
||||
[SupportedChainId.ARBITRUM_ONE]: 25,
|
||||
[SupportedChainId.ARBITRUM_RINKEBY]: 25,
|
||||
}
|
||||
|
||||
const DEFAULT_DISTRIBUTION_PERCENT = 10
|
||||
|
||||
function getConfig(chainId: ChainId | undefined) {
|
||||
function getConfig(chainId: SupportedChainId | undefined) {
|
||||
return {
|
||||
// Limit to only V2 and V3.
|
||||
protocols: [Protocol.V2, Protocol.V3],
|
||||
|
||||
distributionPercent: (chainId && DistributionPercents[chainId]) ?? DEFAULT_DISTRIBUTION_PERCENT,
|
||||
}
|
||||
}
|
||||
@@ -43,22 +43,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,
|
||||
@@ -67,46 +65,38 @@ 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(() => {
|
||||
if (wrapType !== WrapType.NOT_APPLICABLE) {
|
||||
return
|
||||
}
|
||||
setLoading(true)
|
||||
if (isDebouncing) return
|
||||
const getQuoteResult = useCallback(async (): Promise<{ data?: GetQuoteResult; error?: unknown }> => {
|
||||
if (wrapType !== WrapType.NONE) return { error: undefined }
|
||||
if (!queryArgs || !params) return { error: undefined }
|
||||
try {
|
||||
// Lazy-load the smart order router to improve initial pageload times.
|
||||
const quoteResult = await (
|
||||
await import('./clientSideSmartOrderRouter')
|
||||
).getClientSideQuote(queryArgs, params, config)
|
||||
|
||||
let stale = false
|
||||
fetchQuote()
|
||||
return () => {
|
||||
stale = true
|
||||
setLoading(false)
|
||||
// There is significant post-fetch processing, so delay a tick to prevent dropped frames.
|
||||
// This is only important in the context of integrations - if we control the whole site,
|
||||
// then we can afford to drop a few frames.
|
||||
return new Promise((resolve) => setImmediate(() => resolve(quoteResult)))
|
||||
} 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, wrapType])
|
||||
const getIsValidBlock = useGetIsValidBlock()
|
||||
const { data: quoteResult, error } = usePoll(getQuoteResult, JSON.stringify(queryArgs), {
|
||||
debounce: isDebouncing,
|
||||
staleCallback: useCallback(({ data }) => !getIsValidBlock(Number(data?.blockNumber) || 0), [getIsValidBlock]),
|
||||
}) ?? {
|
||||
error: undefined,
|
||||
}
|
||||
const isValid = getIsValidBlock(Number(quoteResult?.blockNumber) || 0)
|
||||
|
||||
const route = useMemo(
|
||||
() => computeRoutes(currencyIn, currencyOut, tradeType, quoteResult),
|
||||
@@ -129,11 +119,12 @@ export default function useClientSideSmartOrderRouterTrade<TTradeType extends Tr
|
||||
return { state: TradeState.INVALID, trade: undefined }
|
||||
}
|
||||
|
||||
// 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 (!trade && !error) {
|
||||
if (isDebouncing) {
|
||||
return { state: TradeState.SYNCING, trade: undefined }
|
||||
} else if (!isValid) {
|
||||
return { state: TradeState.LOADING, trade: undefined }
|
||||
}
|
||||
}
|
||||
|
||||
let otherAmount = undefined
|
||||
@@ -148,7 +139,7 @@ export default function useClientSideSmartOrderRouterTrade<TTradeType extends Tr
|
||||
}
|
||||
}
|
||||
|
||||
if (error || !otherAmount || !route || route.length === 0 || !queryArgs) {
|
||||
if (error || !otherAmount || !route || route.length === 0) {
|
||||
return { state: TradeState.NO_ROUTE_FOUND, trade: undefined }
|
||||
}
|
||||
|
||||
@@ -156,5 +147,5 @@ 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, trade, error, isValid, quoteResult, route, isDebouncing, tradeType])
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Currency, CurrencyAmount, TradeType } from '@uniswap/sdk-core'
|
||||
import { Currency, CurrencyAmount } from '@uniswap/sdk-core'
|
||||
import { useAtom } from 'jotai'
|
||||
import { useAtomValue, useUpdateAtom } from 'jotai/utils'
|
||||
import { pickAtom } from 'lib/state/atoms'
|
||||
@@ -63,16 +63,6 @@ export function useIsSwapFieldIndependent(field: Field): boolean {
|
||||
return independentField === field
|
||||
}
|
||||
|
||||
export function useSwapTradeType(): TradeType {
|
||||
const independentField = useAtomValue(independentFieldAtom)
|
||||
switch (independentField) {
|
||||
case Field.INPUT:
|
||||
return TradeType.EXACT_INPUT
|
||||
case Field.OUTPUT:
|
||||
return TradeType.EXACT_OUTPUT
|
||||
}
|
||||
}
|
||||
|
||||
const amountAtom = pickAtom(swapAtom, 'amount')
|
||||
|
||||
// check if any amount has been entered by user
|
||||
@@ -83,7 +73,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 +91,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
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { Currency, CurrencyAmount, TradeType } from '@uniswap/sdk-core'
|
||||
import { useClientSideV3Trade } from 'hooks/useClientSideV3Trade'
|
||||
import useLast from 'hooks/useLast'
|
||||
import { useMemo } from 'react'
|
||||
import { InterfaceTrade, TradeState } from 'state/routing/types'
|
||||
|
||||
import useClientSideSmartOrderRouterTrade from '../routing/useClientSideSmartOrderRouterTrade'
|
||||
@@ -18,15 +20,38 @@ export function useBestTrade(
|
||||
state: TradeState
|
||||
trade: InterfaceTrade<Currency, Currency, TradeType> | undefined
|
||||
} {
|
||||
const clientSORTrade = useClientSideSmartOrderRouterTrade(tradeType, amountSpecified, otherCurrency)
|
||||
const clientSORTradeObject = useClientSideSmartOrderRouterTrade(tradeType, amountSpecified, otherCurrency)
|
||||
|
||||
// Use a simple client side logic as backup if SOR is not available.
|
||||
const useFallback = clientSORTrade.state === TradeState.NO_ROUTE_FOUND || clientSORTrade.state === TradeState.INVALID
|
||||
const fallbackTrade = useClientSideV3Trade(
|
||||
const useFallback =
|
||||
clientSORTradeObject.state === TradeState.NO_ROUTE_FOUND || clientSORTradeObject.state === TradeState.INVALID
|
||||
const fallbackTradeObject = useClientSideV3Trade(
|
||||
tradeType,
|
||||
useFallback ? amountSpecified : undefined,
|
||||
useFallback ? otherCurrency : undefined
|
||||
)
|
||||
|
||||
return useFallback ? fallbackTrade : clientSORTrade
|
||||
const tradeObject = useFallback ? fallbackTradeObject : clientSORTradeObject
|
||||
const lastTrade = useLast(tradeObject.trade, Boolean) ?? undefined
|
||||
|
||||
// Return the last trade while syncing/loading to avoid jank from clearing the last trade while loading.
|
||||
// If the trade is unsettled and not stale, return the last trade as a placeholder during settling.
|
||||
return useMemo(() => {
|
||||
const { state, trade } = tradeObject
|
||||
// If the trade is in a settled state, return it.
|
||||
if ((state !== TradeState.LOADING && state !== TradeState.SYNCING) || trade) return tradeObject
|
||||
|
||||
const [currencyIn, currencyOut] =
|
||||
tradeType === TradeType.EXACT_INPUT
|
||||
? [amountSpecified?.currency, otherCurrency]
|
||||
: [otherCurrency, amountSpecified?.currency]
|
||||
|
||||
// If the trade currencies have switched, consider it stale - do not return the last trade.
|
||||
const isStale =
|
||||
(currencyIn && !lastTrade?.inputAmount?.currency.equals(currencyIn)) ||
|
||||
(currencyOut && !lastTrade?.outputAmount?.currency.equals(currencyOut))
|
||||
if (isStale) return tradeObject
|
||||
|
||||
return { state, trade: lastTrade }
|
||||
}, [amountSpecified?.currency, lastTrade, otherCurrency, tradeObject, tradeType])
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { BigNumber } from '@ethersproject/bignumber'
|
||||
import { TransactionResponse, Web3Provider } from '@ethersproject/providers'
|
||||
import { JsonRpcProvider, TransactionResponse } from '@ethersproject/providers'
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { t, Trans } from '@lingui/macro'
|
||||
import { Trade } from '@uniswap/router-sdk'
|
||||
@@ -40,7 +40,7 @@ interface FailedCall extends SwapCallEstimate {
|
||||
export default function useSendSwapTransaction(
|
||||
account: string | null | undefined,
|
||||
chainId: number | undefined,
|
||||
library: Web3Provider | undefined,
|
||||
library: JsonRpcProvider | undefined,
|
||||
trade: AnyTrade | undefined, // trade to execute, required
|
||||
swapCalls: SwapCall[]
|
||||
): { callback: null | (() => Promise<TransactionResponse>) } {
|
||||
|
||||
@@ -7,7 +7,6 @@ import useActiveWeb3React from 'hooks/useActiveWeb3React'
|
||||
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'
|
||||
|
||||
import { ApprovalState, useApproval, useApprovalStateForSpender } from '../useApproval'
|
||||
@@ -75,10 +74,6 @@ export default function useSwapApproval(
|
||||
const spender = useSwapRouterAddress(trade)
|
||||
|
||||
const approval = useApproval(amountToApprove, spender, useIsPendingApproval)
|
||||
if (trade instanceof V2Trade || trade instanceof V3Trade) {
|
||||
const approvalState = approval[0]
|
||||
invariant(approvalState === ApprovalState.APPROVED, 'Trying to approve legacy router')
|
||||
}
|
||||
return approval
|
||||
}
|
||||
|
||||
@@ -171,39 +166,39 @@ export const useApproveOrPermit = (
|
||||
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()
|
||||
try {
|
||||
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 await getApproval()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return await getApproval()
|
||||
}
|
||||
} else {
|
||||
return getApproval()
|
||||
} catch (e) {
|
||||
// Swallow approval errors - user rejections do not need to be displayed.
|
||||
}
|
||||
}, [signatureState, gatherPermitSignature, getApproval])
|
||||
|
||||
const approvalState = useMemo(() => {
|
||||
if (approval === ApprovalState.PENDING) {
|
||||
return ApproveOrPermitState.PENDING_APPROVAL
|
||||
}
|
||||
if (signatureState === UseERC20PermitState.LOADING) {
|
||||
} else if (signatureState === UseERC20PermitState.LOADING) {
|
||||
return ApproveOrPermitState.PENDING_SIGNATURE
|
||||
}
|
||||
if (notApproved && Boolean(gatherPermitSignature)) {
|
||||
} else if (approval !== ApprovalState.NOT_APPROVED || signatureState === UseERC20PermitState.SIGNED) {
|
||||
return ApproveOrPermitState.APPROVED
|
||||
} else if (gatherPermitSignature) {
|
||||
return ApproveOrPermitState.REQUIRES_SIGNATURE
|
||||
}
|
||||
if (notApproved) {
|
||||
} else {
|
||||
return ApproveOrPermitState.REQUIRES_APPROVAL
|
||||
}
|
||||
return ApproveOrPermitState.APPROVED
|
||||
}, [approval, gatherPermitSignature, notApproved, signatureState])
|
||||
}, [approval, gatherPermitSignature, signatureState])
|
||||
|
||||
return {
|
||||
approvalState,
|
||||
|
||||
@@ -20,8 +20,8 @@ export enum SwapCallbackState {
|
||||
|
||||
interface UseSwapCallbackReturns {
|
||||
state: SwapCallbackState
|
||||
callback: null | (() => Promise<TransactionResponse>)
|
||||
error: ReactNode | null
|
||||
callback?: () => Promise<TransactionResponse>
|
||||
error?: ReactNode
|
||||
}
|
||||
interface UseSwapCallbackArgs {
|
||||
trade: AnyTrade | undefined // trade to execute, required
|
||||
@@ -59,24 +59,19 @@ export function useSwapCallback({
|
||||
|
||||
return useMemo(() => {
|
||||
if (!trade || !library || !account || !chainId || !callback) {
|
||||
return { state: SwapCallbackState.INVALID, callback: null, error: <Trans>Missing dependencies</Trans> }
|
||||
return { state: SwapCallbackState.INVALID, error: <Trans>Missing dependencies</Trans> }
|
||||
}
|
||||
if (!recipient) {
|
||||
if (recipientAddressOrName !== null) {
|
||||
return { state: SwapCallbackState.INVALID, callback: null, error: <Trans>Invalid recipient</Trans> }
|
||||
return { state: SwapCallbackState.INVALID, error: <Trans>Invalid recipient</Trans> }
|
||||
} else {
|
||||
return { state: SwapCallbackState.LOADING, callback: null, error: null }
|
||||
return { state: SwapCallbackState.LOADING }
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
state: SwapCallbackState.VALID,
|
||||
callback: async function onSwap(): Promise<TransactionResponse> {
|
||||
return callback().then((response) => {
|
||||
return response
|
||||
})
|
||||
},
|
||||
error: null,
|
||||
callback: async () => callback(),
|
||||
}
|
||||
}, [trade, library, account, chainId, callback, recipient, recipientAddressOrName])
|
||||
}
|
||||
|
||||
@@ -1,151 +1,111 @@
|
||||
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> }
|
||||
tradeCurrencyAmounts: { [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 {
|
||||
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 { 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 parsedAmount = useMemo(
|
||||
() => tryParseCurrencyAmount(amount, (isExactIn ? inputCurrency : outputCurrency) ?? undefined),
|
||||
[inputCurrency, isExactIn, outputCurrency, amount]
|
||||
)
|
||||
|
||||
//@TODO(ianlapham): this would eventually be replaced with routing api logic.
|
||||
const parsedAmount = useMemo(
|
||||
() => tryParseCurrencyAmount(amount, (isExactIn ? currencyIn : currencyOut) ?? undefined),
|
||||
[amount, isExactIn, currencyIn, currencyOut]
|
||||
)
|
||||
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 tradeCurrencyAmounts = 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, outputUSDC, impact } = useUSDCPriceImpact(trade.trade?.inputAmount, trade.trade?.outputAmount)
|
||||
|
||||
return useMemo(
|
||||
() => ({
|
||||
currencies,
|
||||
currencyBalances,
|
||||
inputError,
|
||||
[Field.INPUT]: {
|
||||
currency: currencyIn,
|
||||
amount: amountIn,
|
||||
balance: balanceIn,
|
||||
usdc: inputUSDC,
|
||||
},
|
||||
[Field.OUTPUT]: {
|
||||
currency: currencyOut,
|
||||
amount: amountOut,
|
||||
balance: balanceOut,
|
||||
usdc: outputUSDC,
|
||||
},
|
||||
trade,
|
||||
tradeCurrencyAmounts,
|
||||
allowedSlippage,
|
||||
feeOptions,
|
||||
slippage,
|
||||
impact,
|
||||
}),
|
||||
[currencies, currencyBalances, inputError, trade, tradeCurrencyAmounts, allowedSlippage, feeOptions]
|
||||
[
|
||||
amountIn,
|
||||
amountOut,
|
||||
balanceIn,
|
||||
balanceOut,
|
||||
currencyIn,
|
||||
currencyOut,
|
||||
impact,
|
||||
inputUSDC,
|
||||
outputUSDC,
|
||||
slippage,
|
||||
trade,
|
||||
]
|
||||
)
|
||||
}
|
||||
|
||||
const swapInfoAtom = atom<SwapInfo>({
|
||||
currencies: {},
|
||||
currencyBalances: {},
|
||||
[Field.INPUT]: {},
|
||||
[Field.OUTPUT]: {},
|
||||
trade: { state: TradeState.INVALID },
|
||||
tradeCurrencyAmounts: {},
|
||||
allowedSlippage: new Percent(0),
|
||||
feeOptions: undefined,
|
||||
slippage: { auto: true, allowed: new Percent(0) },
|
||||
})
|
||||
|
||||
export function SwapInfoUpdater() {
|
||||
|
||||
@@ -1,136 +1,63 @@
|
||||
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 { useMemo } from 'react'
|
||||
|
||||
import { WRAPPED_NATIVE_CURRENCY } from '../../../constants/tokens'
|
||||
import useActiveWeb3React from '../useActiveWeb3React'
|
||||
import { useCurrencyBalances } from '../useCurrencyBalance'
|
||||
import useCurrencyBalance from '../useCurrencyBalance'
|
||||
|
||||
export enum WrapType {
|
||||
NOT_APPLICABLE,
|
||||
NONE,
|
||||
WRAP,
|
||||
UNWRAP,
|
||||
}
|
||||
interface UseWrapCallbackReturns {
|
||||
callback: () => Promise<ContractTransaction>
|
||||
error: WrapError
|
||||
loading: boolean
|
||||
callback?: () => Promise<ContractTransaction>
|
||||
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,
|
||||
independentField,
|
||||
[Field.INPUT]: inputCurrency,
|
||||
[Field.OUTPUT]: outputCurrency,
|
||||
} = useAtomValue(swapAtom)
|
||||
const { amount, [Field.INPUT]: inputCurrency, [Field.OUTPUT]: outputCurrency } = useAtomValue(swapAtom)
|
||||
|
||||
const wrapType = useMemo(() => {
|
||||
if (!inputCurrency || !outputCurrency || !chainId) {
|
||||
return WrapType.NOT_APPLICABLE
|
||||
if (chainId && inputCurrency && outputCurrency) {
|
||||
if (inputCurrency.isNative && WRAPPED_NATIVE_CURRENCY[chainId]?.equals(outputCurrency)) {
|
||||
return WrapType.WRAP
|
||||
}
|
||||
if (outputCurrency.isNative && WRAPPED_NATIVE_CURRENCY[chainId]?.equals(inputCurrency)) {
|
||||
return WrapType.UNWRAP
|
||||
}
|
||||
}
|
||||
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
|
||||
return WrapType.NONE
|
||||
}, [chainId, inputCurrency, outputCurrency])
|
||||
|
||||
const isExactIn = independentField === Field.INPUT
|
||||
const parsedAmount = useMemo(
|
||||
() => tryParseCurrencyAmount(amount, (isExactIn ? inputCurrency : outputCurrency) ?? undefined),
|
||||
[inputCurrency, isExactIn, outputCurrency, amount]
|
||||
const parsedAmountIn = useMemo(
|
||||
() => tryParseCurrencyAmount(amount, inputCurrency ?? undefined),
|
||||
[inputCurrency, amount]
|
||||
)
|
||||
const parsedAmountIn = isExactIn ? parsedAmount : undefined
|
||||
const balanceIn = useCurrencyBalance(account, inputCurrency)
|
||||
|
||||
const relevantTokenBalances = useCurrencyBalances(
|
||||
account,
|
||||
useMemo(() => [inputCurrency ?? undefined, outputCurrency ?? undefined], [inputCurrency, outputCurrency])
|
||||
)
|
||||
const currencyBalances = useMemo(
|
||||
() => ({
|
||||
[Field.INPUT]: relevantTokenBalances[0],
|
||||
[Field.OUTPUT]: relevantTokenBalances[1],
|
||||
}),
|
||||
[relevantTokenBalances]
|
||||
)
|
||||
const callback = useMemo(() => {
|
||||
if (
|
||||
wrapType === WrapType.NONE ||
|
||||
!parsedAmountIn ||
|
||||
!balanceIn ||
|
||||
balanceIn.lessThan(parsedAmountIn) ||
|
||||
!wrappedNativeCurrencyContract
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
const hasInputAmount = Boolean(parsedAmount?.greaterThan('0'))
|
||||
const sufficientBalance = parsedAmountIn && !currencyBalances[Field.INPUT]?.lessThan(parsedAmountIn)
|
||||
return async () =>
|
||||
wrapType === WrapType.WRAP
|
||||
? wrappedNativeCurrencyContract.deposit({ value: `0x${parsedAmountIn.quotient.toString(16)}` })
|
||||
: wrappedNativeCurrencyContract.withdraw(`0x${parsedAmountIn.quotient.toString(16)}`)
|
||||
}, [wrapType, parsedAmountIn, balanceIn, wrappedNativeCurrencyContract])
|
||||
|
||||
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)}`))
|
||||
// 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]
|
||||
)
|
||||
return useMemo(() => ({ callback, type: wrapType }), [callback, 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())
|
||||
}
|
||||
98
src/lib/hooks/useActiveWeb3React.tsx
Normal file
98
src/lib/hooks/useActiveWeb3React.tsx
Normal file
@@ -0,0 +1,98 @@
|
||||
import { ExternalProvider, JsonRpcProvider, Web3Provider } from '@ethersproject/providers'
|
||||
import { initializeConnector, Web3ReactHooks } from '@web3-react/core'
|
||||
import { EIP1193 } from '@web3-react/eip1193'
|
||||
import { EMPTY } from '@web3-react/empty'
|
||||
import { Actions, Connector, Provider as Eip1193Provider, Web3ReactStore } from '@web3-react/types'
|
||||
import { Url } from '@web3-react/url'
|
||||
import { useAtom, WritableAtom } from 'jotai'
|
||||
import { atom } from 'jotai'
|
||||
import JsonRpcConnector from 'lib/utils/JsonRpcConnector'
|
||||
import { createContext, PropsWithChildren, useContext, useEffect, useMemo } from 'react'
|
||||
|
||||
type Web3ContextType = {
|
||||
connector: Connector
|
||||
library?: (JsonRpcProvider & { provider?: ExternalProvider }) | Web3Provider
|
||||
chainId?: ReturnType<Web3ReactHooks['useChainId']>
|
||||
accounts?: ReturnType<Web3ReactHooks['useAccounts']>
|
||||
account?: ReturnType<Web3ReactHooks['useAccount']>
|
||||
active?: ReturnType<Web3ReactHooks['useIsActive']>
|
||||
error?: ReturnType<Web3ReactHooks['useError']>
|
||||
ensNames?: ReturnType<Web3ReactHooks['useENSNames']>
|
||||
ensName?: ReturnType<Web3ReactHooks['useENSName']>
|
||||
}
|
||||
|
||||
const EMPTY_CONNECTOR = initializeConnector(() => EMPTY)
|
||||
const EMPTY_CONTEXT: Web3ContextType = { connector: EMPTY }
|
||||
const urlConnectorAtom = atom<[Connector, Web3ReactHooks, Web3ReactStore]>(EMPTY_CONNECTOR)
|
||||
const injectedConnectorAtom = atom<[Connector, Web3ReactHooks, Web3ReactStore]>(EMPTY_CONNECTOR)
|
||||
const Web3Context = createContext(EMPTY_CONTEXT)
|
||||
|
||||
export default function useActiveWeb3React() {
|
||||
return useContext(Web3Context)
|
||||
}
|
||||
|
||||
function useConnector<T extends { new (actions: Actions, initializer: I): Connector }, I>(
|
||||
connectorAtom: WritableAtom<[Connector, Web3ReactHooks, Web3ReactStore], [Connector, Web3ReactHooks, Web3ReactStore]>,
|
||||
Connector: T,
|
||||
initializer: I | undefined
|
||||
) {
|
||||
const [connector, setConnector] = useAtom(connectorAtom)
|
||||
useEffect(() => {
|
||||
if (initializer) {
|
||||
const [connector, hooks, store] = initializeConnector((actions) => new Connector(actions, initializer))
|
||||
connector.activate()
|
||||
setConnector([connector, hooks, store])
|
||||
} else {
|
||||
setConnector(EMPTY_CONNECTOR)
|
||||
}
|
||||
}, [Connector, initializer, setConnector])
|
||||
return connector
|
||||
}
|
||||
|
||||
interface ActiveWeb3ProviderProps {
|
||||
provider?: Eip1193Provider | JsonRpcProvider
|
||||
jsonRpcEndpoint?: string | JsonRpcProvider
|
||||
}
|
||||
|
||||
export function ActiveWeb3Provider({
|
||||
provider,
|
||||
jsonRpcEndpoint,
|
||||
children,
|
||||
}: PropsWithChildren<ActiveWeb3ProviderProps>) {
|
||||
const Injected = useMemo(() => {
|
||||
if (provider) {
|
||||
if (JsonRpcProvider.isProvider(provider)) return JsonRpcConnector
|
||||
if (JsonRpcProvider.isProvider((provider as any).provider)) {
|
||||
throw new Error('Eip1193Bridge is experimental: pass your ethers Provider directly')
|
||||
}
|
||||
}
|
||||
return EIP1193
|
||||
}, [provider]) as { new (actions: Actions, initializer: typeof provider): Connector }
|
||||
const injectedConnector = useConnector(injectedConnectorAtom, Injected, provider)
|
||||
const urlConnector = useConnector(urlConnectorAtom, Url, jsonRpcEndpoint)
|
||||
const [connector, hooks] = injectedConnector[1].useIsActive() ? injectedConnector : urlConnector ?? EMPTY_CONNECTOR
|
||||
|
||||
const library = hooks.useProvider()
|
||||
const chainId = hooks.useChainId()
|
||||
const accounts = hooks.useAccounts()
|
||||
const account = hooks.useAccount()
|
||||
const active = hooks.useIsActive()
|
||||
const error = hooks.useError()
|
||||
const ensNames = hooks.useENSNames()
|
||||
const ensName = hooks.useENSName()
|
||||
const web3 = useMemo(() => {
|
||||
if (connector === EMPTY || !active) {
|
||||
return EMPTY_CONTEXT
|
||||
}
|
||||
return { connector, library, chainId, accounts, account, active, error, ensNames, ensName }
|
||||
}, [account, accounts, active, chainId, connector, ensName, ensNames, error, library])
|
||||
|
||||
// Log web3 errors to facilitate debugging.
|
||||
useEffect(() => {
|
||||
if (error) {
|
||||
console.error('web3 error:', error)
|
||||
}
|
||||
}, [error])
|
||||
|
||||
return <Web3Context.Provider value={web3}>{children}</Web3Context.Provider>
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
import { Provider as EthersProvider } from '@ethersproject/abstract-provider'
|
||||
import { VoidSigner } from '@ethersproject/abstract-signer'
|
||||
import { Eip1193Bridge as ExperimentalEip1193Bridge } from '@ethersproject/experimental'
|
||||
import { JsonRpcProvider, JsonRpcSigner } from '@ethersproject/providers'
|
||||
import { Provider as Eip1193Provider } from '@web3-react/types'
|
||||
import { ZERO_ADDRESS } from 'constants/misc'
|
||||
import { useMemo } from 'react'
|
||||
|
||||
const voidSigner = new VoidSigner(ZERO_ADDRESS)
|
||||
|
||||
class Eip1193Bridge extends ExperimentalEip1193Bridge {
|
||||
async send(method: string, params?: Array<any>): Promise<any> {
|
||||
switch (method) {
|
||||
case 'eth_chainId': {
|
||||
// TODO(https://github.com/ethers-io/ethers.js/pull/2711): Returns eth_chainId as a hexadecimal.
|
||||
const result = await this.provider.getNetwork()
|
||||
return '0x' + result.chainId.toString(16)
|
||||
}
|
||||
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 | EthersSigningProvider | EthersProvider
|
||||
): Eip1193Provider | undefined {
|
||||
return useMemo(() => {
|
||||
if (provider) {
|
||||
if (EthersProvider.isProvider(provider)) {
|
||||
const signer = 'getSigner' in provider ? provider.getSigner() : null ?? voidSigner
|
||||
return new Eip1193Bridge(signer, 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 provider
|
||||
}, [provider])
|
||||
}
|
||||
45
src/lib/hooks/useIsValidBlock.ts
Normal file
45
src/lib/hooks/useIsValidBlock.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import { atomWithImmer } from 'jotai/immer'
|
||||
import { useAtomValue, useUpdateAtom } from 'jotai/utils'
|
||||
import { useCallback } from 'react'
|
||||
|
||||
import useActiveWeb3React from './useActiveWeb3React'
|
||||
import useBlockNumber from './useBlockNumber'
|
||||
|
||||
// The oldest block (per chain) to be considered valid.
|
||||
const oldestBlockMapAtom = atomWithImmer<{ [chainId: number]: number }>({})
|
||||
|
||||
const DEFAULT_MAX_BLOCK_AGE = 10
|
||||
|
||||
export function useSetOldestValidBlock(): (block: number) => void {
|
||||
const { chainId } = useActiveWeb3React()
|
||||
const updateValidBlock = useUpdateAtom(oldestBlockMapAtom)
|
||||
return useCallback(
|
||||
(block: number) => {
|
||||
if (!chainId) return
|
||||
updateValidBlock((oldestBlockMap) => {
|
||||
oldestBlockMap[chainId] = Math.max(block, oldestBlockMap[chainId] || 0)
|
||||
})
|
||||
},
|
||||
[chainId, updateValidBlock]
|
||||
)
|
||||
}
|
||||
|
||||
export function useGetIsValidBlock(maxBlockAge = DEFAULT_MAX_BLOCK_AGE): (block: number) => boolean {
|
||||
const { chainId } = useActiveWeb3React()
|
||||
const currentBlock = useBlockNumber()
|
||||
const oldestBlockMap = useAtomValue(oldestBlockMapAtom)
|
||||
const oldestBlock = chainId ? oldestBlockMap[chainId] : 0
|
||||
return useCallback(
|
||||
(block: number) => {
|
||||
if (!currentBlock) return false
|
||||
if (currentBlock - block > maxBlockAge) return false
|
||||
if (currentBlock < oldestBlock) return false
|
||||
return true
|
||||
},
|
||||
[currentBlock, maxBlockAge, oldestBlock]
|
||||
)
|
||||
}
|
||||
|
||||
export default function useIsValidBlock(block: number): boolean {
|
||||
return useGetIsValidBlock()(block)
|
||||
}
|
||||
88
src/lib/hooks/usePoll.ts
Normal file
88
src/lib/hooks/usePoll.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
import ms from 'ms.macro'
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
|
||||
const DEFAULT_POLLING_INTERVAL = ms`15s`
|
||||
const DEFAULT_KEEP_UNUSED_DATA_FOR = ms`10s`
|
||||
|
||||
interface PollingOptions<T> {
|
||||
// If true, any cached result will be returned, but no new fetch will be initiated.
|
||||
debounce?: boolean
|
||||
|
||||
// If stale, any cached result will be returned, and a new fetch will be initiated.
|
||||
staleCallback?: (value: T) => boolean
|
||||
|
||||
pollingInterval?: number
|
||||
keepUnusedDataFor?: number
|
||||
}
|
||||
|
||||
interface CacheEntry<T> {
|
||||
ttl: number | null // null denotes a pending fetch
|
||||
result?: T
|
||||
}
|
||||
|
||||
export default function usePoll<T>(
|
||||
fetch: () => Promise<T>,
|
||||
key = '',
|
||||
{
|
||||
debounce = false,
|
||||
staleCallback,
|
||||
pollingInterval = DEFAULT_POLLING_INTERVAL,
|
||||
keepUnusedDataFor = DEFAULT_KEEP_UNUSED_DATA_FOR,
|
||||
}: PollingOptions<T>
|
||||
): T | undefined {
|
||||
const cache = useMemo(() => new Map<string, CacheEntry<T>>(), [])
|
||||
const [, setData] = useState<{ key: string; result?: T }>({ key })
|
||||
|
||||
useEffect(() => {
|
||||
if (debounce) return
|
||||
|
||||
let timeout: number
|
||||
|
||||
const entry = cache.get(key)
|
||||
const isStale = staleCallback && entry?.result !== undefined ? staleCallback(entry.result) : false
|
||||
if (entry) {
|
||||
// If there is not a pending fetch (and there should be), queue one.
|
||||
if (entry.ttl) {
|
||||
if (isStale) {
|
||||
poll() // stale results should be refetched immediately
|
||||
} else if (entry.ttl && entry.ttl + keepUnusedDataFor > Date.now()) {
|
||||
timeout = setTimeout(poll, Math.max(0, entry.ttl - Date.now()))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// If there is no cached entry, trigger a poll immediately.
|
||||
poll()
|
||||
}
|
||||
setData({ key, result: entry?.result })
|
||||
|
||||
return () => {
|
||||
clearTimeout(timeout)
|
||||
}
|
||||
|
||||
async function poll(ttl = Date.now() + pollingInterval) {
|
||||
timeout = setTimeout(poll, pollingInterval) // queue the next poll
|
||||
cache.set(key, { ttl: null, ...cache.get(key) }) // mark the entry as a pending fetch
|
||||
|
||||
// Always set the result in the cache, but only set it as data if the key is still being queried.
|
||||
const result = await fetch()
|
||||
cache.set(key, { ttl, result })
|
||||
setData((data) => (data.key === key ? { key, result } : data))
|
||||
}
|
||||
}, [cache, debounce, fetch, keepUnusedDataFor, key, pollingInterval, staleCallback])
|
||||
|
||||
useEffect(() => {
|
||||
// Cleanup stale entries when a new key is used.
|
||||
void key
|
||||
|
||||
const now = Date.now()
|
||||
cache.forEach(({ ttl }, key) => {
|
||||
if (ttl && ttl + keepUnusedDataFor <= now) {
|
||||
cache.delete(key)
|
||||
}
|
||||
})
|
||||
}, [cache, keepUnusedDataFor, key])
|
||||
|
||||
// Use data.result to force a re-render, but actually retrieve the data from the cache.
|
||||
// This gives the _first_ render access to a new result, avoiding lag introduced by React.
|
||||
return cache.get(key)?.result
|
||||
}
|
||||
@@ -1,7 +1,5 @@
|
||||
import { css } from 'lib/theme'
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
|
||||
import useNativeEvent from './useNativeEvent'
|
||||
import { useMemo } from 'react'
|
||||
|
||||
const overflowCss = css`
|
||||
overflow-y: scroll;
|
||||
@@ -48,21 +46,15 @@ interface ScrollbarOptions {
|
||||
}
|
||||
|
||||
export default function useScrollbar(element: HTMLElement | null, { padded = false }: ScrollbarOptions = {}) {
|
||||
const [overflow, setOverflow] = useState(true)
|
||||
useEffect(() => {
|
||||
setOverflow(hasOverflow(element))
|
||||
}, [element])
|
||||
useNativeEvent(
|
||||
element,
|
||||
'transitionend',
|
||||
useCallback(() => setOverflow(hasOverflow(element)), [element])
|
||||
return useMemo(
|
||||
// NB: The css must be applied on an element's first render. WebKit will not re-apply overflow
|
||||
// properties until any transitions have ended, so waiting a frame for state would cause jank.
|
||||
() => (hasOverflow(element) ? scrollbarCss(padded) : overflowCss),
|
||||
[element, padded]
|
||||
)
|
||||
return useMemo(() => (overflow ? scrollbarCss(padded) : overflowCss), [overflow, padded])
|
||||
|
||||
function hasOverflow(element: HTMLElement | null) {
|
||||
if (!element) {
|
||||
return true
|
||||
}
|
||||
if (!element) return true
|
||||
return element.scrollHeight > element.clientHeight
|
||||
}
|
||||
}
|
||||
|
||||
41
src/lib/hooks/useSlippage.ts
Normal file
41
src/lib/hooks/useSlippage.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
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 { useMemo } from 'react'
|
||||
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)
|
||||
}
|
||||
|
||||
export interface Slippage {
|
||||
auto: boolean
|
||||
allowed: Percent
|
||||
warning?: 'warning' | 'error'
|
||||
}
|
||||
|
||||
/** Returns the allowed slippage, and whether it is auto-slippage. */
|
||||
export default function useSlippage(trade: InterfaceTrade<Currency, Currency, TradeType> | undefined): Slippage {
|
||||
const shouldUseAutoSlippage = useAtomValue(autoSlippageAtom)
|
||||
const autoSlippage = useAutoSlippageTolerance(shouldUseAutoSlippage ? trade : undefined)
|
||||
const maxSlippageValue = useAtomValue(maxSlippageAtom)
|
||||
const maxSlippage = useMemo(() => toPercent(maxSlippageValue), [maxSlippageValue])
|
||||
return useMemo(() => {
|
||||
const auto = shouldUseAutoSlippage || !maxSlippage
|
||||
const allowed = shouldUseAutoSlippage ? autoSlippage : maxSlippage ?? autoSlippage
|
||||
const warning = auto ? undefined : getSlippageWarning(allowed)
|
||||
return { auto, allowed, warning }
|
||||
}, [autoSlippage, maxSlippage, shouldUseAutoSlippage])
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import { NativeCurrency, Token } from '@uniswap/sdk-core'
|
||||
import { TokenInfo, TokenList } from '@uniswap/token-lists'
|
||||
import { atom } from 'jotai'
|
||||
import { useAtomValue, useUpdateAtom } from 'jotai/utils'
|
||||
import { atom, useAtom } from 'jotai'
|
||||
import { useAtomValue } from 'jotai/utils'
|
||||
import useActiveWeb3React from 'lib/hooks/useActiveWeb3React'
|
||||
import resolveENSContentHash from 'lib/utils/resolveENSContentHash'
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
@@ -14,31 +14,35 @@ import { validateTokens } from './validateTokenList'
|
||||
|
||||
export const DEFAULT_TOKEN_LIST = 'https://gateway.ipfs.io/ipns/tokens.uniswap.org'
|
||||
|
||||
const chainTokenMapAtom = atom<ChainTokenMap | undefined>(undefined)
|
||||
const chainTokenMapAtom = atom<ChainTokenMap | null>(null)
|
||||
|
||||
export function useIsTokenListLoaded() {
|
||||
return Boolean(useAtomValue(chainTokenMapAtom))
|
||||
}
|
||||
|
||||
export function useSyncTokenList(list: string | TokenInfo[] = DEFAULT_TOKEN_LIST): void {
|
||||
const { chainId, library } = useActiveWeb3React()
|
||||
const setChainTokenMap = useUpdateAtom(chainTokenMapAtom)
|
||||
|
||||
// Error boundaries will not catch (non-rendering) async errors, but it should still be shown
|
||||
const [error, setError] = useState<Error>()
|
||||
if (error) throw error
|
||||
|
||||
const [chainTokenMap, setChainTokenMap] = useAtom(chainTokenMapAtom)
|
||||
useEffect(() => setChainTokenMap(null), [list, setChainTokenMap])
|
||||
|
||||
const { chainId, library } = useActiveWeb3React()
|
||||
const resolver = useCallback(
|
||||
(ensName: string) => {
|
||||
if (library && chainId === 1) {
|
||||
// TODO(zzmp): Use network resolver when wallet is not on chainId === 1.
|
||||
return resolveENSContentHash(ensName, library)
|
||||
}
|
||||
throw new Error('Could not construct mainnet ENS resolver')
|
||||
},
|
||||
[chainId, library]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
// If the list was already loaded, don't reload it.
|
||||
if (chainTokenMap) return
|
||||
|
||||
let stale = false
|
||||
activateList(list)
|
||||
return () => {
|
||||
@@ -53,19 +57,20 @@ export function useSyncTokenList(list: string | TokenInfo[] = DEFAULT_TOKEN_LIST
|
||||
} else {
|
||||
tokens = await validateTokens(list)
|
||||
}
|
||||
const tokenMap = tokensToChainTokenMap(tokens) // also caches the fetched tokens, so it is invoked even if stale
|
||||
// tokensToChainTokenMap also caches the fetched tokens, so it must be invoked even if stale.
|
||||
const map = tokensToChainTokenMap(tokens)
|
||||
if (!stale) {
|
||||
setChainTokenMap(tokenMap)
|
||||
setChainTokenMap(map)
|
||||
setError(undefined)
|
||||
}
|
||||
} catch (e: unknown) {
|
||||
if (!stale) {
|
||||
setChainTokenMap(undefined)
|
||||
// Do not update the token map, in case the map was already resolved without error on mainnet.
|
||||
setError(e as Error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [list, resolver, setChainTokenMap])
|
||||
}, [chainTokenMap, list, resolver, setChainTokenMap])
|
||||
}
|
||||
|
||||
export default function useTokenList(): WrappedTokenInfo[] {
|
||||
|
||||
@@ -2,24 +2,42 @@ import { Currency, CurrencyAmount, Percent, Token } from '@uniswap/sdk-core'
|
||||
import { useUSDCValue } from 'hooks/useUSDCPrice'
|
||||
import { useMemo } from 'react'
|
||||
import { computeFiatValuePriceImpact } from 'utils/computeFiatValuePriceImpact'
|
||||
import { getPriceImpactWarning } from 'utils/prices'
|
||||
|
||||
export interface PriceImpact {
|
||||
percent: Percent
|
||||
warning?: 'warning' | 'error'
|
||||
toString(): string
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes input/output USDC equivalents and the price impact.
|
||||
* Returns the price impact as a human readable string.
|
||||
*/
|
||||
export default function useUSDCPriceImpact(
|
||||
inputAmount: CurrencyAmount<Currency> | undefined,
|
||||
outputAmount: CurrencyAmount<Currency> | undefined
|
||||
): {
|
||||
inputUSDC?: CurrencyAmount<Token>
|
||||
outputUSDC?: CurrencyAmount<Token>
|
||||
priceImpact?: Percent
|
||||
impact?: PriceImpact
|
||||
} {
|
||||
const inputUSDC = useUSDCValue(inputAmount) ?? undefined
|
||||
const outputUSDC = useUSDCValue(outputAmount) ?? undefined
|
||||
return useMemo(() => {
|
||||
const priceImpact = computeFiatValuePriceImpact(inputUSDC, outputUSDC)
|
||||
return { inputUSDC, outputUSDC, priceImpact }
|
||||
const impact = priceImpact
|
||||
? {
|
||||
percent: priceImpact,
|
||||
warning: getPriceImpactWarning(priceImpact),
|
||||
toString: () => toHumanReadablePriceImpact(priceImpact),
|
||||
}
|
||||
: undefined
|
||||
return { inputUSDC, outputUSDC, impact }
|
||||
}, [inputUSDC, outputUSDC])
|
||||
}
|
||||
|
||||
export function toHumanReadablePriceImpact(priceImpact: Percent): string {
|
||||
function toHumanReadablePriceImpact(priceImpact: Percent): string {
|
||||
const sign = priceImpact.lessThan(0) ? '+' : ''
|
||||
const number = parseFloat(priceImpact.multiply(-1)?.toSignificant(3))
|
||||
return `${sign}${number}%`
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
import { RefObject, useEffect } from 'react'
|
||||
|
||||
export const UNMOUNTING = 'unmounting'
|
||||
|
||||
/**
|
||||
* Delays a node's unmounting so that an animation may be applied.
|
||||
* An animation *must* be applied, or the node will not unmount.
|
||||
*/
|
||||
export default function useUnmount(node: RefObject<HTMLElement>) {
|
||||
useEffect(() => {
|
||||
const current = node.current
|
||||
const parent = current?.parentElement
|
||||
const removeChild = parent?.removeChild
|
||||
if (parent && removeChild) {
|
||||
parent.removeChild = function <T extends Node>(child: T) {
|
||||
if ((child as Node) === current) {
|
||||
current.classList.add(UNMOUNTING)
|
||||
current.onanimationend = () => {
|
||||
removeChild.call(parent, child)
|
||||
}
|
||||
return child
|
||||
} else {
|
||||
return removeChild.call(parent, child) as T
|
||||
}
|
||||
}
|
||||
}
|
||||
return () => {
|
||||
if (parent && removeChild) {
|
||||
parent.removeChild = removeChild
|
||||
}
|
||||
}
|
||||
}, [node])
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { i18n } from '@lingui/core'
|
||||
import { I18nProvider } from '@lingui/react'
|
||||
import { DEFAULT_CATALOG, DEFAULT_LOCALE, SupportedLocale } from 'constants/locales'
|
||||
import { SupportedLocale } from 'constants/locales'
|
||||
import {
|
||||
af,
|
||||
ar,
|
||||
@@ -78,12 +78,13 @@ const plurals: LocalePlural = {
|
||||
|
||||
export async function dynamicActivate(locale: SupportedLocale) {
|
||||
i18n.loadLocaleData(locale, { plurals: () => plurals[locale] })
|
||||
// There are no default messages in production; instead, bundle the default to save a network request:
|
||||
// see https://github.com/lingui/js-lingui/issues/388#issuecomment-497779030
|
||||
const catalog =
|
||||
locale === DEFAULT_LOCALE ? DEFAULT_CATALOG : await import(`${process.env.REACT_APP_LOCALES}/${locale}.js`)
|
||||
// Bundlers will either export it as default or as a named export named default.
|
||||
i18n.load(locale, catalog.messages || catalog.default.messages)
|
||||
try {
|
||||
// There are no default messages in production,
|
||||
// see https://github.com/lingui/js-lingui/issues/388#issuecomment-497779030
|
||||
const catalog = await import(`${process.env.REACT_APP_LOCALES}/${locale}.js`)
|
||||
// Bundlers will either export it as default or as a named export named default.
|
||||
i18n.load(locale, catalog.messages || catalog.default.messages)
|
||||
} catch {}
|
||||
i18n.activate(locale)
|
||||
}
|
||||
|
||||
|
||||
@@ -109,17 +109,18 @@ export const Check = styled(icon(CheckIcon))`
|
||||
`
|
||||
|
||||
export const Expando = styled(icon(ExpandoIcon))<{ open: boolean }>`
|
||||
path {
|
||||
.left,
|
||||
.right {
|
||||
transition: transform 0.25s ease-in-out;
|
||||
will-change: transform;
|
||||
}
|
||||
|
||||
&:first-child {
|
||||
transform: ${({ open }) => open && 'translateX(-25%)'};
|
||||
}
|
||||
.left {
|
||||
transform: ${({ open }) => (open ? undefined : 'translateX(-25%)')};
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
transform: ${({ open }) => open && 'translateX(25%)'};
|
||||
}
|
||||
.right {
|
||||
transform: ${({ open }) => (open ? undefined : 'translateX(25%)')};
|
||||
}
|
||||
`
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user