Compare commits
194 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f50bcbdb2d | ||
|
|
cbe421ee23 | ||
|
|
3439786c38 | ||
|
|
6294915be6 | ||
|
|
984c742d0e | ||
|
|
00b151d7fa | ||
|
|
5967cf5d9d | ||
|
|
e480f0ebe5 | ||
|
|
f6ceecbc5e | ||
|
|
a0348b45be | ||
|
|
e4b37cffcc | ||
|
|
dd69cccf91 | ||
|
|
8b228de88f | ||
|
|
f91fc3c6a6 | ||
|
|
e0e2b40f9f | ||
|
|
bc1c61b63a | ||
|
|
446ad3e0d4 | ||
|
|
65e58a08cf | ||
|
|
71b20b432c | ||
|
|
ecfa179b3f | ||
|
|
6c94a0f585 | ||
|
|
600aeaaff1 | ||
|
|
3bfbc74e47 | ||
|
|
84f76e34b2 | ||
|
|
b965bed865 | ||
|
|
a9039e8d0b | ||
|
|
60d35b46f3 | ||
|
|
3d422cf707 | ||
|
|
84c70ac84d | ||
|
|
de3a33dfcb | ||
|
|
99a084f230 | ||
|
|
e880955743 | ||
|
|
bbf43fcd27 | ||
|
|
a00ac56389 | ||
|
|
7201944bc2 | ||
|
|
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 | ||
|
|
e569dc2152 | ||
|
|
1aa042c5ef | ||
|
|
1450315b98 | ||
|
|
aefbb3d812 | ||
|
|
c3f12398cd | ||
|
|
2272f2a01a | ||
|
|
fb71078ea2 | ||
|
|
1c7c93191e | ||
|
|
0713f730b3 | ||
|
|
5f7a18b411 | ||
|
|
020c8d181a | ||
|
|
ab3f024031 | ||
|
|
d989c61de5 | ||
|
|
5dd8059734 | ||
|
|
b50e5511ea | ||
|
|
1efe5e9cd5 | ||
|
|
2944dc4d0b | ||
|
|
29ae755f2a | ||
|
|
27b831b301 | ||
|
|
6d9d38819e | ||
|
|
2de29129ed | ||
|
|
52af0e506b | ||
|
|
4d69c946bf | ||
|
|
542bf0bf66 | ||
|
|
a4fbfae4ba | ||
|
|
b2288258f2 | ||
|
|
8703013b2d | ||
|
|
4f6173675d | ||
|
|
2469eb58b9 | ||
|
|
e0a8ac2408 | ||
|
|
0a736b5e62 | ||
|
|
b44eb8877c | ||
|
|
92e61fa34b | ||
|
|
ef62fd33b2 | ||
|
|
96a42f66d4 | ||
|
|
c446f20d2f | ||
|
|
5a1ef8fb7d | ||
|
|
2863971640 | ||
|
|
dcaf10ec29 | ||
|
|
bca5113569 | ||
|
|
6779c1a024 | ||
|
|
f79ef12494 | ||
|
|
7bcda46934 | ||
|
|
f4ba24cfd5 | ||
|
|
59c6ab16dd | ||
|
|
db17dcbf2c | ||
|
|
1835de7f5f | ||
|
|
00f158209c | ||
|
|
2108ceedd5 | ||
|
|
ad080470da | ||
|
|
fc34912b53 | ||
|
|
c25d2b894c | ||
|
|
83c99b8c04 | ||
|
|
ccdf1e7575 | ||
|
|
c9faafee5e | ||
|
|
26a44fb51b | ||
|
|
1e16ac8449 |
@@ -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"
|
||||
|
||||
2
.github/workflows/integration-tests.yaml
vendored
@@ -38,10 +38,10 @@ jobs:
|
||||
run: yarn install --frozen-lockfile
|
||||
|
||||
- run: yarn cypress install
|
||||
|
||||
- run: yarn build
|
||||
env:
|
||||
CI: false
|
||||
REACT_APP_NETWORK_URL: 'https://mainnet.infura.io/v3/4bf032f2d38a4ed6bb975b80d6340847'
|
||||
REACT_APP_SERVICE_WORKER: false
|
||||
|
||||
- run: yarn test:e2e
|
||||
|
||||
1
.gitignore
vendored
@@ -19,6 +19,7 @@
|
||||
|
||||
# builds
|
||||
/build
|
||||
/cosmos-export
|
||||
/dist
|
||||
/dts
|
||||
|
||||
|
||||
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
@@ -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
@@ -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.
|
||||
@@ -7,4 +7,4 @@
|
||||
"overridePath": "cosmos.override.cjs"
|
||||
},
|
||||
"port": 5001
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,16 +2,27 @@ describe('Swap', () => {
|
||||
beforeEach(() => {
|
||||
cy.visit('/swap')
|
||||
})
|
||||
|
||||
it('starts with ETH selected by default', () => {
|
||||
cy.get('#swap-currency-input .token-amount-input').should('have.value', '')
|
||||
cy.get('#swap-currency-input .token-symbol-container').should('contain.text', 'ETH')
|
||||
cy.get('#swap-currency-output .token-amount-input').should('not.have.value')
|
||||
cy.get('#swap-currency-output .token-symbol-container').should('contain.text', 'Select a token')
|
||||
})
|
||||
|
||||
it('can enter an amount into input', () => {
|
||||
cy.get('#swap-currency-input .token-amount-input').type('0.001', { delay: 200 }).should('have.value', '0.001')
|
||||
cy.get('#swap-currency-input .token-amount-input')
|
||||
.clear()
|
||||
.type('0.001', { delay: 200 })
|
||||
.should('have.value', '0.001')
|
||||
})
|
||||
|
||||
it('zero swap amount', () => {
|
||||
cy.get('#swap-currency-input .token-amount-input').type('0.0', { delay: 200 }).should('have.value', '0.0')
|
||||
cy.get('#swap-currency-input .token-amount-input').clear().type('0.0', { delay: 200 }).should('have.value', '0.0')
|
||||
})
|
||||
|
||||
it('invalid swap amount', () => {
|
||||
cy.get('#swap-currency-input .token-amount-input').type('\\', { delay: 200 }).should('have.value', '')
|
||||
cy.get('#swap-currency-input .token-amount-input').clear().type('\\', { delay: 200 }).should('have.value', '')
|
||||
})
|
||||
|
||||
it('can enter an amount into output', () => {
|
||||
|
||||
67
package.json
@@ -1,38 +1,45 @@
|
||||
{
|
||||
"name": "@uniswap/widgets",
|
||||
"version": "0.0.14-beta",
|
||||
"version": "1.0.6",
|
||||
"description": "Uniswap Interface",
|
||||
"homepage": ".",
|
||||
"type": "module",
|
||||
"files": [
|
||||
"lib",
|
||||
"dist"
|
||||
],
|
||||
"type": "module",
|
||||
"types": "dist/index.d.ts",
|
||||
"main": "dist/cjs/index.cjs",
|
||||
"module": "dist/index.js",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/widgets.d.ts",
|
||||
"import": "./dist/widgets.esm.js",
|
||||
"require": "./dist/widgets.js"
|
||||
"types": "./dist/index.d.ts",
|
||||
"import": "./dist/index.js",
|
||||
"require": "./dist/cjs/index.cjs"
|
||||
},
|
||||
"./locales/*": {
|
||||
"import": "./dist/locales/*.js",
|
||||
"require": "./dist/cjs/locales/*.cjs"
|
||||
},
|
||||
"./fonts.css": {
|
||||
"import": "./dist/fonts.css",
|
||||
"require": "./dist/fonts.css"
|
||||
}
|
||||
},
|
||||
"types": "./dist/widgets.d.ts",
|
||||
"module": "./dist/widgets.esm.js",
|
||||
"main": "./dist/widgets.js",
|
||||
"devDependencies": {
|
||||
"@babel/plugin-transform-runtime": "^7.17.0",
|
||||
"@babel/preset-env": "^7.16.11",
|
||||
"@babel/preset-react": "^7.16.7",
|
||||
"@babel/preset-typescript": "^7.16.7",
|
||||
"@ethersproject/experimental": "^5.4.0",
|
||||
"@gnosis.pm/safe-apps-web3-react": "^0.6.0",
|
||||
"@graphql-codegen/cli": "1.21.5",
|
||||
"@graphql-codegen/typescript": "1.22.3",
|
||||
"@graphql-codegen/typescript-operations": "^1.18.2",
|
||||
"@graphql-codegen/typescript-rtk-query": "^1.1.1",
|
||||
"@lingui/cli": "^3.9.0",
|
||||
"@lingui/core": "^3.9.0",
|
||||
"@lingui/macro": "^3.9.0",
|
||||
"@lingui/react": "^3.9.0",
|
||||
"@metamask/jazzicon": "^2.0.0",
|
||||
"@reach/dialog": "^0.10.3",
|
||||
"@reach/portal": "^0.10.3",
|
||||
@@ -83,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.16-alpha.0",
|
||||
"@web3-react/walletconnect": "8.0.16-alpha.0",
|
||||
"@web3-react/metamask": "^8.0.19-beta.0",
|
||||
"@web3-react/walletconnect": "^8.0.26-beta.0",
|
||||
"array.prototype.flat": "^1.2.4",
|
||||
"array.prototype.flatmap": "^1.2.4",
|
||||
"babel-plugin-macros": "^3.1.0",
|
||||
@@ -112,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",
|
||||
@@ -126,6 +133,7 @@
|
||||
"rollup-plugin-copy": "^3.4.0",
|
||||
"rollup-plugin-delete": "^2.0.0",
|
||||
"rollup-plugin-dts": "^4.1.0",
|
||||
"rollup-plugin-multi-input": "^1.3.1",
|
||||
"rollup-plugin-node-externals": "^3.1.2",
|
||||
"rollup-plugin-scss": "^3.0.0",
|
||||
"rollup-plugin-typescript2": "^0.31.1",
|
||||
@@ -145,7 +153,7 @@
|
||||
"web3-react-portis-connector": "npm:@web3-react/portis-connector@^6.0.9",
|
||||
"web3-react-types": "npm:@web3-react/types@^6.0.7",
|
||||
"web3-react-walletconnect-connector": "npm:@web3-react/walletconnect-connector@^7.0.2-alpha.0",
|
||||
"web3-react-walletlink-connector": "npm:@web3-react/walletlink-connector@^6.2.11",
|
||||
"web3-react-walletlink-connector": "npm:@web3-react/walletlink-connector@^6.2.13",
|
||||
"workbox-core": "^6.1.0",
|
||||
"workbox-precaching": "^6.1.0",
|
||||
"workbox-routing": "^6.1.0"
|
||||
@@ -186,42 +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",
|
||||
"@lingui/core": "^3.9.0",
|
||||
"@lingui/macro": "^3.9.0",
|
||||
"@lingui/react": "^3.9.0",
|
||||
"@popperjs/core": "^2.4.4",
|
||||
"@reduxjs/toolkit": "^1.6.1",
|
||||
"@uniswap/redux-multicall": "^1.0.0",
|
||||
"@uniswap/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.16-alpha.0",
|
||||
"@web3-react/eip1193": "8.0.16-alpha.0",
|
||||
"@web3-react/empty": "8.0.17-alpha.0",
|
||||
"@web3-react/types": "8.0.16-alpha.0",
|
||||
"@web3-react/url": "8.0.17-alpha.0",
|
||||
"@web3-react/core": "^8.0.23-beta.0",
|
||||
"@web3-react/eip1193": "^8.0.18-beta.0",
|
||||
"@web3-react/empty": "^8.0.12-beta.0",
|
||||
"@web3-react/types": "^8.0.12-beta.0",
|
||||
"@web3-react/url": "^8.0.17-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",
|
||||
|
||||
@@ -17,6 +17,8 @@ import { RollupWarning } from 'rollup'
|
||||
import copy from 'rollup-plugin-copy'
|
||||
import del from 'rollup-plugin-delete'
|
||||
import dts from 'rollup-plugin-dts'
|
||||
// @ts-ignore // missing types
|
||||
import multi from 'rollup-plugin-multi-input'
|
||||
import externals from 'rollup-plugin-node-externals'
|
||||
import sass from 'rollup-plugin-scss'
|
||||
import { CompilerOptions } from 'typescript'
|
||||
@@ -24,6 +26,8 @@ import { CompilerOptions } from 'typescript'
|
||||
const REPLACEMENTS = {
|
||||
'process.env.REACT_APP_IS_WIDGET': true,
|
||||
'process.env.REACT_APP_LOCALES': '"./locales"',
|
||||
// esm requires fully-specified paths:
|
||||
'react/jsx-runtime': 'react/jsx-runtime.js',
|
||||
}
|
||||
|
||||
const EXTENSIONS = ['.js', '.jsx', '.ts', '.tsx']
|
||||
@@ -33,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
|
||||
@@ -45,54 +54,86 @@ const aliases = Object.entries({ ...paths }).flatMap(([find, replacements]) => {
|
||||
|
||||
const plugins = [
|
||||
// Dependency resolution
|
||||
externals({ exclude: ['constants'], deps: true, peerDeps: true }), // marks builtins, dependencies, and peerDependencies external
|
||||
resolve({ extensions: EXTENSIONS }), // resolves third-party modules within node_modules/
|
||||
alias({ entries: aliases }), // resolves paths aliased through the tsconfig (babel does not use tsconfig path resolution)
|
||||
|
||||
// Source code transformation
|
||||
replace({ ...REPLACEMENTS, preventAssignment: true }),
|
||||
json(), // imports json; doing so type-checking allows the json to be type-checked
|
||||
json(), // imports json as ES6; doing so enables type-checking and module resolution
|
||||
]
|
||||
|
||||
const check = {
|
||||
input: 'src/lib/index.tsx',
|
||||
output: { file: 'dist/widgets.tsc' },
|
||||
external: isAsset,
|
||||
plugins: [...plugins, typescript({ tsconfig: TS_CONFIG })],
|
||||
output: { file: 'dist/widgets.tsc', inlineDynamicImports: true },
|
||||
external: (source: string) => isAsset(source) || isEthers(source),
|
||||
plugins: [
|
||||
externals({ exclude: ['constants'], deps: true, peerDeps: true }), // marks builtins, dependencies, and peerDependencies external
|
||||
...plugins,
|
||||
typescript({ tsconfig: TS_CONFIG }),
|
||||
],
|
||||
onwarn: squelchTranspilationWarnings, // this pipeline is only for typechecking and generating definitions
|
||||
}
|
||||
|
||||
const type = {
|
||||
input: 'dist/dts/lib/index.d.ts',
|
||||
output: { file: 'dist/widgets.d.ts' },
|
||||
external: isAsset,
|
||||
output: { file: 'dist/index.d.ts' },
|
||||
external: (source: string) => isAsset(source) || isEthers(source),
|
||||
plugins: [
|
||||
externals({ exclude: ['constants'], deps: true, peerDeps: true }),
|
||||
dts({ compilerOptions: { baseUrl: 'dist/dts' } }),
|
||||
process.env.ROLLUP_WATCH ? undefined : del({ hook: 'buildEnd', targets: ['dist/widgets.tsc', 'dist/dts'] }),
|
||||
],
|
||||
}
|
||||
|
||||
/**
|
||||
* This exports scheme works for nextjs and for CRA5.
|
||||
*
|
||||
* It will also work for CRA4 if you use direct imports:
|
||||
* instead of `import { SwapWidget } from '@uniswap/widgets'`,
|
||||
* `import { SwapWidget } from '@uniswap/widgets/dist/index.js'`.
|
||||
* I do not know why CRA4 does not seem to use exports for resolution.
|
||||
*
|
||||
* Note that chunks are enabled. This is so the tokenlist spec can be loaded async,
|
||||
* to improve first load time (due to ajv). Locales are also in separate chunks.
|
||||
*
|
||||
* Lastly, note that JSON and lingui are bundled into the library, as neither are fully
|
||||
* supported/compatible with ES Modules. Both _could_ be bundled only with esm, but this
|
||||
* yields a less complex pipeline.
|
||||
*/
|
||||
|
||||
const transpile = {
|
||||
input: 'src/lib/index.tsx',
|
||||
output: [
|
||||
{
|
||||
file: 'dist/widgets.js',
|
||||
format: 'cjs',
|
||||
sourcemap: true,
|
||||
dir: 'dist',
|
||||
format: 'esm',
|
||||
sourcemap: false,
|
||||
},
|
||||
{
|
||||
file: 'dist/widgets.esm.js',
|
||||
format: 'esm',
|
||||
sourcemap: true,
|
||||
dir: 'dist/cjs',
|
||||
entryFileNames: '[name].cjs',
|
||||
chunkFileNames: '[name]-[hash].cjs',
|
||||
format: 'cjs',
|
||||
sourcemap: false,
|
||||
},
|
||||
],
|
||||
external: isEthers,
|
||||
plugins: [
|
||||
externals({
|
||||
exclude: [
|
||||
'constants',
|
||||
/@lingui\/(core|react)/, // @lingui incorrectly exports esm, so it must be bundled in
|
||||
/\.json$/, // esm does not support JSON loading, so it must be bundled in
|
||||
],
|
||||
deps: true,
|
||||
peerDeps: true,
|
||||
}),
|
||||
...plugins,
|
||||
|
||||
// Source code transformation
|
||||
url({ include: ASSET_EXTENSIONS.map((extname) => '**/*' + extname) }), // imports assets as data URIs
|
||||
url({ include: ASSET_EXTENSIONS.map((extname) => '**/*' + extname), limit: Infinity }), // imports assets as data URIs
|
||||
svgr({ exportType: 'named', svgo: false }), // imports svgs as React components
|
||||
sass({ output: 'dist/fonts.css' }), // generates widgets.css
|
||||
sass({ output: 'dist/fonts.css' }), // generates fonts.css
|
||||
commonjs(), // transforms cjs dependencies into tree-shakeable ES modules
|
||||
|
||||
babel({
|
||||
@@ -104,16 +145,30 @@ const transpile = {
|
||||
'@babel/plugin-transform-runtime', // embeds the babel runtime for library distribution
|
||||
],
|
||||
}),
|
||||
|
||||
copy({
|
||||
copyOnce: true,
|
||||
targets: [{ src: 'src/locales/*.js', dest: 'dist/locales' }],
|
||||
}),
|
||||
],
|
||||
onwarn: squelchTypeWarnings, // this pipeline is only for transpilation
|
||||
}
|
||||
|
||||
const config = [check, type, transpile]
|
||||
const locales = {
|
||||
input: 'src/locales/*.js',
|
||||
output: [
|
||||
{
|
||||
dir: 'dist',
|
||||
format: 'esm',
|
||||
sourcemap: false,
|
||||
},
|
||||
],
|
||||
plugins: [
|
||||
copy({
|
||||
copyOnce: true,
|
||||
targets: [{ src: 'src/locales/*.js', dest: 'dist/cjs/locales', rename: (name) => `${name}.cjs` }],
|
||||
}),
|
||||
commonjs(),
|
||||
multi(),
|
||||
],
|
||||
}
|
||||
|
||||
const config = [check, type, transpile, locales]
|
||||
export default config
|
||||
|
||||
function squelchTranspilationWarnings(warning: RollupWarning, warn: (warning: RollupWarning) => void) {
|
||||
|
||||
BIN
src/assets/images/ukraine.png
Normal file
|
After Width: | Height: | Size: 121 KiB |
BIN
src/assets/images/widget-screenshot.png
Normal file
|
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,3 +1,3 @@
|
||||
<svg width="100%" height="35" viewBox="800 0 300 200" xmlns="http://www.w3.org/2000/svg">
|
||||
<line x1="0" x2="2000" y1="100" y2="100" stroke="currentColor" stroke-width="20" stroke-linecap="round" stroke-dasharray="1, 45"/>
|
||||
<svg width="100%" height="35" viewBox="850 0 300 200" xmlns="http://www.w3.org/2000/svg">
|
||||
<line x1="0" x2="3000" y1="100" y2="100" stroke="currentColor" stroke-width="20" stroke-linecap="round" stroke-dasharray="1, 45"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 233 B After Width: | Height: | Size: 233 B |
@@ -1,16 +1,5 @@
|
||||
<svg width="170" height="168" viewBox="0 0 170 168" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0)">
|
||||
<path opacity="0.6" d="M85.05 168C132.022 168 170.1 130.105 170.1 83.3593C170.1 36.6135 0 36.6135 0 83.3593C0 130.105 38.0782 168 85.05 168Z" fill="#FF505F"/>
|
||||
<path opacity="0.6" d="M85.05 168C132.022 168 170.1 130.105 170.1 83.3593C170.1 36.6135 0 36.6135 0 83.3593C0 130.105 38.0782 168 85.05 168Z" fill="#FF0320"/>
|
||||
<path d="M85.05 0C132.022 0 170.1 37.8949 170.1 84.6407C170.1 131.386 0 131.386 0 84.6407C0 37.8949 38.0782 0 85.05 0Z" fill="white"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M144.665 64.0394L112.444 12.3742L89.0263 78.9477L144.665 64.0394Z" fill="#FF4E65"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M143.777 64.215L112.444 12.3742L165.349 58.4347L143.777 64.215Z" fill="white"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M144.551 63.613L142.479 124.467L88.912 78.5213L144.551 63.613Z" fill="#D0001A"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M143.663 63.7886L142.479 124.467L165.235 58.0083L143.663 63.7886Z" fill="#FF697B"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0">
|
||||
<rect width="170" height="168" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
<svg width="500" height="500" viewBox="0 0 500 500" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="250" cy="250" r="250" fill="#FF0420"/>
|
||||
<path d="M177.133 316.446C162.247 316.446 150.051 312.943 140.544 305.938C131.162 298.808 126.471 288.676 126.471 275.541C126.471 272.789 126.784 269.411 127.409 265.408C129.036 256.402 131.35 245.581 134.352 232.947C142.858 198.547 164.812 181.347 200.213 181.347C209.845 181.347 218.476 182.973 226.107 186.225C233.738 189.352 239.742 194.106 244.12 200.486C248.498 206.74 250.688 214.246 250.688 223.002C250.688 225.629 250.375 228.944 249.749 232.947C247.873 244.08 245.621 254.901 242.994 265.408C238.616 282.546 231.048 295.368 220.29 303.874C209.532 312.255 195.147 316.446 177.133 316.446ZM179.76 289.426C186.766 289.426 192.707 287.362 197.586 283.234C202.59 279.106 206.155 272.789 208.281 264.283C211.158 252.524 213.348 242.266 214.849 233.51C215.349 230.883 215.599 228.194 215.599 225.441C215.599 214.058 209.657 208.366 197.774 208.366C190.768 208.366 184.764 210.43 179.76 214.558C174.882 218.687 171.379 225.004 169.253 233.51C167.001 241.891 164.749 252.149 162.498 264.283C161.997 266.784 161.747 269.411 161.747 272.163C161.747 283.672 167.752 289.426 179.76 289.426Z" fill="white"/>
|
||||
<path d="M259.303 314.57C257.927 314.57 256.863 314.132 256.113 313.256C255.487 312.255 255.3 311.13 255.55 309.879L281.444 187.914C281.694 186.538 282.382 185.412 283.508 184.536C284.634 183.661 285.822 183.223 287.073 183.223H336.985C350.87 183.223 362.003 186.1 370.384 191.854C378.891 197.609 383.144 205.927 383.144 216.81C383.144 219.937 382.769 223.19 382.018 226.567C378.891 240.953 372.574 251.586 363.067 258.466C353.685 265.346 340.8 268.786 324.413 268.786H299.082L290.451 309.879C290.2 311.255 289.512 312.38 288.387 313.256C287.261 314.132 286.072 314.57 284.822 314.57H259.303ZM325.727 242.892C330.98 242.892 335.546 241.453 339.424 238.576C343.427 235.699 346.054 231.571 347.305 226.192C347.68 224.065 347.868 222.189 347.868 220.563C347.868 216.935 346.805 214.183 344.678 212.307C342.551 210.305 338.924 209.305 333.795 209.305H311.278L304.148 242.892H325.727Z" fill="white"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 2.1 KiB |
@@ -33,6 +33,7 @@ const BLOCKED_ADDRESSES: string[] = [
|
||||
'0x5512d943ed1f7c8a43f3435c85f7ab68b30121b0',
|
||||
'0xc455f7fd3e0e12afd51fba5c106909934d8a0e4a',
|
||||
'0x629e7Da20197a5429d30da36E77d06CdF796b71A',
|
||||
'0x7FF9cFad3877F21d41Da833E2F775dB0569eE3D9',
|
||||
]
|
||||
|
||||
export default function Blocklist({ children }: { children: ReactNode }) {
|
||||
|
||||
@@ -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,13 @@ type ErrorBoundaryState = {
|
||||
|
||||
const IS_UNISWAP = window.location.hostname === 'app.uniswap.org'
|
||||
|
||||
async function updateServiceWorker(): Promise<ServiceWorkerRegistration> {
|
||||
const ready = await navigator.serviceWorker.ready
|
||||
// the return type of update is incorrectly typed as Promise<void>. See
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerRegistration/update
|
||||
return ready.update() as unknown as Promise<ServiceWorkerRegistration>
|
||||
}
|
||||
|
||||
export default class ErrorBoundary extends React.Component<unknown, ErrorBoundaryState> {
|
||||
constructor(props: unknown) {
|
||||
super(props)
|
||||
@@ -56,15 +63,29 @@ export default class ErrorBoundary extends React.Component<unknown, ErrorBoundar
|
||||
}
|
||||
|
||||
static getDerivedStateFromError(error: Error): ErrorBoundaryState {
|
||||
updateServiceWorker()
|
||||
.then(async (registration) => {
|
||||
// We want to refresh only if we detect a new service worker is waiting to be activated.
|
||||
// See details about it: https://developers.google.com/web/fundamentals/primers/service-workers/lifecycle
|
||||
if (registration?.waiting) {
|
||||
await registration.unregister()
|
||||
|
||||
// Makes Workbox call skipWaiting(). For more info on skipWaiting see: https://developer.chrome.com/docs/workbox/handling-service-worker-updates/
|
||||
registration.waiting.postMessage({ type: 'SKIP_WAITING' })
|
||||
|
||||
// Once the service worker is unregistered, we can reload the page to let
|
||||
// the browser download a fresh copy of our app (invalidating the cache)
|
||||
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'
|
||||
|
||||
@@ -51,6 +51,16 @@ const FlyoutHeader = styled.div`
|
||||
font-weight: 400;
|
||||
`
|
||||
const FlyoutMenu = styled.div`
|
||||
position: absolute;
|
||||
top: 54px;
|
||||
width: 272px;
|
||||
z-index: 99;
|
||||
padding-top: 10px;
|
||||
@media screen and (min-width: ${MEDIA_WIDTHS.upToSmall}px) {
|
||||
top: 40px;
|
||||
}
|
||||
`
|
||||
const FlyoutMenuContents = styled.div`
|
||||
align-items: flex-start;
|
||||
background-color: ${({ theme }) => theme.bg0};
|
||||
box-shadow: 0px 0px 1px rgba(0, 0, 0, 0.01), 0px 4px 8px rgba(0, 0, 0, 0.04), 0px 16px 24px rgba(0, 0, 0, 0.04),
|
||||
@@ -61,16 +71,9 @@ const FlyoutMenu = styled.div`
|
||||
font-size: 16px;
|
||||
overflow: auto;
|
||||
padding: 16px;
|
||||
position: absolute;
|
||||
top: 64px;
|
||||
width: 272px;
|
||||
z-index: 99;
|
||||
& > *:not(:last-child) {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
@media screen and (min-width: ${MEDIA_WIDTHS.upToSmall}px) {
|
||||
top: 50px;
|
||||
}
|
||||
`
|
||||
const FlyoutRow = styled.div<{ active: boolean }>`
|
||||
align-items: center;
|
||||
@@ -252,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()
|
||||
@@ -305,21 +308,23 @@ export default function NetworkSelector() {
|
||||
}
|
||||
|
||||
return (
|
||||
<SelectorWrapper ref={node as any}>
|
||||
<SelectorControls onClick={toggle} interactive>
|
||||
<SelectorWrapper ref={node as any} onMouseEnter={toggle} onMouseLeave={toggle}>
|
||||
<SelectorControls interactive>
|
||||
<SelectorLogo interactive src={info.logoUrl} />
|
||||
<SelectorLabel>{info.label}</SelectorLabel>
|
||||
<StyledChevronDown />
|
||||
</SelectorControls>
|
||||
{open && (
|
||||
<FlyoutMenu onMouseLeave={toggle}>
|
||||
<FlyoutHeader>
|
||||
<Trans>Select a network</Trans>
|
||||
</FlyoutHeader>
|
||||
<Row onSelectChain={handleChainSwitch} targetChain={SupportedChainId.MAINNET} />
|
||||
<Row onSelectChain={handleChainSwitch} targetChain={SupportedChainId.POLYGON} />
|
||||
<Row onSelectChain={handleChainSwitch} targetChain={SupportedChainId.OPTIMISM} />
|
||||
<Row onSelectChain={handleChainSwitch} targetChain={SupportedChainId.ARBITRUM_ONE} />
|
||||
<FlyoutMenu>
|
||||
<FlyoutMenuContents>
|
||||
<FlyoutHeader>
|
||||
<Trans>Select a network</Trans>
|
||||
</FlyoutHeader>
|
||||
<Row onSelectChain={handleChainSwitch} targetChain={SupportedChainId.MAINNET} />
|
||||
<Row onSelectChain={handleChainSwitch} targetChain={SupportedChainId.POLYGON} />
|
||||
<Row onSelectChain={handleChainSwitch} targetChain={SupportedChainId.OPTIMISM} />
|
||||
<Row onSelectChain={handleChainSwitch} targetChain={SupportedChainId.ARBITRUM_ONE} />
|
||||
</FlyoutMenuContents>
|
||||
</FlyoutMenu>
|
||||
)}
|
||||
</SelectorWrapper>
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -14,11 +14,12 @@ const MobilePopupWrapper = styled.div<{ height: string | number }>`
|
||||
max-width: 100%;
|
||||
height: ${({ height }) => height};
|
||||
margin: ${({ height }) => (height ? '0 auto;' : 0)};
|
||||
margin-bottom: ${({ height }) => (height ? '20px' : 0)}};
|
||||
margin-bottom: ${({ height }) => (height ? '20px' : 0)};
|
||||
|
||||
display: none;
|
||||
${({ theme }) => theme.mediaWidth.upToSmall`
|
||||
display: block;
|
||||
padding-top: 20px;
|
||||
`};
|
||||
`
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ import { PositionDetails } from 'types/position'
|
||||
import { formatTickPrice } from 'utils/formatTickPrice'
|
||||
import { unwrappedToken } from 'utils/unwrappedToken'
|
||||
|
||||
import { DAI, USDC, USDT, WBTC, WRAPPED_NATIVE_CURRENCY } from '../../constants/tokens'
|
||||
import { DAI, USDC_MAINNET, USDT, WBTC, WRAPPED_NATIVE_CURRENCY } from '../../constants/tokens'
|
||||
|
||||
const LinkRow = styled(Link)`
|
||||
align-items: center;
|
||||
@@ -145,7 +145,7 @@ export function getPriceOrderingFromPositionForUI(position?: Position): {
|
||||
const token1 = position.amount1.currency
|
||||
|
||||
// if token0 is a dollar-stable asset, set it as the quote token
|
||||
const stables = [DAI, USDC, USDT]
|
||||
const stables = [DAI, USDC_MAINNET, USDT]
|
||||
if (stables.some((stable) => stable.equals(token0))) {
|
||||
return {
|
||||
priceLower: position.token0PriceUpper.invert(),
|
||||
@@ -157,7 +157,7 @@ export function getPriceOrderingFromPositionForUI(position?: Position): {
|
||||
|
||||
// if token1 is an ETH-/BTC-stable asset, set it as the base token
|
||||
const bases = [...Object.values(WRAPPED_NATIVE_CURRENCY), WBTC]
|
||||
if (bases.some((base) => base.equals(token1))) {
|
||||
if (bases.some((base) => base && base.equals(token1))) {
|
||||
return {
|
||||
priceLower: position.token0PriceUpper.invert(),
|
||||
priceUpper: position.token0PriceLower.invert(),
|
||||
|
||||
@@ -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'
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Protocol } from '@uniswap/router-sdk'
|
||||
import { Currency, Percent } from '@uniswap/sdk-core'
|
||||
import { FeeAmount } from '@uniswap/v3-sdk'
|
||||
import { DAI, USDC, WBTC } from 'constants/tokens'
|
||||
import { DAI, USDC_MAINNET, WBTC } from 'constants/tokens'
|
||||
import { render } from 'test-utils'
|
||||
|
||||
import RoutingDiagram, { RoutingDiagramEntry } from './RoutingDiagram'
|
||||
@@ -10,16 +10,16 @@ const percent = (strings: TemplateStringsArray) => new Percent(parseInt(strings[
|
||||
|
||||
const singleRoute: RoutingDiagramEntry = {
|
||||
percent: percent`100`,
|
||||
path: [[USDC, DAI, FeeAmount.LOW]],
|
||||
path: [[USDC_MAINNET, DAI, FeeAmount.LOW]],
|
||||
protocol: Protocol.V3,
|
||||
}
|
||||
|
||||
const multiRoute: RoutingDiagramEntry[] = [
|
||||
{ percent: percent`75`, path: [[USDC, DAI, FeeAmount.LOWEST]], protocol: Protocol.V2 },
|
||||
{ percent: percent`75`, path: [[USDC_MAINNET, DAI, FeeAmount.LOWEST]], protocol: Protocol.V2 },
|
||||
{
|
||||
percent: percent`25`,
|
||||
path: [
|
||||
[USDC, WBTC, FeeAmount.MEDIUM],
|
||||
[USDC_MAINNET, WBTC, FeeAmount.MEDIUM],
|
||||
[WBTC, DAI, FeeAmount.HIGH],
|
||||
],
|
||||
protocol: Protocol.V3,
|
||||
@@ -47,16 +47,16 @@ jest.mock('hooks/useTokenInfoFromActiveList', () => ({
|
||||
}))
|
||||
|
||||
it('renders when no routes are provided', () => {
|
||||
const { asFragment } = render(<RoutingDiagram currencyIn={DAI} currencyOut={USDC} routes={[]} />)
|
||||
const { asFragment } = render(<RoutingDiagram currencyIn={DAI} currencyOut={USDC_MAINNET} routes={[]} />)
|
||||
expect(asFragment()).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it('renders single route', () => {
|
||||
const { asFragment } = render(<RoutingDiagram currencyIn={USDC} currencyOut={DAI} routes={[singleRoute]} />)
|
||||
const { asFragment } = render(<RoutingDiagram currencyIn={USDC_MAINNET} currencyOut={DAI} routes={[singleRoute]} />)
|
||||
expect(asFragment()).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it('renders multi route', () => {
|
||||
const { asFragment } = render(<RoutingDiagram currencyIn={USDC} currencyOut={DAI} routes={multiRoute} />)
|
||||
const { asFragment } = render(<RoutingDiagram currencyIn={USDC_MAINNET} currencyOut={DAI} routes={multiRoute} />)
|
||||
expect(asFragment()).toMatchSnapshot()
|
||||
})
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -48,7 +48,7 @@ export function ImportToken(props: ImportProps) {
|
||||
<RowBetween>
|
||||
{onBack ? <ArrowLeft style={{ cursor: 'pointer' }} onClick={onBack} /> : <div />}
|
||||
<ThemedText.MediumHeader>
|
||||
<Plural value={tokens.length} one="Import token" other="Import tokens" />
|
||||
<Plural value={tokens.length} _1="Import token" other="Import tokens" />
|
||||
</ThemedText.MediumHeader>
|
||||
{onDismiss ? <CloseIcon onClick={onDismiss} /> : <div />}
|
||||
</RowBetween>
|
||||
|
||||
@@ -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
@@ -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.
|
||||
|
||||
@@ -12,6 +12,8 @@ export const COMMON_CONTRACT_NAMES: Record<number, { [address: string]: string }
|
||||
[TIMELOCK_ADDRESS[SupportedChainId.MAINNET]]: 'Timelock',
|
||||
[GOVERNANCE_ALPHA_V0_ADDRESSES[SupportedChainId.MAINNET]]: 'Governance (V0)',
|
||||
[GOVERNANCE_ALPHA_V1_ADDRESSES[SupportedChainId.MAINNET]]: 'Governance',
|
||||
'0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e': 'ENS Registry',
|
||||
'0x4976fb03C32e5B8cfe2b6cCB31c09Ba78EBaBa41': 'ENS Public Resolver',
|
||||
},
|
||||
}
|
||||
|
||||
@@ -21,3 +23,5 @@ export const DEFAULT_AVERAGE_BLOCK_TIME_IN_SECS = 13
|
||||
export const AVERAGE_BLOCK_TIME_IN_SECS: { [chainId: number]: number } = {
|
||||
1: DEFAULT_AVERAGE_BLOCK_TIME_IN_SECS,
|
||||
}
|
||||
|
||||
export const LATEST_GOVERNOR_INDEX = 2
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -18,8 +18,8 @@ import {
|
||||
sETH2,
|
||||
SWISE,
|
||||
TRIBE,
|
||||
USDC,
|
||||
USDC_ARBITRUM,
|
||||
USDC_MAINNET,
|
||||
USDC_OPTIMISM,
|
||||
USDC_POLYGON,
|
||||
USDT,
|
||||
@@ -44,13 +44,21 @@ type ChainCurrencyList = {
|
||||
}
|
||||
|
||||
const WRAPPED_NATIVE_CURRENCIES_ONLY: ChainTokenList = Object.fromEntries(
|
||||
Object.entries(WRAPPED_NATIVE_CURRENCY).map(([key, value]) => [key, [value]])
|
||||
Object.entries(WRAPPED_NATIVE_CURRENCY)
|
||||
.map(([key, value]) => [key, [value]])
|
||||
.filter(Boolean)
|
||||
)
|
||||
|
||||
// used to construct intermediary pairs for trading
|
||||
export const BASES_TO_CHECK_TRADES_AGAINST: ChainTokenList = {
|
||||
...WRAPPED_NATIVE_CURRENCIES_ONLY,
|
||||
[SupportedChainId.MAINNET]: [...WRAPPED_NATIVE_CURRENCIES_ONLY[SupportedChainId.MAINNET], DAI, USDC, USDT, WBTC],
|
||||
[SupportedChainId.MAINNET]: [
|
||||
...WRAPPED_NATIVE_CURRENCIES_ONLY[SupportedChainId.MAINNET],
|
||||
DAI,
|
||||
USDC_MAINNET,
|
||||
USDT,
|
||||
WBTC,
|
||||
],
|
||||
[SupportedChainId.OPTIMISM]: [
|
||||
...WRAPPED_NATIVE_CURRENCIES_ONLY[SupportedChainId.OPTIMISM],
|
||||
DAI_OPTIMISM,
|
||||
@@ -90,7 +98,7 @@ export const ADDITIONAL_BASES: { [chainId: number]: { [tokenAddress: string]: To
|
||||
*/
|
||||
export const CUSTOM_BASES: { [chainId: number]: { [tokenAddress: string]: Token[] } } = {
|
||||
[SupportedChainId.MAINNET]: {
|
||||
[AMPL.address]: [DAI, WRAPPED_NATIVE_CURRENCY[SupportedChainId.MAINNET]],
|
||||
[AMPL.address]: [DAI, WRAPPED_NATIVE_CURRENCY[SupportedChainId.MAINNET] as Token],
|
||||
},
|
||||
}
|
||||
|
||||
@@ -101,32 +109,38 @@ export const COMMON_BASES: ChainCurrencyList = {
|
||||
[SupportedChainId.MAINNET]: [
|
||||
nativeOnChain(SupportedChainId.MAINNET),
|
||||
DAI,
|
||||
USDC,
|
||||
USDC_MAINNET,
|
||||
USDT,
|
||||
WBTC,
|
||||
WRAPPED_NATIVE_CURRENCY[SupportedChainId.MAINNET],
|
||||
WRAPPED_NATIVE_CURRENCY[SupportedChainId.MAINNET] as Token,
|
||||
],
|
||||
[SupportedChainId.ROPSTEN]: [
|
||||
nativeOnChain(SupportedChainId.ROPSTEN),
|
||||
WRAPPED_NATIVE_CURRENCY[SupportedChainId.ROPSTEN],
|
||||
WRAPPED_NATIVE_CURRENCY[SupportedChainId.ROPSTEN] as Token,
|
||||
],
|
||||
[SupportedChainId.RINKEBY]: [
|
||||
nativeOnChain(SupportedChainId.RINKEBY),
|
||||
WRAPPED_NATIVE_CURRENCY[SupportedChainId.RINKEBY],
|
||||
WRAPPED_NATIVE_CURRENCY[SupportedChainId.RINKEBY] as Token,
|
||||
],
|
||||
[SupportedChainId.GOERLI]: [
|
||||
nativeOnChain(SupportedChainId.GOERLI),
|
||||
WRAPPED_NATIVE_CURRENCY[SupportedChainId.GOERLI] as Token,
|
||||
],
|
||||
[SupportedChainId.KOVAN]: [
|
||||
nativeOnChain(SupportedChainId.KOVAN),
|
||||
WRAPPED_NATIVE_CURRENCY[SupportedChainId.KOVAN] as Token,
|
||||
],
|
||||
[SupportedChainId.GOERLI]: [nativeOnChain(SupportedChainId.GOERLI), WRAPPED_NATIVE_CURRENCY[SupportedChainId.GOERLI]],
|
||||
[SupportedChainId.KOVAN]: [nativeOnChain(SupportedChainId.KOVAN), WRAPPED_NATIVE_CURRENCY[SupportedChainId.KOVAN]],
|
||||
[SupportedChainId.ARBITRUM_ONE]: [
|
||||
nativeOnChain(SupportedChainId.ARBITRUM_ONE),
|
||||
DAI_ARBITRUM_ONE,
|
||||
USDC_ARBITRUM,
|
||||
USDT_ARBITRUM_ONE,
|
||||
WBTC_ARBITRUM_ONE,
|
||||
WRAPPED_NATIVE_CURRENCY[SupportedChainId.ARBITRUM_ONE],
|
||||
WRAPPED_NATIVE_CURRENCY[SupportedChainId.ARBITRUM_ONE] as Token,
|
||||
],
|
||||
[SupportedChainId.ARBITRUM_RINKEBY]: [
|
||||
nativeOnChain(SupportedChainId.ARBITRUM_RINKEBY),
|
||||
WRAPPED_NATIVE_CURRENCY[SupportedChainId.ARBITRUM_RINKEBY],
|
||||
WRAPPED_NATIVE_CURRENCY[SupportedChainId.ARBITRUM_RINKEBY] as Token,
|
||||
],
|
||||
[SupportedChainId.OPTIMISM]: [
|
||||
nativeOnChain(SupportedChainId.OPTIMISM),
|
||||
@@ -146,7 +160,7 @@ export const COMMON_BASES: ChainCurrencyList = {
|
||||
],
|
||||
[SupportedChainId.POLYGON_MUMBAI]: [
|
||||
nativeOnChain(SupportedChainId.POLYGON_MUMBAI),
|
||||
WRAPPED_NATIVE_CURRENCY[SupportedChainId.POLYGON_MUMBAI],
|
||||
WRAPPED_NATIVE_CURRENCY[SupportedChainId.POLYGON_MUMBAI] as Token,
|
||||
WETH_POLYGON_MUMBAI,
|
||||
],
|
||||
}
|
||||
@@ -154,7 +168,13 @@ export const COMMON_BASES: ChainCurrencyList = {
|
||||
// used to construct the list of all pairs we consider by default in the frontend
|
||||
export const BASES_TO_TRACK_LIQUIDITY_FOR: ChainTokenList = {
|
||||
...WRAPPED_NATIVE_CURRENCIES_ONLY,
|
||||
[SupportedChainId.MAINNET]: [...WRAPPED_NATIVE_CURRENCIES_ONLY[SupportedChainId.MAINNET], DAI, USDC, USDT, WBTC],
|
||||
[SupportedChainId.MAINNET]: [
|
||||
...WRAPPED_NATIVE_CURRENCIES_ONLY[SupportedChainId.MAINNET],
|
||||
DAI,
|
||||
USDC_MAINNET,
|
||||
USDT,
|
||||
WBTC,
|
||||
],
|
||||
}
|
||||
export const PINNED_PAIRS: { readonly [chainId: number]: [Token, Token][] } = {
|
||||
[SupportedChainId.MAINNET]: [
|
||||
@@ -168,7 +188,7 @@ export const PINNED_PAIRS: { readonly [chainId: number]: [Token, Token][] } = {
|
||||
'Compound USD Coin'
|
||||
),
|
||||
],
|
||||
[USDC, USDT],
|
||||
[USDC_MAINNET, USDT],
|
||||
[DAI, USDT],
|
||||
],
|
||||
}
|
||||
|
||||
@@ -1101,6 +1101,20 @@
|
||||
"name": "BMEX",
|
||||
"symbol": "BMEX",
|
||||
"decimals": 18
|
||||
},
|
||||
{
|
||||
"chainId": 1,
|
||||
"address": "0x322A46E88fa3C78F9c9E3DBb0254b61664a06109",
|
||||
"name": "Ukraine DAO",
|
||||
"symbol": "Ukraine",
|
||||
"decimals": 18
|
||||
},
|
||||
{
|
||||
"chainId": 1,
|
||||
"address": "0x8290D7a64F25e6b5002d98367E8367c1b532b534",
|
||||
"name": "oneUNI",
|
||||
"symbol": "oneUNI",
|
||||
"decimals": 18
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,8 +1,87 @@
|
||||
import { Currency, Ether, NativeCurrency, Token, WETH9 } from '@uniswap/sdk-core'
|
||||
import invariant from 'tiny-invariant'
|
||||
|
||||
import { UNI_ADDRESS } from './addresses'
|
||||
import { SupportedChainId } from './chains'
|
||||
|
||||
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,
|
||||
'0xD46bA6D942050d489DBd938a2C909A5d5039A161',
|
||||
@@ -31,27 +110,19 @@ export const DAI_OPTIMISM = new Token(
|
||||
'DAI',
|
||||
'Dai stable coin'
|
||||
)
|
||||
export const USDC = new Token(
|
||||
SupportedChainId.MAINNET,
|
||||
'0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48',
|
||||
6,
|
||||
'USDC',
|
||||
'USD//C'
|
||||
)
|
||||
export const USDC_ARBITRUM = new Token(
|
||||
SupportedChainId.ARBITRUM_ONE,
|
||||
'0xFF970A61A04b1cA14834A43f5dE4533eBDDB5CC8',
|
||||
6,
|
||||
'USDC',
|
||||
'USD//C'
|
||||
)
|
||||
export const USDC_POLYGON = new Token(
|
||||
SupportedChainId.POLYGON,
|
||||
'0x2791bca1f2de4661ed88a30c99a7a9449aa84174',
|
||||
6,
|
||||
'USDC',
|
||||
'USD//C'
|
||||
)
|
||||
export const USDC: { [chainId in SupportedChainId]: Token } = {
|
||||
[SupportedChainId.MAINNET]: USDC_MAINNET,
|
||||
[SupportedChainId.ARBITRUM_ONE]: USDC_ARBITRUM,
|
||||
[SupportedChainId.OPTIMISM]: USDC_OPTIMISM,
|
||||
[SupportedChainId.ARBITRUM_RINKEBY]: USDC_ARBITRUM_RINKEBY,
|
||||
[SupportedChainId.OPTIMISTIC_KOVAN]: USDC_OPTIMISTIC_KOVAN,
|
||||
[SupportedChainId.POLYGON]: USDC_POLYGON,
|
||||
[SupportedChainId.POLYGON_MUMBAI]: USDC_POLYGON_MUMBAI,
|
||||
[SupportedChainId.GOERLI]: USDC_GOERLI,
|
||||
[SupportedChainId.RINKEBY]: USDC_RINKEBY,
|
||||
[SupportedChainId.KOVAN]: USDC_KOVAN,
|
||||
[SupportedChainId.ROPSTEN]: USDC_ROPSTEN,
|
||||
}
|
||||
export const DAI_POLYGON = new Token(
|
||||
SupportedChainId.POLYGON,
|
||||
'0x8f3Cf7ad23Cd3CaDbD9735AFf958023239c6A063',
|
||||
@@ -73,13 +144,6 @@ export const WBTC_POLYGON = new Token(
|
||||
'WBTC',
|
||||
'Wrapped BTC'
|
||||
)
|
||||
export const USDC_OPTIMISM = new Token(
|
||||
SupportedChainId.OPTIMISM,
|
||||
'0x7F5c764cBc14f9669B88837ca1490cCa17c31607',
|
||||
6,
|
||||
'USDC',
|
||||
'USD//C'
|
||||
)
|
||||
export const USDT = new Token(
|
||||
SupportedChainId.MAINNET,
|
||||
'0xdAC17F958D2ee523a2206206994597C13D831ec7',
|
||||
@@ -208,8 +272,8 @@ export const UNI: { [chainId: number]: Token } = {
|
||||
[SupportedChainId.KOVAN]: new Token(SupportedChainId.KOVAN, UNI_ADDRESS[42], 18, 'UNI', 'Uniswap'),
|
||||
}
|
||||
|
||||
export const WRAPPED_NATIVE_CURRENCY: { [chainId: number]: Token } = {
|
||||
...WETH9,
|
||||
export const WRAPPED_NATIVE_CURRENCY: { [chainId: number]: Token | undefined } = {
|
||||
...(WETH9 as Record<SupportedChainId, Token>),
|
||||
[SupportedChainId.OPTIMISM]: new Token(
|
||||
SupportedChainId.OPTIMISM,
|
||||
'0x4200000000000000000000000000000000000006',
|
||||
@@ -265,7 +329,9 @@ class MaticNativeCurrency extends NativeCurrency {
|
||||
|
||||
get wrapped(): Token {
|
||||
if (!isMatic(this.chainId)) throw new Error('Not matic')
|
||||
return WRAPPED_NATIVE_CURRENCY[this.chainId]
|
||||
const wrapped = WRAPPED_NATIVE_CURRENCY[this.chainId]
|
||||
invariant(wrapped instanceof Token)
|
||||
return wrapped
|
||||
}
|
||||
|
||||
public constructor(chainId: number) {
|
||||
@@ -276,7 +342,8 @@ class MaticNativeCurrency extends NativeCurrency {
|
||||
|
||||
export class ExtendedEther extends Ether {
|
||||
public get wrapped(): Token {
|
||||
if (this.chainId in WRAPPED_NATIVE_CURRENCY) return WRAPPED_NATIVE_CURRENCY[this.chainId]
|
||||
const wrapped = WRAPPED_NATIVE_CURRENCY[this.chainId]
|
||||
if (wrapped) return wrapped
|
||||
throw new Error('Unsupported chain ID')
|
||||
}
|
||||
|
||||
@@ -296,3 +363,19 @@ export function nativeOnChain(chainId: number): NativeCurrency {
|
||||
: ExtendedEther.onChain(chainId))
|
||||
)
|
||||
}
|
||||
|
||||
export const TOKEN_SHORTHANDS: { [shorthand: string]: { [chainId in SupportedChainId]?: string } } = {
|
||||
USDC: {
|
||||
[SupportedChainId.MAINNET]: USDC_MAINNET.address,
|
||||
[SupportedChainId.ARBITRUM_ONE]: USDC_ARBITRUM.address,
|
||||
[SupportedChainId.OPTIMISM]: USDC_OPTIMISM.address,
|
||||
[SupportedChainId.ARBITRUM_RINKEBY]: USDC_ARBITRUM_RINKEBY.address,
|
||||
[SupportedChainId.OPTIMISTIC_KOVAN]: USDC_OPTIMISTIC_KOVAN.address,
|
||||
[SupportedChainId.POLYGON]: USDC_POLYGON.address,
|
||||
[SupportedChainId.POLYGON_MUMBAI]: USDC_POLYGON_MUMBAI.address,
|
||||
[SupportedChainId.GOERLI]: USDC_GOERLI.address,
|
||||
[SupportedChainId.RINKEBY]: USDC_RINKEBY.address,
|
||||
[SupportedChainId.KOVAN]: USDC_KOVAN.address,
|
||||
[SupportedChainId.ROPSTEN]: USDC_ROPSTEN.address,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Currency, Token } from '@uniswap/sdk-core'
|
||||
import { CHAIN_INFO } from 'constants/chainInfo'
|
||||
import { L2_CHAIN_IDS, SupportedChainId, SupportedL2ChainId } from 'constants/chains'
|
||||
import useActiveWeb3React from 'hooks/useActiveWeb3React'
|
||||
import { useCurrencyFromMap, useTokenFromMap } from 'lib/hooks/useCurrency'
|
||||
import { useCurrencyFromMap, useTokenFromMapOrNetwork } from 'lib/hooks/useCurrency'
|
||||
import { getTokenFilter } from 'lib/hooks/useTokenList/filtering'
|
||||
import { useMemo } from 'react'
|
||||
|
||||
@@ -159,7 +159,7 @@ export function useIsUserAddedToken(currency: Currency | undefined | null): bool
|
||||
// otherwise returns the token
|
||||
export function useToken(tokenAddress?: string | null): Token | null | undefined {
|
||||
const tokens = useAllTokens()
|
||||
return useTokenFromMap(tokens, tokenAddress)
|
||||
return useTokenFromMapOrNetwork(tokens, tokenAddress)
|
||||
}
|
||||
|
||||
export function useCurrency(currencyId?: string | null): Currency | null | undefined {
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -13,6 +13,7 @@ import useUSDCPrice, { useUSDCValue } from './useUSDCPrice'
|
||||
|
||||
const V3_SWAP_DEFAULT_SLIPPAGE = new Percent(50, 10_000) // .50%
|
||||
const ONE_TENTHS_PERCENT = new Percent(10, 10_000) // .10%
|
||||
export const DEFAULT_AUTO_SLIPPAGE = ONE_TENTHS_PERCENT
|
||||
|
||||
/**
|
||||
* Return a guess of the gas cost used in computing slippage tolerance for a given trade
|
||||
@@ -41,10 +42,10 @@ export default function useAutoSlippageTolerance(
|
||||
|
||||
const gasEstimate = guesstimateGas(trade)
|
||||
const nativeCurrency = useNativeCurrency()
|
||||
const nativeCurrencyPrice = useUSDCPrice(nativeCurrency ?? undefined)
|
||||
const nativeCurrencyPrice = useUSDCPrice((trade && nativeCurrency) ?? undefined)
|
||||
|
||||
return useMemo(() => {
|
||||
if (!trade || onL2) return ONE_TENTHS_PERCENT
|
||||
if (!trade || onL2) return DEFAULT_AUTO_SLIPPAGE
|
||||
|
||||
const nativeGasCost =
|
||||
nativeGasPrice && typeof gasEstimate === 'number'
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { renderHook } from '@testing-library/react-hooks'
|
||||
import { CurrencyAmount, TradeType } from '@uniswap/sdk-core'
|
||||
import { DAI, USDC } from 'constants/tokens'
|
||||
import { DAI, USDC_MAINNET } from 'constants/tokens'
|
||||
import { TradeState } from 'state/routing/types'
|
||||
|
||||
import { useRoutingAPITrade } from '../state/routing/useRoutingAPITrade'
|
||||
@@ -10,7 +10,7 @@ import { useClientSideV3Trade } from './useClientSideV3Trade'
|
||||
import useDebounce from './useDebounce'
|
||||
import useIsWindowVisible from './useIsWindowVisible'
|
||||
|
||||
const USDCAmount = CurrencyAmount.fromRawAmount(USDC, '10000')
|
||||
const USDCAmount = CurrencyAmount.fromRawAmount(USDC_MAINNET, '10000')
|
||||
const DAIAmount = CurrencyAmount.fromRawAmount(DAI, '10000')
|
||||
|
||||
jest.mock('./useAutoRouterSupported')
|
||||
@@ -126,10 +126,10 @@ describe('#useBestV3Trade ExactOut', () => {
|
||||
expectRouterMock(TradeState.INVALID)
|
||||
expectClientSideMock(TradeState.VALID)
|
||||
|
||||
const { result } = renderHook(() => useBestTrade(TradeType.EXACT_OUTPUT, DAIAmount, USDC))
|
||||
const { result } = renderHook(() => useBestTrade(TradeType.EXACT_OUTPUT, DAIAmount, USDC_MAINNET))
|
||||
|
||||
expect(mockUseRoutingAPITrade).toHaveBeenCalledWith(TradeType.EXACT_OUTPUT, undefined, USDC)
|
||||
expect(mockUseClientSideV3Trade).toHaveBeenCalledWith(TradeType.EXACT_OUTPUT, DAIAmount, USDC)
|
||||
expect(mockUseRoutingAPITrade).toHaveBeenCalledWith(TradeType.EXACT_OUTPUT, undefined, USDC_MAINNET)
|
||||
expect(mockUseClientSideV3Trade).toHaveBeenCalledWith(TradeType.EXACT_OUTPUT, DAIAmount, USDC_MAINNET)
|
||||
expect(result.current).toEqual({ state: TradeState.VALID, trade: undefined })
|
||||
})
|
||||
|
||||
@@ -138,17 +138,17 @@ describe('#useBestV3Trade ExactOut', () => {
|
||||
expectRouterMock(TradeState.NO_ROUTE_FOUND)
|
||||
expectClientSideMock(TradeState.VALID)
|
||||
|
||||
const { result } = renderHook(() => useBestTrade(TradeType.EXACT_OUTPUT, DAIAmount, USDC))
|
||||
const { result } = renderHook(() => useBestTrade(TradeType.EXACT_OUTPUT, DAIAmount, USDC_MAINNET))
|
||||
|
||||
expect(mockUseRoutingAPITrade).toHaveBeenCalledWith(TradeType.EXACT_OUTPUT, undefined, USDC)
|
||||
expect(mockUseClientSideV3Trade).toHaveBeenCalledWith(TradeType.EXACT_OUTPUT, DAIAmount, USDC)
|
||||
expect(mockUseRoutingAPITrade).toHaveBeenCalledWith(TradeType.EXACT_OUTPUT, undefined, USDC_MAINNET)
|
||||
expect(mockUseClientSideV3Trade).toHaveBeenCalledWith(TradeType.EXACT_OUTPUT, DAIAmount, USDC_MAINNET)
|
||||
expect(result.current).toEqual({ state: TradeState.VALID, trade: undefined })
|
||||
})
|
||||
describe('when routing api is in non-error state', () => {
|
||||
it('does not compute client side v3 trade if routing api is LOADING', () => {
|
||||
expectRouterMock(TradeState.LOADING)
|
||||
|
||||
const { result } = renderHook(() => useBestTrade(TradeType.EXACT_OUTPUT, DAIAmount, USDC))
|
||||
const { result } = renderHook(() => useBestTrade(TradeType.EXACT_OUTPUT, DAIAmount, USDC_MAINNET))
|
||||
|
||||
expect(mockUseClientSideV3Trade).toHaveBeenCalledWith(TradeType.EXACT_OUTPUT, undefined, undefined)
|
||||
expect(result.current).toEqual({ state: TradeState.LOADING, trade: undefined })
|
||||
@@ -157,7 +157,7 @@ describe('#useBestV3Trade ExactOut', () => {
|
||||
it('does not compute client side v3 trade if routing api is VALID', () => {
|
||||
expectRouterMock(TradeState.VALID)
|
||||
|
||||
const { result } = renderHook(() => useBestTrade(TradeType.EXACT_OUTPUT, DAIAmount, USDC))
|
||||
const { result } = renderHook(() => useBestTrade(TradeType.EXACT_OUTPUT, DAIAmount, USDC_MAINNET))
|
||||
|
||||
expect(mockUseClientSideV3Trade).toHaveBeenCalledWith(TradeType.EXACT_OUTPUT, undefined, undefined)
|
||||
expect(result.current).toEqual({ state: TradeState.VALID, trade: undefined })
|
||||
@@ -166,7 +166,7 @@ describe('#useBestV3Trade ExactOut', () => {
|
||||
it('does not compute client side v3 trade if routing api is SYNCING', () => {
|
||||
expectRouterMock(TradeState.SYNCING)
|
||||
|
||||
const { result } = renderHook(() => useBestTrade(TradeType.EXACT_OUTPUT, DAIAmount, USDC))
|
||||
const { result } = renderHook(() => useBestTrade(TradeType.EXACT_OUTPUT, DAIAmount, USDC_MAINNET))
|
||||
|
||||
expect(mockUseClientSideV3Trade).toHaveBeenCalledWith(TradeType.EXACT_OUTPUT, undefined, undefined)
|
||||
expect(result.current).toEqual({ state: TradeState.SYNCING, trade: undefined })
|
||||
@@ -178,7 +178,7 @@ describe('#useBestV3Trade ExactOut', () => {
|
||||
expectRouterMock(TradeState.INVALID)
|
||||
expectClientSideMock(TradeState.VALID)
|
||||
|
||||
renderHook(() => useBestTrade(TradeType.EXACT_OUTPUT, DAIAmount, USDC))
|
||||
renderHook(() => useBestTrade(TradeType.EXACT_OUTPUT, DAIAmount, USDC_MAINNET))
|
||||
|
||||
expect(mockUseClientSideV3Trade).toHaveBeenCalledWith(TradeType.EXACT_OUTPUT, undefined, undefined)
|
||||
})
|
||||
@@ -187,9 +187,9 @@ describe('#useBestV3Trade ExactOut', () => {
|
||||
expectRouterMock(TradeState.NO_ROUTE_FOUND)
|
||||
expectClientSideMock(TradeState.VALID)
|
||||
|
||||
const { result } = renderHook(() => useBestTrade(TradeType.EXACT_OUTPUT, DAIAmount, USDC))
|
||||
const { result } = renderHook(() => useBestTrade(TradeType.EXACT_OUTPUT, DAIAmount, USDC_MAINNET))
|
||||
|
||||
expect(mockUseClientSideV3Trade).toHaveBeenCalledWith(TradeType.EXACT_OUTPUT, DAIAmount, USDC)
|
||||
expect(mockUseClientSideV3Trade).toHaveBeenCalledWith(TradeType.EXACT_OUTPUT, DAIAmount, USDC_MAINNET)
|
||||
expect(result.current).toEqual({ state: TradeState.VALID, trade: undefined })
|
||||
})
|
||||
})
|
||||
|
||||
@@ -36,21 +36,8 @@ export function useBestTrade(
|
||||
debouncedOtherCurrency
|
||||
)
|
||||
|
||||
const isLoading = amountSpecified !== undefined && debouncedAmount === undefined
|
||||
|
||||
// consider trade debouncing when inputs/outputs do not match
|
||||
const debouncing =
|
||||
routingAPITrade.trade &&
|
||||
amountSpecified &&
|
||||
(tradeType === TradeType.EXACT_INPUT
|
||||
? !routingAPITrade.trade.inputAmount.equalTo(amountSpecified) ||
|
||||
!amountSpecified.currency.equals(routingAPITrade.trade.inputAmount.currency) ||
|
||||
!debouncedOtherCurrency?.equals(routingAPITrade.trade.outputAmount.currency)
|
||||
: !routingAPITrade.trade.outputAmount.equalTo(amountSpecified) ||
|
||||
!amountSpecified.currency.equals(routingAPITrade.trade.outputAmount.currency) ||
|
||||
!debouncedOtherCurrency?.equals(routingAPITrade.trade.inputAmount.currency))
|
||||
|
||||
const useFallback = !autoRouterSupported || (!debouncing && routingAPITrade.state === TradeState.NO_ROUTE_FOUND)
|
||||
const isLoading = routingAPITrade.state === TradeState.LOADING
|
||||
const useFallback = !autoRouterSupported || routingAPITrade.state === TradeState.NO_ROUTE_FOUND
|
||||
|
||||
// only use client side router if routing api trade failed or is not supported
|
||||
const bestV3Trade = useClientSideV3Trade(
|
||||
@@ -63,9 +50,8 @@ export function useBestTrade(
|
||||
return useMemo(
|
||||
() => ({
|
||||
...(useFallback ? bestV3Trade : routingAPITrade),
|
||||
...(debouncing ? { state: TradeState.SYNCING } : {}),
|
||||
...(isLoading ? { state: TradeState.LOADING } : {}),
|
||||
}),
|
||||
[bestV3Trade, debouncing, isLoading, routingAPITrade, useFallback]
|
||||
[bestV3Trade, isLoading, routingAPITrade, useFallback]
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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,9 +1,9 @@
|
||||
import { Token } from '@uniswap/sdk-core'
|
||||
import { SupportedChainId } from 'constants/chains'
|
||||
import uriToHttp from 'lib/utils/uriToHttp'
|
||||
import Vibrant from 'node-vibrant/lib/bundle'
|
||||
import Vibrant from 'node-vibrant/lib/bundle.js'
|
||||
import { shade } from 'polished'
|
||||
import { useLayoutEffect, useState } from 'react'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { WrappedTokenInfo } from 'state/lists/wrappedTokenInfo'
|
||||
import { hex } from 'wcag-contrast'
|
||||
|
||||
@@ -64,7 +64,7 @@ async function getColorFromUriPath(uri: string): Promise<string | null> {
|
||||
export function useColor(token?: Token) {
|
||||
const [color, setColor] = useState('#2172E5')
|
||||
|
||||
useLayoutEffect(() => {
|
||||
useEffect(() => {
|
||||
let stale = false
|
||||
|
||||
if (token) {
|
||||
@@ -87,7 +87,7 @@ export function useColor(token?: Token) {
|
||||
export function useListColor(listImageUri?: string) {
|
||||
const [color, setColor] = useState('#2172E5')
|
||||
|
||||
useLayoutEffect(() => {
|
||||
useEffect(() => {
|
||||
let stale = false
|
||||
|
||||
if (listImageUri) {
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
import { BigNumber } from '@ethersproject/bignumber'
|
||||
import { useSingleCallResult } from 'lib/hooks/multicall'
|
||||
import { useMemo } from 'react'
|
||||
|
||||
import { useInterfaceMulticall } from './useContract'
|
||||
|
||||
// gets the current timestamp from the blockchain
|
||||
export default function useCurrentBlockTimestamp(): BigNumber | undefined {
|
||||
const multicall = useInterfaceMulticall()
|
||||
return useSingleCallResult(multicall, 'getCurrentBlockTimestamp')?.result?.[0]
|
||||
const resultStr: string | undefined = useSingleCallResult(
|
||||
multicall,
|
||||
'getCurrentBlockTimestamp'
|
||||
)?.result?.[0]?.toString()
|
||||
return useMemo(() => (typeof resultStr === 'string' ? BigNumber.from(resultStr) : undefined), [resultStr])
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ import { useSingleCallResult } from 'lib/hooks/multicall'
|
||||
import { useMemo, useState } from 'react'
|
||||
|
||||
import { SWAP_ROUTER_ADDRESSES, V3_ROUTER_ADDRESS } from '../constants/addresses'
|
||||
import { DAI, UNI, USDC } from '../constants/tokens'
|
||||
import { DAI, UNI, USDC_MAINNET } from '../constants/tokens'
|
||||
import { useEIP2612Contract } from './useContract'
|
||||
import useIsArgentWallet from './useIsArgentWallet'
|
||||
|
||||
@@ -36,7 +36,7 @@ const PERMITTABLE_TOKENS: {
|
||||
}
|
||||
} = {
|
||||
1: {
|
||||
[USDC.address]: { type: PermitType.AMOUNT, name: 'USD Coin', version: '2' },
|
||||
[USDC_MAINNET.address]: { type: PermitType.AMOUNT, name: 'USD Coin', version: '2' },
|
||||
[DAI.address]: { type: PermitType.ALLOWED, name: 'Dai Stablecoin', version: '1' },
|
||||
[UNI[1].address]: { type: PermitType.AMOUNT, name: 'Uniswap' },
|
||||
},
|
||||
|
||||
@@ -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])
|
||||
}
|
||||
|
||||
@@ -12,13 +12,14 @@ function isWindowVisible() {
|
||||
* Returns whether the window is currently visible to the user.
|
||||
*/
|
||||
export default function useIsWindowVisible(): boolean {
|
||||
const [focused, setFocused] = useState<boolean>(isWindowVisible())
|
||||
const [focused, setFocused] = useState<boolean>(false)
|
||||
const listener = useCallback(() => {
|
||||
setFocused(isWindowVisible())
|
||||
}, [setFocused])
|
||||
|
||||
useEffect(() => {
|
||||
if (!isVisibilityStateSupported()) return undefined
|
||||
setFocused((focused) => isWindowVisible())
|
||||
|
||||
document.addEventListener('visibilitychange', listener)
|
||||
return () => {
|
||||
|
||||
@@ -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]
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import { Currency, CurrencyAmount, Price, Token, TradeType } from '@uniswap/sdk-core'
|
||||
import useActiveWeb3React from 'hooks/useActiveWeb3React'
|
||||
import tryParseCurrencyAmount from 'lib/utils/tryParseCurrencyAmount'
|
||||
import { useMemo } from 'react'
|
||||
import { useMemo, useRef } from 'react'
|
||||
|
||||
import { SupportedChainId } from '../constants/chains'
|
||||
import { DAI_OPTIMISM, USDC, USDC_ARBITRUM, USDC_POLYGON } from '../constants/tokens'
|
||||
import { DAI_OPTIMISM, USDC_ARBITRUM, USDC_MAINNET, USDC_POLYGON } from '../constants/tokens'
|
||||
import { useBestV2Trade } from './useBestV2Trade'
|
||||
import { useClientSideV3Trade } from './useClientSideV3Trade'
|
||||
|
||||
// Stablecoin amounts used when calculating spot price for a given currency.
|
||||
// The amount is large enough to filter low liquidity pairs.
|
||||
export const STABLECOIN_AMOUNT_OUT: { [chainId: number]: CurrencyAmount<Token> } = {
|
||||
[SupportedChainId.MAINNET]: CurrencyAmount.fromRawAmount(USDC, 100_000e6),
|
||||
[SupportedChainId.MAINNET]: CurrencyAmount.fromRawAmount(USDC_MAINNET, 100_000e6),
|
||||
[SupportedChainId.ARBITRUM_ONE]: CurrencyAmount.fromRawAmount(USDC_ARBITRUM, 10_000e6),
|
||||
[SupportedChainId.OPTIMISM]: CurrencyAmount.fromRawAmount(DAI_OPTIMISM, 10_000e18),
|
||||
[SupportedChainId.POLYGON]: CurrencyAmount.fromRawAmount(USDC_POLYGON, 10_000e6),
|
||||
@@ -32,8 +32,7 @@ export default function useUSDCPrice(currency?: Currency): Price<Currency, Token
|
||||
maxHops: 2,
|
||||
})
|
||||
const v3USDCTrade = useClientSideV3Trade(TradeType.EXACT_OUTPUT, amountOut, currency)
|
||||
|
||||
return useMemo(() => {
|
||||
const price = useMemo(() => {
|
||||
if (!currency || !stablecoin) {
|
||||
return undefined
|
||||
}
|
||||
@@ -54,6 +53,12 @@ export default function useUSDCPrice(currency?: Currency): Price<Currency, Token
|
||||
|
||||
return undefined
|
||||
}, [currency, stablecoin, v2USDCTrade, v3USDCTrade.trade])
|
||||
|
||||
const lastPrice = useRef(price)
|
||||
if (!price || !lastPrice.current || !price.equalTo(lastPrice.current)) {
|
||||
lastPrice.current = price
|
||||
}
|
||||
return lastPrice.current
|
||||
}
|
||||
|
||||
export function useUSDCValue(currencyAmount: CurrencyAmount<Currency> | undefined | null) {
|
||||
@@ -78,17 +83,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 {
|
||||
|
||||
@@ -23,13 +23,11 @@ export function useV3PositionFees(
|
||||
const tokenIdHexString = tokenId?.toHexString()
|
||||
const latestBlockNumber = useBlockNumber()
|
||||
|
||||
// TODO find a way to get this into multicall
|
||||
// we can't use multicall for this because we need to simulate the call from a specific address
|
||||
// latestBlockNumber is included to ensure data stays up-to-date every block
|
||||
const [amounts, setAmounts] = useState<[BigNumber, BigNumber]>()
|
||||
const [amounts, setAmounts] = useState<[BigNumber, BigNumber] | undefined>()
|
||||
useEffect(() => {
|
||||
let stale = false
|
||||
|
||||
if (positionManager && tokenIdHexString && owner && typeof latestBlockNumber === 'number') {
|
||||
if (positionManager && tokenIdHexString && owner) {
|
||||
positionManager.callStatic
|
||||
.collect(
|
||||
{
|
||||
@@ -41,19 +39,15 @@ export function useV3PositionFees(
|
||||
{ from: owner } // need to simulate the call as the owner
|
||||
)
|
||||
.then((results) => {
|
||||
if (!stale) setAmounts([results.amount0, results.amount1])
|
||||
setAmounts([results.amount0, results.amount1])
|
||||
})
|
||||
}
|
||||
|
||||
return () => {
|
||||
stale = true
|
||||
}
|
||||
}, [positionManager, tokenIdHexString, owner, latestBlockNumber])
|
||||
|
||||
if (pool && amounts) {
|
||||
return [
|
||||
CurrencyAmount.fromRawAmount(!asWETH ? unwrappedToken(pool.token0) : pool.token0, amounts[0].toString()),
|
||||
CurrencyAmount.fromRawAmount(!asWETH ? unwrappedToken(pool.token1) : pool.token1, amounts[1].toString()),
|
||||
CurrencyAmount.fromRawAmount(asWETH ? pool.token0 : unwrappedToken(pool.token0), amounts[0].toString()),
|
||||
CurrencyAmount.fromRawAmount(asWETH ? pool.token1 : unwrappedToken(pool.token1), amounts[1].toString()),
|
||||
]
|
||||
} else {
|
||||
return [undefined, undefined]
|
||||
|
||||
@@ -3,7 +3,7 @@ import 'inter-ui'
|
||||
import 'polyfills'
|
||||
import 'components/analytics'
|
||||
|
||||
import { BlockUpdater } from 'lib/hooks/useBlockNumber'
|
||||
import { BlockNumberProvider } from 'lib/hooks/useBlockNumber'
|
||||
import { MulticallUpdater } from 'lib/state/multicall'
|
||||
import { StrictMode } from 'react'
|
||||
import ReactDOM from 'react-dom'
|
||||
@@ -40,7 +40,6 @@ function Updaters() {
|
||||
<UserUpdater />
|
||||
<ApplicationUpdater />
|
||||
<TransactionUpdater />
|
||||
<BlockUpdater />
|
||||
<MulticallUpdater />
|
||||
<LogsUpdater />
|
||||
</>
|
||||
@@ -55,11 +54,13 @@ ReactDOM.render(
|
||||
<Web3ReactProvider getLibrary={getLibrary}>
|
||||
<Web3ProviderNetwork getLibrary={getLibrary}>
|
||||
<Blocklist>
|
||||
<Updaters />
|
||||
<ThemeProvider>
|
||||
<ThemedGlobalStyle />
|
||||
<App />
|
||||
</ThemeProvider>
|
||||
<BlockNumberProvider>
|
||||
<Updaters />
|
||||
<ThemeProvider>
|
||||
<ThemedGlobalStyle />
|
||||
<App />
|
||||
</ThemeProvider>
|
||||
</BlockNumberProvider>
|
||||
</Blocklist>
|
||||
</Web3ProviderNetwork>
|
||||
</Web3ReactProvider>
|
||||
@@ -73,4 +74,3 @@ ReactDOM.render(
|
||||
if (process.env.REACT_APP_SERVICE_WORKER !== 'false') {
|
||||
serviceWorkerRegistration.register()
|
||||
}
|
||||
export { INFURA_NETWORK_URLS } from 'constants/infura'
|
||||
|
||||
@@ -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": [
|
||||
|
||||
BIN
src/lib/assets/missing-token-image.png
Normal file
|
After Width: | Height: | Size: 2.5 KiB |
35
src/lib/assets/svg/inline_spinner.svg
Normal file
@@ -0,0 +1,35 @@
|
||||
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<mask id="mask">
|
||||
<circle cx="12" cy="12" r="10" fill="black" stroke="black" stroke-width="2" />
|
||||
<rect width="12" height="12" fill="white" stroke-width="0" />
|
||||
<circle cx="2" cy="12" r="1" fill="white" stroke-width="0" />
|
||||
<circle cx="12" cy="2" r="1" fill="white" stroke-width="0" />
|
||||
</mask>
|
||||
<circle
|
||||
cx="12"
|
||||
cy="12"
|
||||
r="6"
|
||||
stroke="none"
|
||||
/>
|
||||
<circle
|
||||
id="track"
|
||||
cx="12"
|
||||
cy="12"
|
||||
r="10"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
fill="none"
|
||||
/>
|
||||
<circle
|
||||
cx="12"
|
||||
cy="12"
|
||||
r="10"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
fill="none"
|
||||
mask="url(#mask)"
|
||||
/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 931 B |
@@ -1,12 +1,11 @@
|
||||
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<mask id="mask">
|
||||
<rect width="24" height="24" fill="white" stroke-width="0" />
|
||||
<circle cx="12" cy="12" r="10" stroke="white" stroke-width="2" />
|
||||
<rect width="12" height="12" fill="black" stroke-width="0" />
|
||||
<circle cx="2" cy="12" r="1" fill="white" stroke-width="0" />
|
||||
<circle cx="12" cy="2" r="1" fill="white" stroke-width="0" />
|
||||
</mask>
|
||||
<circle
|
||||
id="circle"
|
||||
cx="12"
|
||||
cy="12"
|
||||
r="10"
|
||||
|
||||
|
Before Width: | Height: | Size: 608 B After Width: | Height: | Size: 592 B |
@@ -6,9 +6,9 @@ import Button from './Button'
|
||||
import Row from './Row'
|
||||
|
||||
const StyledButton = styled(Button)`
|
||||
border-radius: ${({ theme }) => theme.borderRadius}em;
|
||||
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 +35,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,27 +59,28 @@ export const Overlay = styled(Row)<{ hasAction: boolean }>`
|
||||
export interface Action {
|
||||
message: ReactNode
|
||||
icon?: Icon
|
||||
onClick: () => void
|
||||
children: ReactNode
|
||||
onClick?: () => void
|
||||
children?: ReactNode
|
||||
}
|
||||
|
||||
export interface ActionButtonProps {
|
||||
export interface BaseProps {
|
||||
color?: Color
|
||||
disabled?: boolean
|
||||
action?: Action
|
||||
onClick: () => void
|
||||
children: ReactNode
|
||||
}
|
||||
|
||||
export type ActionButtonProps = BaseProps & Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, keyof BaseProps>
|
||||
|
||||
export default function ActionButton({ color = 'accent', disabled, action, onClick, children }: ActionButtonProps) {
|
||||
const textColor = useMemo(() => (color === 'accent' && !disabled ? 'onAccent' : 'currentColor'), [color, disabled])
|
||||
return (
|
||||
<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} />
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
import styled, { Color } from 'lib/theme'
|
||||
|
||||
import Row from './Row'
|
||||
|
||||
const Badge = styled(Row)<{ borderRadius?: number; padding?: string; color?: Color }>`
|
||||
background-color: ${({ theme, color = 'outline' }) => theme[color]};
|
||||
border-radius: ${({ borderRadius }) => `${borderRadius ?? 0.5}em`};
|
||||
padding: ${({ padding }) => padding ?? '0.25em 0.375em'};
|
||||
`
|
||||
|
||||
export default Badge
|
||||
@@ -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,5 +1,5 @@
|
||||
import { Icon } from 'lib/icons'
|
||||
import styled, { Color } from 'lib/theme'
|
||||
import styled, { Color, css } from 'lib/theme'
|
||||
import { ComponentProps, forwardRef } from 'react'
|
||||
|
||||
export const BaseButton = styled.button`
|
||||
@@ -10,21 +10,31 @@ export const BaseButton = styled.button`
|
||||
cursor: pointer;
|
||||
font-size: inherit;
|
||||
font-weight: inherit;
|
||||
height: inherit;
|
||||
line-height: inherit;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
|
||||
:enabled {
|
||||
transition: filter 0.125s linear;
|
||||
}
|
||||
|
||||
:disabled {
|
||||
cursor: initial;
|
||||
filter: saturate(0) opacity(0.4);
|
||||
}
|
||||
`
|
||||
const transitionCss = css`
|
||||
transition: background-color 0.125s linear, border-color 0.125s linear, filter 0.125s linear;
|
||||
`
|
||||
|
||||
export default styled(BaseButton)<{ color?: Color }>`
|
||||
export default styled(BaseButton)<{ color?: Color; transition?: boolean }>`
|
||||
border: 1px solid transparent;
|
||||
color: ${({ color = 'interactive', theme }) => color === 'interactive' && theme.onInteractive};
|
||||
|
||||
:enabled {
|
||||
background-color: ${({ color = 'interactive', theme }) => theme[color]};
|
||||
${({ transition = true }) => transition && transitionCss};
|
||||
}
|
||||
|
||||
:enabled:hover {
|
||||
@@ -32,9 +42,8 @@ export default styled(BaseButton)<{ color?: Color }>`
|
||||
}
|
||||
|
||||
:disabled {
|
||||
border: 1px solid ${({ theme }) => theme.outline};
|
||||
border-color: ${({ theme }) => theme.outline};
|
||||
color: ${({ theme }) => theme.secondary};
|
||||
cursor: initial;
|
||||
}
|
||||
`
|
||||
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -40,7 +40,10 @@ export function Provider({ value, children }: ProviderProps) {
|
||||
}
|
||||
}, [active])
|
||||
return (
|
||||
<div ref={ref}>
|
||||
<div
|
||||
ref={ref}
|
||||
style={{ isolation: 'isolate' }} // creates a new stacking context, preventing the dialog from intercepting non-dialog clicks
|
||||
>
|
||||
<Context.Provider value={context}>{children}</Context.Provider>
|
||||
</div>
|
||||
)
|
||||
@@ -94,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)
|
||||
@@ -105,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,17 +1,14 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import useScrollbar from 'lib/hooks/useScrollbar'
|
||||
import { AlertTriangle, Expando, Icon, Info, LargeIcon } from 'lib/icons'
|
||||
import ActionButton from 'lib/components/ActionButton'
|
||||
import Column from 'lib/components/Column'
|
||||
import Expando from 'lib/components/Expando'
|
||||
import { AlertTriangle, Icon, LargeIcon } from 'lib/icons'
|
||||
import styled, { Color, ThemedText } from 'lib/theme'
|
||||
import { ReactNode, useState } from 'react'
|
||||
|
||||
import ActionButton from '../ActionButton'
|
||||
import { IconButton } from '../Button'
|
||||
import Column from '../Column'
|
||||
import Row from '../Row'
|
||||
import Rule from '../Rule'
|
||||
import { ReactNode, useCallback, useState } from 'react'
|
||||
|
||||
const HeaderIcon = styled(LargeIcon)`
|
||||
flex-grow: 1;
|
||||
transition: height 0.25s, width 0.25s;
|
||||
|
||||
svg {
|
||||
transition: height 0.25s, width 0.25s;
|
||||
@@ -34,7 +31,6 @@ export function StatusHeader({ icon: Icon, iconColor, iconSize = 4, children }:
|
||||
{children}
|
||||
</Column>
|
||||
</Column>
|
||||
<Rule />
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -48,40 +44,6 @@ const ErrorHeader = styled(Column)<{ open: boolean }>`
|
||||
transition: max-height 0.25s;
|
||||
}
|
||||
`
|
||||
const ErrorColumn = styled(Column)``
|
||||
const ExpandoColumn = styled(Column)<{ open: boolean }>`
|
||||
flex-grow: ${({ open }) => (open ? 2 : 0)};
|
||||
transition: flex-grow 0.25s, gap 0.25s;
|
||||
|
||||
${Rule} {
|
||||
margin-bottom: ${({ open }) => (open ? 0 : 0.75)}em;
|
||||
transition: margin-bottom 0.25s;
|
||||
}
|
||||
|
||||
${ErrorColumn} {
|
||||
flex-basis: 0;
|
||||
flex-grow: ${({ open }) => (open ? 1 : 0)};
|
||||
overflow-y: hidden;
|
||||
position: relative;
|
||||
transition: flex-grow 0.25s;
|
||||
|
||||
${Column} {
|
||||
height: 100%;
|
||||
padding: ${({ open }) => (open ? '0.5em 0' : 0)};
|
||||
transition: padding 0.25s;
|
||||
|
||||
:after {
|
||||
background: linear-gradient(#ffffff00, ${({ theme }) => theme.dialog});
|
||||
bottom: 0;
|
||||
content: '';
|
||||
height: 0.75em;
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
width: calc(100% - 1em);
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
interface ErrorDialogProps {
|
||||
header?: ReactNode
|
||||
@@ -92,8 +54,8 @@ interface ErrorDialogProps {
|
||||
|
||||
export default function ErrorDialog({ header, error, action, onClick }: ErrorDialogProps) {
|
||||
const [open, setOpen] = useState(false)
|
||||
const [details, setDetails] = useState<HTMLDivElement | null>(null)
|
||||
const scrollbar = useScrollbar(details)
|
||||
const onExpand = useCallback(() => setOpen((open) => !open), [])
|
||||
|
||||
return (
|
||||
<Column flex padded gap={0.75} align="stretch" style={{ height: '100%' }}>
|
||||
<StatusHeader icon={AlertTriangle} iconColor="error" iconSize={open ? 3 : 4}>
|
||||
@@ -104,27 +66,15 @@ export default function ErrorDialog({ header, error, action, onClick }: ErrorDia
|
||||
<ThemedText.Body2>{header}</ThemedText.Body2>
|
||||
</ErrorHeader>
|
||||
</StatusHeader>
|
||||
<Row>
|
||||
<Row gap={0.5}>
|
||||
<Info color="secondary" />
|
||||
<ThemedText.Subhead2 color="secondary">
|
||||
<Trans>Error details</Trans>
|
||||
</ThemedText.Subhead2>
|
||||
</Row>
|
||||
<IconButton color="secondary" onClick={() => setOpen(!open)} icon={Expando} iconProps={{ open }} />
|
||||
</Row>
|
||||
<ExpandoColumn flex align="stretch" open={open}>
|
||||
<Rule />
|
||||
<ErrorColumn>
|
||||
<Column gap={0.5} ref={setDetails} css={scrollbar}>
|
||||
<ThemedText.Code>
|
||||
{error.name}
|
||||
{error.message ? `: ${error.message}` : ''}
|
||||
</ThemedText.Code>
|
||||
</Column>
|
||||
</ErrorColumn>
|
||||
<Column gap={open ? 0 : 0.75} style={{ transition: 'gap 0.25s' }}>
|
||||
<Expando title={<Trans>Error details</Trans>} open={open} onExpand={onExpand} height={7.5}>
|
||||
<ThemedText.Code userSelect>
|
||||
{error.name}
|
||||
{error.message ? `: ${error.message}` : ''}
|
||||
</ThemedText.Code>
|
||||
</Expando>
|
||||
<ActionButton onClick={onClick}>{action}</ActionButton>
|
||||
</ExpandoColumn>
|
||||
</Column>
|
||||
</Column>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
import { SUPPORTED_LOCALES } from 'constants/locales'
|
||||
import { WidgetProps } from 'lib/components/Widget'
|
||||
import { IntegrationError } from 'lib/errors'
|
||||
import { PropsWithChildren, useEffect } from 'react'
|
||||
|
||||
export default function WidgetsPropsValidator(props: PropsWithChildren<WidgetProps>) {
|
||||
const { jsonRpcEndpoint, provider } = props
|
||||
|
||||
useEffect(() => {
|
||||
if (!provider && !jsonRpcEndpoint) {
|
||||
throw new IntegrationError('This widget requires a provider or jsonRpcEndpoint.')
|
||||
}
|
||||
}, [provider, jsonRpcEndpoint])
|
||||
|
||||
const { width } = props
|
||||
useEffect(() => {
|
||||
if (width && width < 300) {
|
||||
throw new IntegrationError(`Set widget width to at least 300px. (You set it to ${width}.)`)
|
||||
}
|
||||
}, [width])
|
||||
|
||||
const { locale } = props
|
||||
useEffect(() => {
|
||||
if (locale && locale !== 'pseudo' && !SUPPORTED_LOCALES.includes(locale)) {
|
||||
console.warn('Unsupported locale: ', locale)
|
||||
}
|
||||
}, [locale])
|
||||
|
||||
return <>{props.children}</>
|
||||
}
|
||||
@@ -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
@@ -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,7 @@
|
||||
import JSBI from 'jsbi'
|
||||
import { loadingOpacity } from 'lib/css/loading'
|
||||
import styled, { css } from 'lib/theme'
|
||||
import { forwardRef, HTMLProps, useCallback, useEffect, useState } from 'react'
|
||||
import { transparentize } from 'polished'
|
||||
import { ChangeEvent, forwardRef, HTMLProps, useCallback } from 'react'
|
||||
|
||||
const Input = styled.input`
|
||||
-webkit-appearance: textfield;
|
||||
@@ -35,6 +36,16 @@ const Input = styled.input`
|
||||
::placeholder {
|
||||
color: ${({ theme }) => theme.secondary};
|
||||
}
|
||||
|
||||
:enabled {
|
||||
transition: color 0.125s linear;
|
||||
}
|
||||
|
||||
:disabled {
|
||||
// Overrides WebKit's override of input:disabled color.
|
||||
-webkit-text-fill-color: ${({ theme }) => transparentize(1 - loadingOpacity, theme.primary)};
|
||||
color: ${({ theme }) => transparentize(1 - loadingOpacity, theme.primary)};
|
||||
}
|
||||
`
|
||||
|
||||
export default Input
|
||||
@@ -77,42 +88,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"
|
||||
|
||||
@@ -14,14 +14,16 @@ const PopoverContainer = styled.div<{ show: boolean }>`
|
||||
border: 1px solid ${({ theme }) => theme.outline};
|
||||
border-radius: 0.5em;
|
||||
opacity: ${(props) => (props.show ? 1 : 0)};
|
||||
padding: 8px;
|
||||
padding: 10px 12px;
|
||||
transition: visibility 0.25s linear, opacity 0.25s linear;
|
||||
visibility: ${(props) => (props.show ? 'visible' : 'hidden')};
|
||||
z-index: ${Layer.TOOLTIP};
|
||||
`
|
||||
|
||||
const Reference = styled.div`
|
||||
align-self: flex-start;
|
||||
display: inline-block;
|
||||
height: 1em;
|
||||
`
|
||||
|
||||
const Arrow = styled.div`
|
||||
|
||||
@@ -6,6 +6,10 @@ const Rule = styled.hr<{ padded?: true; scrollingEdge?: 'top' | 'bottom' }>`
|
||||
margin: 0 ${({ padded }) => (padded ? '0.75em' : 0)};
|
||||
margin-bottom: ${({ scrollingEdge }) => (scrollingEdge === 'bottom' ? -1 : 0)}px;
|
||||
margin-top: ${({ scrollingEdge }) => (scrollingEdge !== 'bottom' ? -1 : 0)}px;
|
||||
|
||||
// Integrators will commonly modify hr width - this overrides any modifications within the widget.
|
||||
max-width: auto;
|
||||
width: auto;
|
||||
`
|
||||
|
||||
export default Rule
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useLingui } from '@lingui/react'
|
||||
import { useUSDCValue } from 'hooks/useUSDCPrice'
|
||||
import { loadingOpacityCss } from 'lib/css/loading'
|
||||
import { Currency, CurrencyAmount } from '@uniswap/sdk-core'
|
||||
import { loadingTransitionCss } from 'lib/css/loading'
|
||||
import {
|
||||
useIsSwapFieldIndependent,
|
||||
useSwapAmount,
|
||||
@@ -21,8 +21,8 @@ import Row from '../Row'
|
||||
import TokenImg from '../TokenImg'
|
||||
import TokenInput from './TokenInput'
|
||||
|
||||
export const LoadingRow = styled(Row)<{ $loading: boolean }>`
|
||||
${loadingOpacityCss};
|
||||
export const USDC = styled(Row)`
|
||||
${loadingTransitionCss};
|
||||
`
|
||||
|
||||
export const Balance = styled(ThemedText.Body2)<{ focused: boolean }>`
|
||||
@@ -45,23 +45,42 @@ export interface InputProps {
|
||||
focused: boolean
|
||||
}
|
||||
|
||||
interface UseFormattedFieldAmountArguments {
|
||||
disabled: boolean
|
||||
currencyAmount?: CurrencyAmount<Currency>
|
||||
fieldAmount?: string
|
||||
}
|
||||
|
||||
export function useFormattedFieldAmount({ disabled, currencyAmount, fieldAmount }: UseFormattedFieldAmountArguments) {
|
||||
return useMemo(() => {
|
||||
if (disabled) {
|
||||
return ''
|
||||
}
|
||||
if (fieldAmount !== undefined) {
|
||||
return fieldAmount
|
||||
}
|
||||
if (currencyAmount) {
|
||||
return currencyAmount.toSignificant(6)
|
||||
}
|
||||
return ''
|
||||
}, [disabled, currencyAmount, fieldAmount])
|
||||
}
|
||||
|
||||
export default function Input({ disabled, focused }: InputProps) {
|
||||
const { i18n } = useLingui()
|
||||
const {
|
||||
[Field.INPUT]: { balance, amount: tradeCurrencyAmount, usdc },
|
||||
trade: { state: tradeState },
|
||||
currencyBalances: { [Field.INPUT]: balance },
|
||||
currencyAmounts: { [Field.INPUT]: swapInputCurrencyAmount },
|
||||
} = useSwapInfo()
|
||||
const inputUSDC = useUSDCValue(swapInputCurrencyAmount)
|
||||
|
||||
const [swapInputAmount, updateSwapInputAmount] = useSwapAmount(Field.INPUT)
|
||||
const [swapInputCurrency, updateSwapInputCurrency] = useSwapCurrency(Field.INPUT)
|
||||
const [inputAmount, updateInputAmount] = useSwapAmount(Field.INPUT)
|
||||
const [inputCurrency, updateInputCurrency] = useSwapCurrency(Field.INPUT)
|
||||
const inputCurrencyAmount = useSwapCurrencyAmount(Field.INPUT)
|
||||
|
||||
// extract eagerly in case of reversal
|
||||
usePrefetchCurrencyColor(swapInputCurrency)
|
||||
usePrefetchCurrencyColor(inputCurrency)
|
||||
|
||||
const isRouteLoading = tradeState === TradeState.SYNCING || tradeState === TradeState.LOADING
|
||||
const isRouteLoading = disabled || tradeState === TradeState.SYNCING || tradeState === TradeState.LOADING
|
||||
const isDependentField = !useIsSwapFieldIndependent(Field.INPUT)
|
||||
const isLoading = isRouteLoading && isDependentField
|
||||
|
||||
@@ -77,27 +96,33 @@ 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: tradeCurrencyAmount,
|
||||
fieldAmount: inputAmount,
|
||||
})
|
||||
|
||||
return (
|
||||
<InputColumn gap={0.5} approved={mockApproved}>
|
||||
<TokenInput
|
||||
currency={swapInputCurrency}
|
||||
amount={(swapInputAmount !== undefined ? swapInputAmount : swapInputCurrencyAmount?.toSignificant(6)) ?? ''}
|
||||
currency={inputCurrency}
|
||||
amount={amount}
|
||||
max={max}
|
||||
disabled={disabled}
|
||||
onChangeInput={updateSwapInputAmount}
|
||||
onChangeCurrency={updateSwapInputCurrency}
|
||||
onChangeInput={updateInputAmount}
|
||||
onChangeCurrency={updateInputCurrency}
|
||||
loading={isLoading}
|
||||
>
|
||||
<ThemedText.Body2 color="secondary">
|
||||
<ThemedText.Body2 color="secondary" userSelect>
|
||||
<Row>
|
||||
<LoadingRow $loading={isLoading}>{inputUSDC ? `$${inputUSDC.toFixed(2)}` : '-'}</LoadingRow>
|
||||
<USDC isLoading={isRouteLoading}>{usdc ? `$${formatCurrencyAmount(usdc, 6, 'en', 2)}` : '-'}</USDC>
|
||||
{balance && (
|
||||
<Balance color={balanceColor} focused={focused}>
|
||||
Balance: <span style={{ userSelect: 'text' }}>{formatCurrencyAmount(balance, 4, i18n.locale)}</span>
|
||||
Balance: <span>{formatCurrencyAmount(balance, 4, i18n.locale)}</span>
|
||||
</Balance>
|
||||
)}
|
||||
</Row>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import { useLingui } from '@lingui/react'
|
||||
import { useUSDCValue } from 'hooks/useUSDCPrice'
|
||||
import { atom } from 'jotai'
|
||||
import { useAtomValue } from 'jotai/utils'
|
||||
import BrandedFooter from 'lib/components/BrandedFooter'
|
||||
@@ -8,15 +7,13 @@ import { useIsSwapFieldIndependent, useSwapAmount, useSwapCurrency, useSwapInfo
|
||||
import useCurrencyColor from 'lib/hooks/useCurrencyColor'
|
||||
import { Field } from 'lib/state/swap'
|
||||
import styled, { DynamicThemeProvider, ThemedText } from 'lib/theme'
|
||||
import { PropsWithChildren, useMemo } from 'react'
|
||||
import { PropsWithChildren } from 'react'
|
||||
import { TradeState } from 'state/routing/types'
|
||||
import { computeFiatValuePriceImpact } from 'utils/computeFiatValuePriceImpact'
|
||||
import { formatCurrencyAmount } from 'utils/formatCurrencyAmount'
|
||||
import { getPriceImpactWarning } from 'utils/prices'
|
||||
|
||||
import Column from '../Column'
|
||||
import Row from '../Row'
|
||||
import { Balance, InputProps, LoadingRow } from './Input'
|
||||
import { Balance, InputProps, USDC, useFormattedFieldAmount } from './Input'
|
||||
import TokenInput from './TokenInput'
|
||||
|
||||
export const colorAtom = atom<string | undefined>(undefined)
|
||||
@@ -41,15 +38,15 @@ export default function Output({ disabled, focused, children }: PropsWithChildre
|
||||
const { i18n } = useLingui()
|
||||
|
||||
const {
|
||||
[Field.OUTPUT]: { balance, amount: outputCurrencyAmount, usdc: outputUSDC },
|
||||
trade: { state: tradeState },
|
||||
currencyBalances: { [Field.OUTPUT]: balance },
|
||||
currencyAmounts: { [Field.INPUT]: inputCurrencyAmount, [Field.OUTPUT]: outputCurrencyAmount },
|
||||
impact,
|
||||
} = useSwapInfo()
|
||||
|
||||
const [swapOutputAmount, updateSwapOutputAmount] = useSwapAmount(Field.OUTPUT)
|
||||
const [swapOutputCurrency, updateSwapOutputCurrency] = useSwapCurrency(Field.OUTPUT)
|
||||
|
||||
const isRouteLoading = tradeState === TradeState.SYNCING || tradeState === TradeState.LOADING
|
||||
const isRouteLoading = disabled || tradeState === TradeState.SYNCING || tradeState === TradeState.LOADING
|
||||
const isDependentField = !useIsSwapFieldIndependent(Field.OUTPUT)
|
||||
const isLoading = isRouteLoading && isDependentField
|
||||
|
||||
@@ -60,23 +57,11 @@ export default function Output({ disabled, focused, children }: PropsWithChildre
|
||||
// different state true/null/false allow smoother color transition
|
||||
const hasColor = swapOutputCurrency ? Boolean(color) || null : false
|
||||
|
||||
const inputUSDC = useUSDCValue(inputCurrencyAmount)
|
||||
const outputUSDC = useUSDCValue(outputCurrencyAmount)
|
||||
|
||||
const priceImpact = useMemo(() => {
|
||||
const fiatValuePriceImpact = computeFiatValuePriceImpact(inputUSDC, outputUSDC)
|
||||
if (!fiatValuePriceImpact) return null
|
||||
|
||||
const color = getPriceImpactWarning(fiatValuePriceImpact)
|
||||
const sign = fiatValuePriceImpact.lessThan(0) ? '+' : ''
|
||||
const displayedPriceImpact = parseFloat(fiatValuePriceImpact.multiply(-1)?.toSignificant(3))
|
||||
return (
|
||||
<ThemedText.Body2 color={color}>
|
||||
({sign}
|
||||
{displayedPriceImpact}%)
|
||||
</ThemedText.Body2>
|
||||
)
|
||||
}, [inputUSDC, outputUSDC])
|
||||
const amount = useFormattedFieldAmount({
|
||||
disabled,
|
||||
currencyAmount: outputCurrencyAmount,
|
||||
fieldAmount: swapOutputAmount,
|
||||
})
|
||||
|
||||
return (
|
||||
<DynamicThemeProvider color={color}>
|
||||
@@ -88,20 +73,21 @@ export default function Output({ disabled, focused, children }: PropsWithChildre
|
||||
</Row>
|
||||
<TokenInput
|
||||
currency={swapOutputCurrency}
|
||||
amount={(swapOutputAmount !== undefined ? swapOutputAmount : outputCurrencyAmount?.toSignificant(6)) ?? ''}
|
||||
amount={amount}
|
||||
disabled={disabled}
|
||||
onChangeInput={updateSwapOutputAmount}
|
||||
onChangeCurrency={updateSwapOutputCurrency}
|
||||
loading={isLoading}
|
||||
>
|
||||
<ThemedText.Body2 color="secondary">
|
||||
<ThemedText.Body2 color="secondary" userSelect>
|
||||
<Row>
|
||||
<LoadingRow gap={0.5} $loading={isLoading}>
|
||||
{outputUSDC ? `$${outputUSDC.toFixed(2)}` : '-'} {priceImpact}
|
||||
</LoadingRow>
|
||||
<USDC gap={0.5} isLoading={isRouteLoading}>
|
||||
{outputUSDC ? `$${formatCurrencyAmount(outputUSDC, 6, 'en', 2)}` : '-'}{' '}
|
||||
{impact && <ThemedText.Body2 color={impact.warning}>({impact.toString()})</ThemedText.Body2>}
|
||||
</USDC>
|
||||
{balance && (
|
||||
<Balance focused={focused}>
|
||||
Balance: <span style={{ userSelect: 'text' }}>{formatCurrencyAmount(balance, 4, i18n.locale)}</span>
|
||||
Balance: <span>{formatCurrencyAmount(balance, 4, i18n.locale)}</span>
|
||||
</Balance>
|
||||
)}
|
||||
</Row>
|
||||
|
||||
54
src/lib/components/Swap/Price.tsx
Normal file
@@ -0,0 +1,54 @@
|
||||
import { useLingui } from '@lingui/react'
|
||||
import { Trade } from '@uniswap/router-sdk'
|
||||
import { Currency, CurrencyAmount, TradeType } from '@uniswap/sdk-core'
|
||||
import Row from 'lib/components/Row'
|
||||
import { ThemedText } from 'lib/theme'
|
||||
import formatLocaleNumber from 'lib/utils/formatLocaleNumber'
|
||||
import { useCallback, useMemo, useState } from 'react'
|
||||
import { formatCurrencyAmount, formatPrice } from 'utils/formatCurrencyAmount'
|
||||
|
||||
import { TextButton } from '../Button'
|
||||
|
||||
interface PriceProps {
|
||||
trade: Trade<Currency, Currency, TradeType>
|
||||
outputUSDC?: CurrencyAmount<Currency>
|
||||
}
|
||||
|
||||
/** Displays the price of a trade. If outputUSDC is included, also displays the unit price. */
|
||||
export default function Price({ trade, outputUSDC }: PriceProps) {
|
||||
const { i18n } = useLingui()
|
||||
const { inputAmount, outputAmount, executionPrice } = trade
|
||||
|
||||
const [base, setBase] = useState<'input' | 'output'>('input')
|
||||
const onClick = useCallback(() => setBase((base) => (base === 'input' ? 'output' : 'input')), [])
|
||||
|
||||
// Compute the usdc price from the output price, so that it aligns with the displayed price.
|
||||
const { price, usdcPrice } = useMemo(() => {
|
||||
switch (base) {
|
||||
case 'input':
|
||||
return {
|
||||
price: executionPrice,
|
||||
usdcPrice: outputUSDC?.multiply(inputAmount.decimalScale).divide(inputAmount),
|
||||
}
|
||||
case 'output':
|
||||
return {
|
||||
price: executionPrice.invert(),
|
||||
usdcPrice: outputUSDC?.multiply(outputAmount.decimalScale).divide(outputAmount),
|
||||
}
|
||||
}
|
||||
}, [base, executionPrice, inputAmount, outputAmount, outputUSDC])
|
||||
|
||||
return (
|
||||
<TextButton color="primary" onClick={onClick}>
|
||||
<ThemedText.Caption>
|
||||
<Row gap={0.25}>
|
||||
{formatLocaleNumber({ number: 1, sigFigs: 1, locale: i18n.locale })} {price.baseCurrency.symbol} ={' '}
|
||||
{formatPrice(price, 6, i18n.locale)} {price.quoteCurrency.symbol}
|
||||
{usdcPrice && (
|
||||
<ThemedText.Caption color="secondary">(${formatCurrencyAmount(usdcPrice, 6, 'en', 2)})</ThemedText.Caption>
|
||||
)}
|
||||
</Row>
|
||||
</ThemedText.Caption>
|
||||
</TextButton>
|
||||
)
|
||||
}
|
||||
@@ -33,6 +33,7 @@ const Overlay = styled.div`
|
||||
|
||||
const StyledReverseButton = styled(Button)<{ turns: number }>`
|
||||
border-radius: ${({ theme }) => theme.borderRadius * 0.75}em;
|
||||
color: ${({ theme }) => theme.primary};
|
||||
height: 2.5em;
|
||||
position: relative;
|
||||
width: 2.5em;
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { Plural, Trans } from '@lingui/macro'
|
||||
import { Currency, TradeType } from '@uniswap/sdk-core'
|
||||
import { FeeAmount } from '@uniswap/v3-sdk'
|
||||
import { ReactComponent as DotLineImage } from 'assets/svg/dot_line.svg'
|
||||
import Badge from 'lib/components/Badge'
|
||||
import { ReactComponent as DotLine } from 'assets/svg/dot_line.svg'
|
||||
import Column from 'lib/components/Column'
|
||||
import Row from 'lib/components/Row'
|
||||
import Rule from 'lib/components/Rule'
|
||||
@@ -14,47 +13,6 @@ import { InterfaceTrade } from 'state/routing/types'
|
||||
|
||||
import { getTokenPath, RoutingDiagramEntry } from './utils'
|
||||
|
||||
const Wrapper = styled(Column)`
|
||||
padding: 0.25em;
|
||||
`
|
||||
|
||||
const RouteRow = styled(Row)`
|
||||
grid-template-columns: 1em 1.15em 1fr 1em;
|
||||
min-width: 430px;
|
||||
`
|
||||
|
||||
const RouteDetailsContainer = styled(Row)`
|
||||
padding: 0.1em 0.5em;
|
||||
position: relative;
|
||||
`
|
||||
|
||||
const DotsContainer = styled.div`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
opacity: 0.5;
|
||||
position: absolute;
|
||||
width: calc(100% - 1em);
|
||||
z-index: ${Layer.UNDERLAYER};
|
||||
`
|
||||
|
||||
const DotsContainerShort = styled(DotsContainer)`
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
width: 71px;
|
||||
`
|
||||
|
||||
const Dots = styled(DotLineImage)`
|
||||
path {
|
||||
stroke: ${({ theme }) => theme.secondary};
|
||||
}
|
||||
`
|
||||
|
||||
const DetailsRow = styled(Row)`
|
||||
display: grid;
|
||||
grid-template-columns: 4.8125em 1fr;
|
||||
width: 100%;
|
||||
`
|
||||
|
||||
const StyledAutoRouterLabel = styled(ThemedText.ButtonSmall)`
|
||||
@supports (-webkit-background-clip: text) and (-webkit-text-fill-color: transparent) {
|
||||
background-image: linear-gradient(90deg, #2172e5 0%, #54e521 163.16%);
|
||||
@@ -63,6 +21,66 @@ const StyledAutoRouterLabel = styled(ThemedText.ButtonSmall)`
|
||||
}
|
||||
`
|
||||
|
||||
function Header({ routes }: { routes: RoutingDiagramEntry[] }) {
|
||||
return (
|
||||
<Row justify="space-between" gap={1}>
|
||||
<ThemedText.Subhead2>
|
||||
<Row gap={0.25}>
|
||||
<AutoRouter />
|
||||
<StyledAutoRouterLabel color="primary" lineHeight={'16px'}>
|
||||
<Trans>Auto Router</Trans>
|
||||
</StyledAutoRouterLabel>
|
||||
</Row>
|
||||
</ThemedText.Subhead2>
|
||||
<ThemedText.Body2>
|
||||
<Plural value={routes.length} _1="Best route via 1 hop" other="Best route via # hops" />
|
||||
</ThemedText.Body2>
|
||||
</Row>
|
||||
)
|
||||
}
|
||||
|
||||
const Dots = styled(DotLine)`
|
||||
color: ${({ theme }) => theme.outline};
|
||||
position: absolute;
|
||||
z-index: ${Layer.UNDERLAYER};
|
||||
`
|
||||
|
||||
const RouteRow = styled(Row)`
|
||||
flex-wrap: nowrap;
|
||||
`
|
||||
|
||||
const RouteNode = styled(Row)`
|
||||
background-color: ${({ theme }) => theme.interactive};
|
||||
border-radius: ${({ theme }) => `${(theme.borderRadius ?? 1) * 0.5}em`};
|
||||
margin-left: 1.625em;
|
||||
padding: 0.25em 0.375em;
|
||||
width: max-content;
|
||||
`
|
||||
|
||||
const RouteBadge = styled.div`
|
||||
background-color: ${({ theme }) => theme.module};
|
||||
border-radius: ${({ theme }) => `${(theme.borderRadius ?? 1) * 0.25}em`};
|
||||
padding: 0.125em;
|
||||
`
|
||||
|
||||
function RouteDetail({ route }: { route: RoutingDiagramEntry }) {
|
||||
const protocol = route.protocol.toUpperCase()
|
||||
return (
|
||||
<RouteNode>
|
||||
<Row gap={0.375}>
|
||||
<ThemedText.Caption>{route.percent.toSignificant(2)}%</ThemedText.Caption>
|
||||
<RouteBadge>
|
||||
<ThemedText.Badge color="secondary">{protocol}</ThemedText.Badge>
|
||||
</RouteBadge>
|
||||
</Row>
|
||||
</RouteNode>
|
||||
)
|
||||
}
|
||||
|
||||
const RoutePool = styled(RouteNode)`
|
||||
margin: 0 0.75em;
|
||||
`
|
||||
|
||||
function Pool({
|
||||
originCurrency,
|
||||
targetCurrency,
|
||||
@@ -73,15 +91,36 @@ function Pool({
|
||||
feeAmount: FeeAmount
|
||||
}) {
|
||||
return (
|
||||
<Badge padding="0 4px" color="dialog">
|
||||
<Badge gap={0.375}>
|
||||
<Row>
|
||||
<RoutePool>
|
||||
<ThemedText.Caption>
|
||||
<Row gap={0.25}>
|
||||
<TokenImg token={originCurrency} />
|
||||
<TokenImg token={targetCurrency} />
|
||||
<TokenImg token={targetCurrency} style={{ marginLeft: '-0.65em' }} />
|
||||
{feeAmount / 10_000}%
|
||||
</Row>
|
||||
<ThemedText.Subhead2>{feeAmount / 10_000}%</ThemedText.Subhead2>
|
||||
</Badge>
|
||||
</Badge>
|
||||
</ThemedText.Caption>
|
||||
</RoutePool>
|
||||
)
|
||||
}
|
||||
|
||||
function Route({ route }: { route: RoutingDiagramEntry }) {
|
||||
const [originCurrency] = route.path[0]
|
||||
const [, targetCurrency] = route.path[route.path.length - 1]
|
||||
|
||||
return (
|
||||
<Row align="center" style={{ gridTemplateColumns: '1em 1fr 1em' }}>
|
||||
<TokenImg token={originCurrency} />
|
||||
<RouteRow flex style={{ position: 'relative' }}>
|
||||
<Dots />
|
||||
<RouteDetail route={route} />
|
||||
<RouteRow justify="space-evenly" flex>
|
||||
{route.path.map(([originCurrency, targetCurrency, feeAmount], index) => (
|
||||
<Pool key={index} originCurrency={originCurrency} targetCurrency={targetCurrency} feeAmount={feeAmount} />
|
||||
))}
|
||||
</RouteRow>
|
||||
</RouteRow>
|
||||
<TokenImg token={targetCurrency} />
|
||||
</Row>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -89,55 +128,12 @@ export default function RoutingDiagram({ trade }: { trade: InterfaceTrade<Curren
|
||||
const routes: RoutingDiagramEntry[] = useMemo(() => getTokenPath(trade), [trade])
|
||||
|
||||
return (
|
||||
<Wrapper gap={0.75}>
|
||||
<Row justify="space-between">
|
||||
<Row gap={0.25}>
|
||||
<AutoRouter />
|
||||
<StyledAutoRouterLabel color="primary" lineHeight={'16px'}>
|
||||
<Trans>Auto Router</Trans>
|
||||
</StyledAutoRouterLabel>
|
||||
</Row>
|
||||
<ThemedText.ButtonSmall>
|
||||
<Plural value={routes.length} _1="Best route via 1 hop" other="Best route via # hops" />
|
||||
</ThemedText.ButtonSmall>
|
||||
</Row>
|
||||
<Column gap={0.75}>
|
||||
<Header routes={routes} />
|
||||
<Rule />
|
||||
{routes.map((route, index) => (
|
||||
<RouteRow key={index} align="center">
|
||||
<TokenImg token={trade.inputAmount.currency} />
|
||||
<DotsContainerShort>
|
||||
<Dots />
|
||||
</DotsContainerShort>
|
||||
<RouteDetailsContainer justify="flex-start" flex>
|
||||
<DotsContainer>
|
||||
<Dots />
|
||||
</DotsContainer>
|
||||
<DetailsRow>
|
||||
<Badge padding="0 4px" color="dialog">
|
||||
<Badge gap={0.375}>
|
||||
<ThemedText.ButtonSmall color="secondary">{route.percent.toSignificant(2)}%</ThemedText.ButtonSmall>
|
||||
<Badge padding="0.125em" borderRadius={0.25} color="module">
|
||||
<ThemedText.Badge color="secondary" fontSize={'0.5rem'}>
|
||||
{route.protocol.toUpperCase()}
|
||||
</ThemedText.Badge>
|
||||
</Badge>
|
||||
</Badge>
|
||||
</Badge>
|
||||
<Row justify="space-evenly" flex style={{ width: '100%' }}>
|
||||
{route.path.map(([originCurrency, targetCurrency, feeAmount], index) => (
|
||||
<Pool
|
||||
key={index}
|
||||
originCurrency={originCurrency}
|
||||
targetCurrency={targetCurrency}
|
||||
feeAmount={feeAmount}
|
||||
/>
|
||||
))}
|
||||
</Row>
|
||||
</DetailsRow>
|
||||
</RouteDetailsContainer>
|
||||
<TokenImg token={trade.outputAmount.currency} />
|
||||
</RouteRow>
|
||||
<Route key={index} route={route} />
|
||||
))}
|
||||
</Wrapper>
|
||||
</Column>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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,9 +1,11 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import ErrorDialog, { StatusHeader } from 'lib/components/Error/ErrorDialog'
|
||||
import EtherscanLink from 'lib/components/EtherscanLink'
|
||||
import Rule from 'lib/components/Rule'
|
||||
import SwapSummary from 'lib/components/Swap/Summary'
|
||||
import useInterval from 'lib/hooks/useInterval'
|
||||
import { CheckCircle, Clock, Spinner } from 'lib/icons'
|
||||
import { SwapTransactionInfo, Transaction } from 'lib/state/transactions'
|
||||
import { SwapTransactionInfo, Transaction, TransactionType, WrapTransactionInfo } from 'lib/state/transactions'
|
||||
import styled, { ThemedText } from 'lib/theme'
|
||||
import ms from 'ms.macro'
|
||||
import { useCallback, useMemo, useState } from 'react'
|
||||
@@ -12,7 +14,6 @@ import { ExplorerDataType } from 'utils/getExplorerLink'
|
||||
import ActionButton from '../../ActionButton'
|
||||
import Column from '../../Column'
|
||||
import Row from '../../Row'
|
||||
import Summary from '../Summary'
|
||||
|
||||
const errorMessage = (
|
||||
<Trans>
|
||||
@@ -26,7 +27,9 @@ const TransactionRow = styled(Row)`
|
||||
flex-direction: row-reverse;
|
||||
`
|
||||
|
||||
function ElapsedTime({ tx }: { tx: Transaction<SwapTransactionInfo> }) {
|
||||
type PendingTransaction = Transaction<SwapTransactionInfo | WrapTransactionInfo>
|
||||
|
||||
function ElapsedTime({ tx }: { tx: PendingTransaction }) {
|
||||
const [elapsedMs, setElapsedMs] = useState(0)
|
||||
|
||||
useInterval(() => setElapsedMs(Date.now() - tx.addedTime), tx.receipt ? null : ms`1s`)
|
||||
@@ -54,7 +57,7 @@ function ElapsedTime({ tx }: { tx: Transaction<SwapTransactionInfo> }) {
|
||||
}
|
||||
|
||||
interface TransactionStatusProps {
|
||||
tx: Transaction<SwapTransactionInfo>
|
||||
tx: PendingTransaction
|
||||
onClose: () => void
|
||||
}
|
||||
|
||||
@@ -63,15 +66,26 @@ function TransactionStatus({ tx, onClose }: TransactionStatusProps) {
|
||||
return tx.receipt?.status ? CheckCircle : Spinner
|
||||
}, [tx.receipt?.status])
|
||||
const heading = useMemo(() => {
|
||||
return tx.receipt?.status ? <Trans>Transaction submitted</Trans> : <Trans>Transaction pending</Trans>
|
||||
}, [tx.receipt?.status])
|
||||
if (tx.info.type === TransactionType.SWAP) {
|
||||
return tx.receipt?.status ? <Trans>Swap confirmed</Trans> : <Trans>Swap pending</Trans>
|
||||
} else if (tx.info.type === TransactionType.WRAP) {
|
||||
if (tx.info.unwrapped) {
|
||||
return tx.receipt?.status ? <Trans>Unwrap confirmed</Trans> : <Trans>Unwrap pending</Trans>
|
||||
}
|
||||
return tx.receipt?.status ? <Trans>Wrap confirmed</Trans> : <Trans>Wrap pending</Trans>
|
||||
}
|
||||
return tx.receipt?.status ? <Trans>Transaction confirmed</Trans> : <Trans>Transaction pending</Trans>
|
||||
}, [tx.info, tx.receipt?.status])
|
||||
|
||||
return (
|
||||
<Column flex padded gap={0.75} align="stretch" style={{ height: '100%' }}>
|
||||
<StatusHeader icon={Icon} iconColor={tx.receipt?.status ? 'success' : undefined}>
|
||||
<ThemedText.Subhead1>{heading}</ThemedText.Subhead1>
|
||||
<Summary input={tx.info.inputCurrencyAmount} output={tx.info.outputCurrencyAmount} />
|
||||
{tx.info.type === TransactionType.SWAP ? (
|
||||
<SwapSummary input={tx.info.inputCurrencyAmount} output={tx.info.outputCurrencyAmount} />
|
||||
) : null}
|
||||
</StatusHeader>
|
||||
<Rule />
|
||||
<TransactionRow flex>
|
||||
<ThemedText.ButtonSmall>
|
||||
<EtherscanLink type={ExplorerDataType.TRANSACTION} data={tx.info.response.hash}>
|
||||
|
||||
@@ -3,7 +3,7 @@ import { SupportedChainId } from 'constants/chains'
|
||||
import { nativeOnChain } from 'constants/tokens'
|
||||
import { useUpdateAtom } from 'jotai/utils'
|
||||
import { useSwapInfo } from 'lib/hooks/swap'
|
||||
import { SwapInfoUpdater } from 'lib/hooks/swap/useSwapInfo'
|
||||
import { SwapInfoProvider } from 'lib/hooks/swap/useSwapInfo'
|
||||
import { Field, swapAtom } from 'lib/state/swap'
|
||||
import { useEffect } from 'react'
|
||||
import { WrappedTokenInfo } from 'state/lists/wrappedTokenInfo'
|
||||
@@ -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,14 +40,22 @@ 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
|
||||
}
|
||||
|
||||
export default (
|
||||
<>
|
||||
<SwapInfoUpdater />
|
||||
<Fixture />
|
||||
<SwapInfoProvider>
|
||||
<Fixture />
|
||||
</SwapInfoProvider>
|
||||
</>
|
||||
)
|
||||
|
||||
@@ -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]};
|
||||
@@ -26,7 +27,7 @@ interface DetailProps {
|
||||
|
||||
function Detail({ label, value, color }: DetailProps) {
|
||||
return (
|
||||
<ThemedText.Caption>
|
||||
<ThemedText.Caption userSelect>
|
||||
<Row gap={2}>
|
||||
<span>{label}</span>
|
||||
<Value color={color}>{value}</Value>
|
||||
@@ -37,16 +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,54 +1,37 @@
|
||||
import { useLingui } from '@lingui/react'
|
||||
import { Currency, CurrencyAmount } from '@uniswap/sdk-core'
|
||||
import { useUSDCValue } from 'hooks/useUSDCPrice'
|
||||
import { PriceImpact } from 'lib/hooks/useUSDCPriceImpact'
|
||||
import { ArrowRight } from 'lib/icons'
|
||||
import styled from 'lib/theme'
|
||||
import { ThemedText } from 'lib/theme'
|
||||
import { useMemo } from 'react'
|
||||
import { computeFiatValuePriceImpact } from 'utils/computeFiatValuePriceImpact'
|
||||
import { PropsWithChildren } from 'react'
|
||||
import { formatCurrencyAmount } from 'utils/formatCurrencyAmount'
|
||||
|
||||
import Column from '../../Column'
|
||||
import Row from '../../Row'
|
||||
import TokenImg from '../../TokenImg'
|
||||
|
||||
const Percent = styled.span<{ gain: boolean }>`
|
||||
color: ${({ gain, theme }) => (gain ? theme.success : theme.error)};
|
||||
`
|
||||
|
||||
interface TokenValueProps {
|
||||
input: CurrencyAmount<Currency>
|
||||
usdc?: boolean
|
||||
change?: number
|
||||
usdc?: CurrencyAmount<Currency>
|
||||
}
|
||||
|
||||
function TokenValue({ input, usdc, change }: TokenValueProps) {
|
||||
function TokenValue({ input, usdc, children }: PropsWithChildren<TokenValueProps>) {
|
||||
const { i18n } = useLingui()
|
||||
const percent = useMemo(() => {
|
||||
if (change) {
|
||||
const percent = change.toPrecision(3)
|
||||
return change > 0 ? `(+${percent}%)` : `(${percent}%)`
|
||||
}
|
||||
return undefined
|
||||
}, [change])
|
||||
|
||||
const usdcAmount = useUSDCValue(input)
|
||||
|
||||
return (
|
||||
<Column justify="flex-start">
|
||||
<Row gap={0.375} justify="flex-start">
|
||||
<TokenImg token={input.currency} />
|
||||
<ThemedText.Body2>
|
||||
<ThemedText.Body2 userSelect>
|
||||
{formatCurrencyAmount(input, 6, i18n.locale)} {input.currency.symbol}
|
||||
</ThemedText.Body2>
|
||||
</Row>
|
||||
{usdc && usdcAmount && (
|
||||
<Row justify="flex-start">
|
||||
<ThemedText.Caption color="secondary">
|
||||
${formatCurrencyAmount(usdcAmount, 2, i18n.locale)}
|
||||
{change && <Percent gain={change > 0}> {percent}</Percent>}
|
||||
</ThemedText.Caption>
|
||||
</Row>
|
||||
{usdc && (
|
||||
<ThemedText.Caption color="secondary" userSelect>
|
||||
<Row justify="flex-start" gap={0.25}>
|
||||
${formatCurrencyAmount(usdc, 6, 'en', 2)}
|
||||
{children}
|
||||
</Row>
|
||||
</ThemedText.Caption>
|
||||
)}
|
||||
</Column>
|
||||
)
|
||||
@@ -57,23 +40,19 @@ function TokenValue({ input, usdc, change }: TokenValueProps) {
|
||||
interface SummaryProps {
|
||||
input: CurrencyAmount<Currency>
|
||||
output: CurrencyAmount<Currency>
|
||||
usdc?: boolean
|
||||
inputUSDC?: CurrencyAmount<Currency>
|
||||
outputUSDC?: CurrencyAmount<Currency>
|
||||
impact?: PriceImpact
|
||||
}
|
||||
|
||||
export default function Summary({ input, output, usdc }: SummaryProps) {
|
||||
const inputUSDCValue = useUSDCValue(input)
|
||||
const outputUSDCValue = useUSDCValue(output)
|
||||
|
||||
const priceImpact = useMemo(() => {
|
||||
const computedChange = computeFiatValuePriceImpact(inputUSDCValue, outputUSDCValue)
|
||||
return computedChange ? parseFloat(computedChange.multiply(-1)?.toSignificant(3)) : undefined
|
||||
}, [inputUSDCValue, outputUSDCValue])
|
||||
|
||||
export default function Summary({ input, output, inputUSDC, outputUSDC, impact }: SummaryProps) {
|
||||
return (
|
||||
<Row gap={usdc ? 1 : 0.25}>
|
||||
<TokenValue input={input} usdc={usdc} />
|
||||
<Row gap={impact ? 1 : 0.25}>
|
||||
<TokenValue input={input} usdc={inputUSDC} />
|
||||
<ArrowRight />
|
||||
<TokenValue input={output} usdc={usdc} change={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,60 +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} usdc={true} />
|
||||
<ThemedText.Caption>
|
||||
{formatLocaleNumber({ number: 1, sigFigs: 1, locale: i18n.locale })} {inputCurrency.symbol} ={' '}
|
||||
{formatPrice(executionPrice, 6, i18n.locale)} {outputCurrency.symbol}
|
||||
</ThemedText.Caption>
|
||||
</SummaryColumn>
|
||||
<Rule />
|
||||
<Row>
|
||||
<Row gap={0.5}>
|
||||
{warning ? <AlertTriangle color={warning} /> : <Info color="secondary" />}
|
||||
<ThemedText.Subhead2 color="secondary">
|
||||
<Trans>Swap details</Trans>
|
||||
</ThemedText.Subhead2>
|
||||
</Row>
|
||||
<IconButton color="secondary" onClick={() => setOpen(!open)} icon={Expando} iconProps={{ open }} />
|
||||
</Row>
|
||||
<ExpandoColumn flex align="stretch">
|
||||
<Rule />
|
||||
<DetailsColumn>
|
||||
<Column gap={0.5} ref={setDetails} css={scrollbar}>
|
||||
<Details trade={trade} allowedSlippage={allowedSlippage} />
|
||||
</Column>
|
||||
</DetailsColumn>
|
||||
<Estimate color="secondary">
|
||||
<Trans>Output is estimated.</Trans>{' '}
|
||||
{tradeType === TradeType.EXACT_INPUT && (
|
||||
<Trans>
|
||||
You will receive at least{' '}
|
||||
{formatCurrencyAmount(trade.minimumAmountOut(allowedSlippage), 6, i18n.locale)} {outputCurrency.symbol}{' '}
|
||||
or the transaction will revert.
|
||||
</Trans>
|
||||
)}
|
||||
{tradeType === TradeType.EXACT_OUTPUT && (
|
||||
<Trans>
|
||||
You will send at most {formatCurrencyAmount(trade.maximumAmountIn(allowedSlippage), 6, i18n.locale)}{' '}
|
||||
{inputCurrency.symbol} or the transaction will revert.
|
||||
</Trans>
|
||||
)}
|
||||
</Estimate>
|
||||
<ActionButton onClick={onConfirm} action={action}>
|
||||
<Trans>Confirm swap</Trans>
|
||||
</ActionButton>
|
||||
</ExpandoColumn>
|
||||
<Body flex align="stretch" padded gap={0.75} open={open}>
|
||||
<Heading gap={0.75} flex justify="center">
|
||||
<Summary
|
||||
input={inputAmount}
|
||||
output={outputAmount}
|
||||
inputUSDC={inputUSDC}
|
||||
outputUSDC={outputUSDC}
|
||||
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,5 +1,4 @@
|
||||
import { tokens } from '@uniswap/default-token-list'
|
||||
import { DAI, USDC } from 'constants/tokens'
|
||||
import { DAI, USDC_MAINNET } from 'constants/tokens'
|
||||
import { useUpdateAtom } from 'jotai/utils'
|
||||
import { useEffect } from 'react'
|
||||
import { useSelect, useValue } from 'react-cosmos/fixture'
|
||||
@@ -37,40 +36,35 @@ function Fixture() {
|
||||
defaultValue: FEE_RECIPIENT_OPTIONS[1],
|
||||
})
|
||||
|
||||
const optionsToAddressMap: Record<string, string> = {
|
||||
none: '',
|
||||
const optionsToAddressMap: Record<string, string | undefined> = {
|
||||
None: undefined,
|
||||
Native: 'NATIVE',
|
||||
DAI: DAI.address,
|
||||
USDC: USDC.address,
|
||||
USDC: USDC_MAINNET.address,
|
||||
}
|
||||
const addressOptions = Object.keys(optionsToAddressMap)
|
||||
const [defaultInput] = useSelect('defaultInputAddress', {
|
||||
options: addressOptions,
|
||||
defaultValue: addressOptions[2],
|
||||
})
|
||||
const inputOptions = ['', '0', '100', '-1']
|
||||
const [defaultInputAmount] = useSelect('defaultInputAmount', {
|
||||
options: inputOptions,
|
||||
defaultValue: inputOptions[2],
|
||||
})
|
||||
const [defaultOutput] = useSelect('defaultOutputAddress', {
|
||||
|
||||
const [defaultInputToken] = useSelect('defaultInputToken', {
|
||||
options: addressOptions,
|
||||
defaultValue: addressOptions[1],
|
||||
})
|
||||
const [defaultOutputAmount] = useSelect('defaultOutputAmount', {
|
||||
options: inputOptions,
|
||||
defaultValue: inputOptions[0],
|
||||
const [defaultInputAmount] = useValue('defaultInputAmount', { defaultValue: 1 })
|
||||
|
||||
const [defaultOutputToken] = useSelect('defaultOutputToken', {
|
||||
options: addressOptions,
|
||||
defaultValue: addressOptions[2],
|
||||
})
|
||||
const [defaultOutputAmount] = useValue('defaultOutputAmount', { defaultValue: 0 })
|
||||
|
||||
return (
|
||||
<Swap
|
||||
convenienceFee={convenienceFee}
|
||||
convenienceFeeRecipient={convenienceFeeRecipient}
|
||||
defaultInputAddress={optionsToAddressMap[defaultInput]}
|
||||
defaultInputTokenAddress={optionsToAddressMap[defaultInputToken]}
|
||||
defaultInputAmount={defaultInputAmount}
|
||||
defaultOutputAddress={optionsToAddressMap[defaultOutput]}
|
||||
defaultOutputTokenAddress={optionsToAddressMap[defaultOutputToken]}
|
||||
defaultOutputAmount={defaultOutputAmount}
|
||||
tokenList={tokens}
|
||||
onConnectWallet={() => console.log('onConnectWallet')} // this handler is included as a test of functionality, but only logs
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,181 +0,0 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import { Token } from '@uniswap/sdk-core'
|
||||
import { useERC20PermitFromTrade } from 'hooks/useERC20Permit'
|
||||
import { useUpdateAtom } from 'jotai/utils'
|
||||
import { useSwapCurrencyAmount, useSwapInfo, useSwapTradeType } from 'lib/hooks/swap'
|
||||
import useSwapApproval, {
|
||||
ApprovalState,
|
||||
useSwapApprovalOptimizedTrade,
|
||||
useSwapRouterAddress,
|
||||
} from 'lib/hooks/swap/useSwapApproval'
|
||||
import { useSwapCallback } from 'lib/hooks/swap/useSwapCallback'
|
||||
import { useAddTransaction } from 'lib/hooks/transactions'
|
||||
import { usePendingApproval } from 'lib/hooks/transactions'
|
||||
import useActiveWeb3React from 'lib/hooks/useActiveWeb3React'
|
||||
import useTransactionDeadline from 'lib/hooks/useTransactionDeadline'
|
||||
import { 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 {
|
||||
trade,
|
||||
allowedSlippage,
|
||||
currencies: { [Field.INPUT]: inputCurrency },
|
||||
currencyBalances: { [Field.INPUT]: inputCurrencyBalance },
|
||||
currencyAmounts: { [Field.INPUT]: inputCurrencyAmount, [Field.OUTPUT]: outputCurrencyAmount },
|
||||
feeOptions,
|
||||
} = useSwapInfo()
|
||||
|
||||
const tradeType = useSwapTradeType()
|
||||
|
||||
const [activeTrade, setActiveTrade] = useState<typeof trade.trade | undefined>()
|
||||
useEffect(() => {
|
||||
setActiveTrade((activeTrade) => activeTrade && trade.trade)
|
||||
}, [trade])
|
||||
|
||||
// TODO(zzmp): Return an optimized trade directly from useSwapInfo.
|
||||
const optimizedTrade =
|
||||
// Use trade.trade if there is no swap optimized trade. This occurs if approvals are still pending.
|
||||
useSwapApprovalOptimizedTrade(trade.trade, allowedSlippage, useIsPendingApproval) || trade.trade
|
||||
|
||||
const approvalCurrencyAmount = useSwapCurrencyAmount(Field.INPUT)
|
||||
const [approval, getApproval] = useSwapApproval(
|
||||
optimizedTrade,
|
||||
allowedSlippage,
|
||||
useIsPendingApproval,
|
||||
approvalCurrencyAmount
|
||||
)
|
||||
const approvalHash = usePendingApproval(
|
||||
inputCurrency?.isToken ? inputCurrency : undefined,
|
||||
useSwapRouterAddress(optimizedTrade)
|
||||
)
|
||||
|
||||
const addTransaction = useAddTransaction()
|
||||
const addApprovalTransaction = useCallback(() => {
|
||||
getApproval().then((transaction) => {
|
||||
if (transaction) {
|
||||
addTransaction({ type: TransactionType.APPROVAL, ...transaction })
|
||||
}
|
||||
})
|
||||
}, [addTransaction, getApproval])
|
||||
|
||||
const actionProps = useMemo((): Partial<ActionButtonProps> | undefined => {
|
||||
if (!disabled && chainId) {
|
||||
if (approval === ApprovalState.NOT_APPROVED) {
|
||||
const currency = inputCurrency || approvalCurrencyAmount?.currency
|
||||
invariant(currency)
|
||||
return {
|
||||
action: {
|
||||
message: <Trans>Approve {currency.symbol} first</Trans>,
|
||||
onClick: addApprovalTransaction,
|
||||
children: <Trans>Approve</Trans>,
|
||||
},
|
||||
}
|
||||
} else if (approval === ApprovalState.PENDING) {
|
||||
return {
|
||||
disabled: true,
|
||||
action: {
|
||||
message: (
|
||||
<EtherscanLink type={ExplorerDataType.TRANSACTION} data={approvalHash}>
|
||||
<Trans>Approval pending</Trans>
|
||||
</EtherscanLink>
|
||||
),
|
||||
icon: Spinner,
|
||||
onClick: addApprovalTransaction,
|
||||
children: <Trans>Approve</Trans>,
|
||||
},
|
||||
}
|
||||
} else if (inputCurrencyAmount && inputCurrencyBalance && !inputCurrencyBalance.lessThan(inputCurrencyAmount)) {
|
||||
return {}
|
||||
}
|
||||
}
|
||||
return { disabled: true }
|
||||
}, [
|
||||
addApprovalTransaction,
|
||||
approval,
|
||||
approvalCurrencyAmount?.currency,
|
||||
approvalHash,
|
||||
chainId,
|
||||
disabled,
|
||||
inputCurrency,
|
||||
inputCurrencyAmount,
|
||||
inputCurrencyBalance,
|
||||
])
|
||||
|
||||
const deadline = useTransactionDeadline()
|
||||
const { signatureData } = useERC20PermitFromTrade(optimizedTrade, allowedSlippage, deadline)
|
||||
|
||||
// the callback to execute the swap
|
||||
const { callback: swapCallback } = useSwapCallback({
|
||||
trade: optimizedTrade,
|
||||
allowedSlippage,
|
||||
recipientAddressOrName: account ?? null,
|
||||
signatureData,
|
||||
deadline,
|
||||
feeOptions,
|
||||
})
|
||||
|
||||
//@TODO(ianlapham): add a loading state, process errors
|
||||
const setDisplayTxHash = useUpdateAtom(displayTxHashAtom)
|
||||
|
||||
const onConfirm = useCallback(() => {
|
||||
swapCallback?.()
|
||||
.then((response) => {
|
||||
setDisplayTxHash(response.hash)
|
||||
invariant(inputCurrencyAmount && outputCurrencyAmount)
|
||||
addTransaction({
|
||||
response,
|
||||
type: TransactionType.SWAP,
|
||||
tradeType,
|
||||
inputCurrencyAmount,
|
||||
outputCurrencyAmount,
|
||||
})
|
||||
})
|
||||
.catch((error) => {
|
||||
//@TODO(ianlapham): add error handling
|
||||
console.log(error)
|
||||
})
|
||||
.finally(() => {
|
||||
setActiveTrade(undefined)
|
||||
})
|
||||
}, [addTransaction, inputCurrencyAmount, outputCurrencyAmount, setDisplayTxHash, swapCallback, tradeType])
|
||||
|
||||
return (
|
||||
<>
|
||||
<ActionButton
|
||||
color={tokenColorExtraction ? 'interactive' : 'accent'}
|
||||
onClick={() => setActiveTrade(trade.trade)}
|
||||
{...actionProps}
|
||||
>
|
||||
<Trans>Review swap</Trans>
|
||||
</ActionButton>
|
||||
{activeTrade && (
|
||||
<Dialog color="dialog" onClose={() => setActiveTrade(undefined)}>
|
||||
<SummaryDialog trade={activeTrade} allowedSlippage={allowedSlippage} onConfirm={onConfirm} />
|
||||
</Dialog>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
200
src/lib/components/Swap/SwapButton/index.tsx
Normal file
@@ -0,0 +1,200 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import { useAtomValue, useUpdateAtom } from 'jotai/utils'
|
||||
import { useSwapInfo } from 'lib/hooks/swap'
|
||||
import { useSwapApprovalOptimizedTrade } from 'lib/hooks/swap/useSwapApproval'
|
||||
import { useSwapCallback } from 'lib/hooks/swap/useSwapCallback'
|
||||
import useWrapCallback, { WrapType } from 'lib/hooks/swap/useWrapCallback'
|
||||
import { useAddTransaction } from 'lib/hooks/transactions'
|
||||
import useActiveWeb3React from 'lib/hooks/useActiveWeb3React'
|
||||
import { useSetOldestValidBlock } from 'lib/hooks/useIsValidBlock'
|
||||
import useTransactionDeadline from 'lib/hooks/useTransactionDeadline'
|
||||
import { Spinner } from 'lib/icons'
|
||||
import { displayTxHashAtom, feeOptionsAtom, Field } from 'lib/state/swap'
|
||||
import { TransactionType } from 'lib/state/transactions'
|
||||
import { useTheme } from 'lib/theme'
|
||||
import { isAnimating } from 'lib/utils/animations'
|
||||
import { memo, useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import { TradeState } from 'state/routing/types'
|
||||
import invariant from 'tiny-invariant'
|
||||
|
||||
import ActionButton, { ActionButtonProps } from '../../ActionButton'
|
||||
import Dialog from '../../Dialog'
|
||||
import { SummaryDialog } from '../Summary'
|
||||
import useApprovalData, { useIsPendingApproval } from './useApprovalData'
|
||||
|
||||
interface SwapButtonProps {
|
||||
disabled?: boolean
|
||||
}
|
||||
|
||||
export default memo(function SwapButton({ disabled }: SwapButtonProps) {
|
||||
const { account, chainId } = useActiveWeb3React()
|
||||
const {
|
||||
[Field.INPUT]: {
|
||||
currency: inputCurrency,
|
||||
amount: inputCurrencyAmount,
|
||||
balance: inputCurrencyBalance,
|
||||
usdc: inputUSDC,
|
||||
},
|
||||
[Field.OUTPUT]: { usdc: outputUSDC },
|
||||
trade,
|
||||
slippage,
|
||||
impact,
|
||||
} = useSwapInfo()
|
||||
const feeOptions = useAtomValue(feeOptionsAtom)
|
||||
|
||||
// TODO(zzmp): Return an optimized trade directly from useSwapInfo.
|
||||
const optimizedTrade =
|
||||
// Use trade.trade if there is no swap optimized trade. This occurs if approvals are still pending.
|
||||
useSwapApprovalOptimizedTrade(trade.trade, slippage.allowed, useIsPendingApproval) || trade.trade
|
||||
const deadline = useTransactionDeadline()
|
||||
|
||||
const { type: wrapType, callback: wrapCallback } = useWrapCallback()
|
||||
const { approvalAction, signatureData } = useApprovalData(optimizedTrade, slippage, inputCurrencyAmount)
|
||||
const { callback: swapCallback } = useSwapCallback({
|
||||
trade: optimizedTrade,
|
||||
allowedSlippage: slippage.allowed,
|
||||
recipientAddressOrName: account ?? null,
|
||||
signatureData,
|
||||
deadline,
|
||||
feeOptions,
|
||||
})
|
||||
|
||||
const [open, setOpen] = useState(false)
|
||||
// Close the review modal if there is no available trade.
|
||||
useEffect(() => setOpen((open) => (trade.trade ? open : false)), [trade.trade])
|
||||
// Close the review modal on chain change.
|
||||
useEffect(() => setOpen(false), [chainId])
|
||||
|
||||
const addTransaction = useAddTransaction()
|
||||
const setDisplayTxHash = useUpdateAtom(displayTxHashAtom)
|
||||
const setOldestValidBlock = useSetOldestValidBlock()
|
||||
|
||||
const [isPending, setIsPending] = useState(false)
|
||||
const onWrap = useCallback(async () => {
|
||||
setIsPending(true)
|
||||
try {
|
||||
const transaction = await wrapCallback?.()
|
||||
if (!transaction) return
|
||||
addTransaction({
|
||||
response: transaction,
|
||||
type: TransactionType.WRAP,
|
||||
unwrapped: wrapType === WrapType.UNWRAP,
|
||||
currencyAmountRaw: transaction.value?.toString() ?? '0',
|
||||
chainId,
|
||||
})
|
||||
setDisplayTxHash(transaction.hash)
|
||||
} catch (e) {
|
||||
// TODO(zzmp): Surface errors from wrap.
|
||||
console.log(e)
|
||||
}
|
||||
|
||||
// Only reset pending after any queued animations to avoid layout thrashing, because a
|
||||
// successful wrap will open the status dialog and immediately cover the button.
|
||||
const postWrap = () => {
|
||||
setIsPending(false)
|
||||
document.removeEventListener('animationend', postWrap)
|
||||
}
|
||||
if (isAnimating(document)) {
|
||||
document.addEventListener('animationend', postWrap)
|
||||
} else {
|
||||
postWrap()
|
||||
}
|
||||
}, [addTransaction, chainId, setDisplayTxHash, wrapCallback, wrapType])
|
||||
// Reset the pending state if user updates the swap.
|
||||
useEffect(() => setIsPending(false), [inputCurrencyAmount, trade])
|
||||
|
||||
const onSwap = useCallback(async () => {
|
||||
try {
|
||||
const transaction = await swapCallback?.()
|
||||
if (!transaction) return
|
||||
invariant(trade.trade)
|
||||
addTransaction({
|
||||
response: transaction,
|
||||
type: TransactionType.SWAP,
|
||||
tradeType: trade.trade.tradeType,
|
||||
inputCurrencyAmount: trade.trade.inputAmount,
|
||||
outputCurrencyAmount: trade.trade.outputAmount,
|
||||
})
|
||||
setDisplayTxHash(transaction.hash)
|
||||
|
||||
// Set the block containing the response to the oldest valid block to ensure that the
|
||||
// completed trade's impact is reflected in future fetched trades.
|
||||
transaction.wait(1).then((receipt) => {
|
||||
setOldestValidBlock(receipt.blockNumber)
|
||||
})
|
||||
|
||||
// Only reset open after any queued animations to avoid layout thrashing, because a
|
||||
// successful swap will open the status dialog and immediately cover the summary dialog.
|
||||
const postSwap = () => {
|
||||
setOpen(false)
|
||||
document.removeEventListener('animationend', postSwap)
|
||||
}
|
||||
if (isAnimating(document)) {
|
||||
document.addEventListener('animationend', postSwap)
|
||||
} else {
|
||||
postSwap()
|
||||
}
|
||||
} catch (e) {
|
||||
// TODO(zzmp): Surface errors from swap.
|
||||
console.log(e)
|
||||
}
|
||||
}, [addTransaction, setDisplayTxHash, setOldestValidBlock, swapCallback, trade.trade])
|
||||
|
||||
const disableSwap = useMemo(
|
||||
() =>
|
||||
disabled ||
|
||||
!chainId ||
|
||||
(wrapType === WrapType.NONE && !optimizedTrade) ||
|
||||
!(inputCurrencyAmount && inputCurrencyBalance) ||
|
||||
inputCurrencyBalance.lessThan(inputCurrencyAmount),
|
||||
[disabled, wrapType, optimizedTrade, chainId, inputCurrencyAmount, inputCurrencyBalance]
|
||||
)
|
||||
const actionProps = useMemo((): Partial<ActionButtonProps> | undefined => {
|
||||
if (disableSwap) {
|
||||
return { disabled: true }
|
||||
} else if (wrapType === WrapType.NONE) {
|
||||
return approvalAction
|
||||
? { action: approvalAction }
|
||||
: trade.state === TradeState.VALID
|
||||
? { onClick: () => setOpen(true) }
|
||||
: { disabled: true }
|
||||
} else {
|
||||
return isPending
|
||||
? { action: { message: <Trans>Confirm in your wallet</Trans>, icon: Spinner } }
|
||||
: { onClick: onWrap }
|
||||
}
|
||||
}, [approvalAction, disableSwap, isPending, onWrap, trade.state, wrapType])
|
||||
const Label = useCallback(() => {
|
||||
switch (wrapType) {
|
||||
case WrapType.UNWRAP:
|
||||
return <Trans>Unwrap {inputCurrency?.symbol}</Trans>
|
||||
case WrapType.WRAP:
|
||||
return <Trans>Wrap {inputCurrency?.symbol}</Trans>
|
||||
case WrapType.NONE:
|
||||
default:
|
||||
return <Trans>Review swap</Trans>
|
||||
}
|
||||
}, [inputCurrency?.symbol, wrapType])
|
||||
const onClose = useCallback(() => setOpen(false), [])
|
||||
|
||||
const { tokenColorExtraction } = useTheme()
|
||||
return (
|
||||
<>
|
||||
<ActionButton color={tokenColorExtraction ? 'interactive' : 'accent'} {...actionProps}>
|
||||
<Label />
|
||||
</ActionButton>
|
||||
{open && trade.trade && (
|
||||
<Dialog color="dialog" onClose={onClose}>
|
||||
<SummaryDialog
|
||||
trade={trade.trade}
|
||||
slippage={slippage}
|
||||
inputUSDC={inputUSDC}
|
||||
outputUSDC={outputUSDC}
|
||||
impact={impact}
|
||||
onConfirm={onSwap}
|
||||
/>
|
||||
</Dialog>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
})
|
||||
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 }
|
||||
}
|
||||
@@ -2,7 +2,7 @@ import 'setimmediate'
|
||||
|
||||
import { Trans } from '@lingui/macro'
|
||||
import { Currency } from '@uniswap/sdk-core'
|
||||
import { loadingOpacityCss } from 'lib/css/loading'
|
||||
import { loadingTransitionCss } from 'lib/css/loading'
|
||||
import styled, { keyframes, ThemedText } from 'lib/theme'
|
||||
import { ReactNode, useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
|
||||
@@ -16,9 +16,10 @@ const TokenInputRow = styled(Row)`
|
||||
grid-template-columns: 1fr;
|
||||
`
|
||||
|
||||
const ValueInput = styled(DecimalInput)<{ $loading: boolean }>`
|
||||
const ValueInput = styled(DecimalInput)`
|
||||
color: ${({ theme }) => theme.primary};
|
||||
height: 1em;
|
||||
height: 1.5em;
|
||||
margin: -0.25em 0;
|
||||
|
||||
:hover:not(:focus-within) {
|
||||
color: ${({ theme }) => theme.onHover(theme.primary)};
|
||||
@@ -28,7 +29,7 @@ const ValueInput = styled(DecimalInput)<{ $loading: boolean }>`
|
||||
color: ${({ theme }) => theme.onHover(theme.secondary)};
|
||||
}
|
||||
|
||||
${loadingOpacityCss}
|
||||
${loadingTransitionCss}
|
||||
`
|
||||
|
||||
const delayedFadeIn = keyframes`
|
||||
@@ -108,7 +109,7 @@ export default function TokenInput({
|
||||
onFocus={() => setShowMax(hasMax)}
|
||||
onChange={onChangeInput}
|
||||
disabled={disabled || !currency}
|
||||
$loading={Boolean(loading)}
|
||||
isLoading={Boolean(loading)}
|
||||
ref={input}
|
||||
></ValueInput>
|
||||
</ThemedText.H2>
|
||||
|
||||
@@ -1,16 +1,23 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import { Currency, TradeType } from '@uniswap/sdk-core'
|
||||
import useUSDCPrice from 'hooks/useUSDCPrice'
|
||||
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 { AlertTriangle, Icon, Info, Spinner } from 'lib/icons'
|
||||
import { ThemedText } from 'lib/theme'
|
||||
import { ReactNode, useMemo, useState } from 'react'
|
||||
import { loadingCss } from 'lib/css/loading'
|
||||
import { PriceImpact } from 'lib/hooks/useUSDCPriceImpact'
|
||||
import { AlertTriangle, Icon, Info, InlineSpinner } from 'lib/icons'
|
||||
import styled, { ThemedText } from 'lib/theme'
|
||||
import { ReactNode, useCallback } from 'react'
|
||||
import { InterfaceTrade } from 'state/routing/types'
|
||||
|
||||
import { TextButton } from '../../Button'
|
||||
import Row from '../../Row'
|
||||
import Price from '../Price'
|
||||
import RoutingDiagram from '../RoutingDiagram'
|
||||
|
||||
const Loading = styled.span`
|
||||
color: ${({ theme }) => theme.secondary};
|
||||
${loadingCss};
|
||||
`
|
||||
|
||||
interface CaptionProps {
|
||||
icon?: Icon
|
||||
caption: ReactNode
|
||||
@@ -25,60 +32,95 @@ function Caption({ icon: Icon = AlertTriangle, caption }: CaptionProps) {
|
||||
)
|
||||
}
|
||||
|
||||
export function Connecting() {
|
||||
return (
|
||||
<Caption
|
||||
icon={InlineSpinner}
|
||||
caption={
|
||||
<Loading>
|
||||
<Trans>Connecting…</Trans>
|
||||
</Loading>
|
||||
}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export function ConnectWallet() {
|
||||
return <Caption caption={<Trans>Connect wallet to swap</Trans>} />
|
||||
}
|
||||
|
||||
export function UnsupportedNetwork() {
|
||||
return <Caption caption={<Trans>Unsupported network - switch to another to trade.</Trans>} />
|
||||
return <Caption caption={<Trans>Unsupported network - switch to another to trade</Trans>} />
|
||||
}
|
||||
|
||||
export function InsufficientBalance({ currency }: { currency: Currency }) {
|
||||
return <Caption caption={<Trans>Insufficient {currency?.symbol} balance</Trans>} />
|
||||
}
|
||||
|
||||
export function InsufficientLiquidity() {
|
||||
return <Caption caption={<Trans>Insufficient liquidity in the pool for your trade</Trans>} />
|
||||
}
|
||||
|
||||
export function Error() {
|
||||
return <Caption caption={<Trans>Error fetching trade</Trans>} />
|
||||
}
|
||||
|
||||
export function Empty() {
|
||||
return <Caption icon={Info} caption={<Trans>Enter an amount</Trans>} />
|
||||
}
|
||||
|
||||
export function LoadingTrade() {
|
||||
return <Caption icon={Spinner} caption={<Trans>Fetching best price…</Trans>} />
|
||||
return (
|
||||
<Caption
|
||||
icon={InlineSpinner}
|
||||
caption={
|
||||
<Loading>
|
||||
<Trans>Fetching best price…</Trans>
|
||||
</Loading>
|
||||
}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export function Trade({ trade }: { trade: InterfaceTrade<Currency, Currency, TradeType> }) {
|
||||
const [flip, setFlip] = useState(true)
|
||||
const { inputAmount, outputAmount, executionPrice } = trade
|
||||
const fiatValueInput = useUSDCPrice(inputAmount.currency)
|
||||
const fiatValueOutput = useUSDCPrice(outputAmount.currency)
|
||||
export function WrapCurrency({ inputCurrency, outputCurrency }: { inputCurrency: Currency; outputCurrency: Currency }) {
|
||||
const Text = useCallback(
|
||||
() => (
|
||||
<Trans>
|
||||
Convert {inputCurrency.symbol} to {outputCurrency.symbol} with no slippage
|
||||
</Trans>
|
||||
),
|
||||
[inputCurrency.symbol, outputCurrency.symbol]
|
||||
)
|
||||
|
||||
const ratio = useMemo(() => {
|
||||
const [a, b] = flip ? [outputAmount, inputAmount] : [inputAmount, outputAmount]
|
||||
const priceString = (!flip ? executionPrice : executionPrice?.invert())?.toSignificant(6)
|
||||
|
||||
const ratio = `1 ${a.currency.symbol} = ${priceString} ${b.currency.symbol}`
|
||||
const usdc = !flip
|
||||
? fiatValueInput
|
||||
? ` ($${fiatValueInput.toSignificant(6)})`
|
||||
: null
|
||||
: fiatValueOutput
|
||||
? ` ($${fiatValueOutput.toSignificant(6)})`
|
||||
: null
|
||||
|
||||
return (
|
||||
<Row gap={0.25} style={{ userSelect: 'text' }}>
|
||||
{ratio}
|
||||
{usdc && <ThemedText.Caption color="secondary">{usdc}</ThemedText.Caption>}
|
||||
</Row>
|
||||
)
|
||||
}, [executionPrice, fiatValueInput, fiatValueOutput, flip, inputAmount, outputAmount])
|
||||
return <Caption icon={Info} caption={<Text />} />
|
||||
}
|
||||
|
||||
export function Trade({
|
||||
trade,
|
||||
outputUSDC,
|
||||
impact,
|
||||
}: {
|
||||
trade: InterfaceTrade<Currency, Currency, TradeType>
|
||||
outputUSDC?: CurrencyAmount<Currency>
|
||||
impact?: PriceImpact
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
<Tooltip placement="bottom" icon={Info}>
|
||||
<RoutingDiagram trade={trade} />
|
||||
<Tooltip placement="bottom" icon={impact?.warning ? AlertTriangle : Info}>
|
||||
<Column gap={0.75}>
|
||||
{impact?.warning && (
|
||||
<>
|
||||
<ThemedText.Caption>
|
||||
The output amount is estimated at {impact.toString()} less than the input amount due to high price
|
||||
impact
|
||||
</ThemedText.Caption>
|
||||
<Rule />
|
||||
</>
|
||||
)}
|
||||
<RoutingDiagram trade={trade} />
|
||||
</Column>
|
||||
</Tooltip>
|
||||
<TextButton color="primary" onClick={() => setFlip(!flip)}>
|
||||
{ratio}
|
||||
</TextButton>
|
||||
<Price trade={trade} outputUSDC={outputUSDC} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { ALL_SUPPORTED_CHAIN_IDS } from 'constants/chains'
|
||||
import { useIsAmountPopulated, useSwapInfo } from 'lib/hooks/swap'
|
||||
import useWrapCallback, { WrapType } from 'lib/hooks/swap/useWrapCallback'
|
||||
import useActiveWeb3React from 'lib/hooks/useActiveWeb3React'
|
||||
import { largeIconCss } from 'lib/icons'
|
||||
import { Field } from 'lib/state/swap'
|
||||
import styled, { ThemedText } from 'lib/theme'
|
||||
import { useMemo } from 'react'
|
||||
import { memo, useMemo } from 'react'
|
||||
import { TradeState } from 'state/routing/types'
|
||||
|
||||
import Row from '../../Row'
|
||||
@@ -16,42 +17,63 @@ const ToolbarRow = styled(Row)`
|
||||
${largeIconCss}
|
||||
`
|
||||
|
||||
export default function Toolbar({ disabled }: { disabled?: boolean }) {
|
||||
const { chainId } = useActiveWeb3React()
|
||||
export default memo(function Toolbar() {
|
||||
const { active, activating, 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 } = useWrapCallback()
|
||||
const caption = useMemo(() => {
|
||||
if (disabled) {
|
||||
if (!active || !chainId) {
|
||||
if (activating) return <Caption.Connecting />
|
||||
return <Caption.ConnectWallet />
|
||||
}
|
||||
|
||||
if (chainId && !ALL_SUPPORTED_CHAIN_IDS.includes(chainId)) {
|
||||
if (!ALL_SUPPORTED_CHAIN_IDS.includes(chainId)) {
|
||||
return <Caption.UnsupportedNetwork />
|
||||
}
|
||||
|
||||
if (inputCurrency && outputCurrency && isAmountPopulated) {
|
||||
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, inputCurrency, isAmountPopulated, isRouteLoading, outputCurrency, trade])
|
||||
}, [
|
||||
activating,
|
||||
active,
|
||||
chainId,
|
||||
impact,
|
||||
inputAmount,
|
||||
inputBalance,
|
||||
inputCurrency,
|
||||
isAmountPopulated,
|
||||
outputCurrency,
|
||||
outputUSDC,
|
||||
state,
|
||||
trade,
|
||||
wrapType,
|
||||
])
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -63,4 +85,4 @@ export default function Toolbar({ disabled }: { disabled?: boolean }) {
|
||||
</ThemedText.Caption>
|
||||
</>
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,17 +1,15 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import { TokenInfo } from '@uniswap/token-lists'
|
||||
import { ALL_SUPPORTED_CHAIN_IDS } from 'constants/chains'
|
||||
import { useAtom } from 'jotai'
|
||||
import { SwapInfoUpdater } from 'lib/hooks/swap/useSwapInfo'
|
||||
import useSyncConvenienceFee from 'lib/hooks/swap/useSyncConvenienceFee'
|
||||
import useSyncSwapDefaults from 'lib/hooks/swap/useSyncSwapDefaults'
|
||||
import { SwapInfoProvider } from 'lib/hooks/swap/useSwapInfo'
|
||||
import useSyncConvenienceFee, { FeeOptions } from 'lib/hooks/swap/useSyncConvenienceFee'
|
||||
import useSyncTokenDefaults, { TokenDefaults } from 'lib/hooks/swap/useSyncTokenDefaults'
|
||||
import { usePendingTransactions } from 'lib/hooks/transactions'
|
||||
import useActiveWeb3React from 'lib/hooks/useActiveWeb3React'
|
||||
import useHasFocus from 'lib/hooks/useHasFocus'
|
||||
import useTokenList, { useSyncTokenList } from 'lib/hooks/useTokenList'
|
||||
import useOnSupportedNetwork from 'lib/hooks/useOnSupportedNetwork'
|
||||
import { displayTxHashAtom } from 'lib/state/swap'
|
||||
import { SwapTransactionInfo, Transaction, TransactionType } from 'lib/state/transactions'
|
||||
import { useMemo, useState } from 'react'
|
||||
import { SwapTransactionInfo, Transaction, TransactionType, WrapTransactionInfo } from 'lib/state/transactions'
|
||||
import { useState } from 'react'
|
||||
|
||||
import Dialog from '../Dialog'
|
||||
import Header from '../Header'
|
||||
@@ -23,67 +21,62 @@ import ReverseButton from './ReverseButton'
|
||||
import Settings from './Settings'
|
||||
import { StatusDialog } from './Status'
|
||||
import SwapButton from './SwapButton'
|
||||
import SwapPropValidator from './SwapPropValidator'
|
||||
import Toolbar from './Toolbar'
|
||||
import useValidate from './useValidate'
|
||||
|
||||
export type DefaultAddress = string | { [chainId: number]: string | 'NATIVE' } | 'NATIVE'
|
||||
|
||||
function getSwapTx(txs: { [hash: string]: Transaction }, hash?: string): Transaction<SwapTransactionInfo> | undefined {
|
||||
function getTransactionFromMap(
|
||||
txs: { [hash: string]: Transaction },
|
||||
hash?: string
|
||||
): Transaction<SwapTransactionInfo | WrapTransactionInfo> | undefined {
|
||||
if (hash) {
|
||||
const tx = txs[hash]
|
||||
if (tx?.info?.type === TransactionType.SWAP) {
|
||||
return tx as Transaction<SwapTransactionInfo>
|
||||
}
|
||||
if (tx?.info?.type === TransactionType.WRAP) {
|
||||
return tx as Transaction<WrapTransactionInfo>
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
export interface SwapProps {
|
||||
tokenList?: string | TokenInfo[]
|
||||
defaultInputAddress?: DefaultAddress
|
||||
defaultInputAmount?: string
|
||||
defaultOutputAddress?: DefaultAddress
|
||||
defaultOutputAmount?: string
|
||||
convenienceFee?: number
|
||||
convenienceFeeRecipient?: string | { [chainId: number]: string }
|
||||
export interface SwapProps extends TokenDefaults, FeeOptions {
|
||||
onConnectWallet?: () => void
|
||||
}
|
||||
|
||||
export default function Swap(props: SwapProps) {
|
||||
useSyncTokenList(props.tokenList)
|
||||
useSyncSwapDefaults(props)
|
||||
useValidate(props)
|
||||
useSyncConvenienceFee(props)
|
||||
useSyncTokenDefaults(props)
|
||||
|
||||
const { active, account, chainId } = useActiveWeb3React()
|
||||
const { active, account } = useActiveWeb3React()
|
||||
const [wrapper, setWrapper] = useState<HTMLDivElement | null>(null)
|
||||
|
||||
const [displayTxHash, setDisplayTxHash] = useAtom(displayTxHashAtom)
|
||||
const pendingTxs = usePendingTransactions()
|
||||
const displayTx = getSwapTx(pendingTxs, displayTxHash)
|
||||
const displayTx = getTransactionFromMap(pendingTxs, displayTxHash)
|
||||
|
||||
const tokenList = useTokenList()
|
||||
const isSwapSupported = useMemo(
|
||||
() => Boolean(chainId && ALL_SUPPORTED_CHAIN_IDS.includes(chainId) && tokenList?.length),
|
||||
[chainId, tokenList]
|
||||
)
|
||||
const onSupportedNetwork = useOnSupportedNetwork()
|
||||
const isDisabled = !(active && onSupportedNetwork)
|
||||
|
||||
const focused = useHasFocus(wrapper)
|
||||
|
||||
return (
|
||||
<SwapPropValidator {...props}>
|
||||
{isSwapSupported && <SwapInfoUpdater />}
|
||||
<>
|
||||
<Header title={<Trans>Swap</Trans>}>
|
||||
{active && <Wallet disabled={!account} onClick={props.onConnectWallet} />}
|
||||
<Settings disabled={!active} />
|
||||
<Settings disabled={isDisabled} />
|
||||
</Header>
|
||||
<div ref={setWrapper}>
|
||||
<BoundaryProvider value={wrapper}>
|
||||
<Input disabled={!active} focused={focused} />
|
||||
<ReverseButton disabled={!active} />
|
||||
<Output disabled={!active} focused={focused}>
|
||||
<Toolbar disabled={!active} />
|
||||
<SwapButton disabled={!account} />
|
||||
</Output>
|
||||
<SwapInfoProvider disabled={isDisabled}>
|
||||
<Input disabled={isDisabled} focused={focused} />
|
||||
<ReverseButton disabled={isDisabled} />
|
||||
<Output disabled={isDisabled} focused={focused}>
|
||||
<Toolbar />
|
||||
<SwapButton disabled={isDisabled} />
|
||||
</Output>
|
||||
</SwapInfoProvider>
|
||||
</BoundaryProvider>
|
||||
</div>
|
||||
{displayTx && (
|
||||
@@ -91,6 +84,6 @@ export default function Swap(props: SwapProps) {
|
||||
<StatusDialog tx={displayTx} onClose={() => setDisplayTxHash()} />
|
||||
</Dialog>
|
||||
)}
|
||||
</SwapPropValidator>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||