Compare commits

...

87 Commits

Author SHA1 Message Date
Christine Legge
71aebf33db fix: remove unused var (#3736) 2022-04-14 16:26:42 -04:00
Christine Legge
5ff428b04b fix: update widgets README 2022-04-14 16:06:57 -04:00
Mark Carbajal
acb0c2056e chore: Remove Portis (#3693)
* Removed portis

* Removed portis

* Removed portis

* Update src/components/WalletModal/index.tsx

Co-authored-by: Bruno Crosier <bruno.crosier@gmail.com>

* regenerate yarn.lock

* revert translation changes

Co-authored-by: Bruno Crosier <bruno.crosier@gmail.com>
Co-authored-by: Noah Zinsmeister <noahwz@gmail.com>
2022-04-14 15:23:31 -04:00
Christine Legge
0a4bcb62da add 1bp fee tier to polygon (#3724) 2022-04-14 15:11:58 -04:00
Zach Pomerantz
f50bcbdb2d fix: initial transitions (#3719)
* fix: rm action fade

* fix: disallow stale swaps

* fix: fade in buttons

* fix: fade in input text

* fix: standardize border handling

* fix: transition token button width

* fix: cleanup transitions

* fix: use transition for button

* chore: cleanup
2022-04-13 11:45:29 -07:00
Christine Legge
cbe421ee23 fix: Reload the app when there is a javascript error and a new version of the app (#3715)
* reload the app when encountering a javascript error if there is an update

* remove console.logs

* Add more comments
2022-04-13 13:53:54 -04:00
Zach Pomerantz
3439786c38 feat: display connecting state (#3713) 2022-04-13 09:21:28 -07:00
Zach Pomerantz
6294915be6 fix: convert token list to context (#3712)
* fix: convert token list to context

* fix: cosmos
2022-04-12 14:53:50 -07:00
Zach Pomerantz
984c742d0e fix: use context for block number (#3708)
* fix: use context for block number

* fix: check for valid BlockNumberContext
2022-04-12 10:10:57 -07:00
Zach Pomerantz
00b151d7fa fix: activation frames (#3711) 2022-04-12 09:23:19 -07:00
guil-lambert
5967cf5d9d fix: bug where user cannot burn lp position if fetching fee values fails. (#3633)
* fix: can burn position even if fetching fees fails.

* Revert "fix: can burn position even if fetching fees fails."

This reverts commit a96f7178e5.

* recover more gracefully from failed fee fetch

Co-authored-by: Noah Zinsmeister <noahwz@gmail.com>
2022-04-12 11:41:31 -04:00
Zach Pomerantz
e480f0ebe5 chore: simplify swap info (#3710)
* fix: prevent unnecessary TokenImg renders

* fix: prevent unnecessary trade renders

* fix: simplify swap info computation
2022-04-11 17:09:11 -07:00
Zach Pomerantz
f6ceecbc5e fix: update hook deps to improve ref equality checks (#3707)
* fix: prevent unnecessary TokenImg renders

* fix: prevent unnecessary trade renders
2022-04-11 16:57:53 -07:00
Zach Pomerantz
a0348b45be chore: bump to v1.0.6 (#3696) 2022-04-08 13:13:11 -07:00
Zach Pomerantz
e4b37cffcc fix: skewed swap info state (#3695)
* fix: skewed swap info state

* fix: typings
2022-04-08 13:11:19 -07:00
Zach Pomerantz
dd69cccf91 fix: always run global updaters (#3694) 2022-04-08 12:38:39 -07:00
Zach Pomerantz
8b228de88f chore: bump to v1.0.5 (#3691) 2022-04-08 10:52:30 -07:00
Zach Pomerantz
f91fc3c6a6 fix: defer layout effects (#3687)
* fix: use effect for color

* chore: clean up token defaults

* fix: condition updaters on active tokens
2022-04-08 10:27:10 -07:00
Zach Pomerantz
e0e2b40f9f chore: bump to v1.0.4 (#3686) 2022-04-07 15:05:35 -07:00
Zach Pomerantz
bc1c61b63a fix: omit document ref (#3685) 2022-04-07 15:05:11 -07:00
Alex Dorsch
446ad3e0d4 fix: missing token balance (#3661)
* increase gas required to read token balance

* set token balance gas requirement to 185_000
2022-04-07 15:00:03 -07:00
Zach Pomerantz
65e58a08cf fix: show i18n keys while messages load (#3683)
* fix: show i18n keys while messages load

* fix: i18n initialization check
2022-04-07 14:55:09 -07:00
Zach Pomerantz
71b20b432c fix: block number stability (#3684)
* fix: block number stability

* fix: chainBlock logic
2022-04-07 14:26:50 -07:00
Zach Pomerantz
ecfa179b3f chore: bump to v1.0.3 2022-04-07 11:24:33 -07:00
Zach Pomerantz
6c94a0f585 fix: swap validator (#3682) 2022-04-07 11:23:21 -07:00
Zach Pomerantz
600aeaaff1 fix: polling memory leak (#3676)
* chore: clarify stale callback

* fix: polling memory leak
2022-04-06 17:21:44 -07:00
Zach Pomerantz
3bfbc74e47 chore: bump to v1.0.2 (#3675) 2022-04-06 13:05:06 -07:00
Zach Pomerantz
84f76e34b2 fix: do not fetch wrap price (#3673)
* fix: do not fetch wrap price

* fix: abort trade computation for wraps
2022-04-06 13:04:16 -07:00
Zach Pomerantz
b965bed865 fix: token input height (#3672) 2022-04-06 12:14:15 -07:00
Zach Pomerantz
a9039e8d0b chore: bump to v1.0.1 (#3670) 2022-04-06 10:02:37 -07:00
Zach Pomerantz
60d35b46f3 fix: simplify validation (#3665)
* fix: simplify widget validation

* test: update cosmos to trigger edge cases

* fix: simplify swap validation
2022-04-06 09:21:50 -07:00
Ian Lapham
3d422cf707 update address list (#3669) 2022-04-06 11:48:47 -04:00
Zach Pomerantz
84c70ac84d chore: bump to v1.0.0 (#3663) 2022-04-05 10:45:36 -07:00
Zach Pomerantz
de3a33dfcb fix: stale data edge cases (#3657)
* fix: stale chain block

* chore: simplify atom usage

* fix: support single-token chain

* fix: avoid extra rpcs

* chore: rename isDisabled

* fix: simplify useUSDCPrice

* fix: simplify useComputeSwapInfo

* chore: include type

* fix: guard hasAmounts
2022-04-05 10:45:21 -07:00
Zach Pomerantz
99a084f230 fix: JsonRpc url wrapper (#3662)
* fix: JsonRpc url wrapper

* chore: finish renaming
2022-04-05 10:00:17 -07:00
Jordan Frankfurt
e880955743 chore(widgets): bump version (#3645) 2022-04-01 18:06:12 -04:00
Zach Pomerantz
bbf43fcd27 fix: walletconnect numeric chain id (#3643) 2022-04-01 10:57:11 -07:00
Zach Pomerantz
a00ac56389 chore: bump to v0.0.30-beta (#3637) 2022-03-31 14:49:44 -07:00
Noah Zinsmeister
7201944bc2 Revert "fix(error handling): try reloading the app when encountering a javascript error (#3435)"
This reverts commit 5cf9e84db5.
2022-03-31 17:24:47 -04:00
Noah Zinsmeister
b0ff0f83b0 fix crash (#3634) 2022-03-31 16:41:01 -04:00
Moody Salem
5cf9e84db5 fix(error handling): try reloading the app when encountering a javascript error (#3435) 2022-03-31 16:39:31 -04:00
Zach Pomerantz
c0bdb8db12 fix: break unnecessary hierarchical deps (#3629) 2022-03-30 21:23:33 -07:00
Zach Pomerantz
2d8f767d74 feat: upgrade web3-react (#3628)
* chore: upgrade web3-react

* feat: use a JsonRpcConnector

* chore: rm @ethersproject/experimental

* fix: assert Web3Provider in app

* fix: type providers more loosely

* chore: reinstall experimental for testing
2022-03-30 20:45:43 -07:00
Zach Pomerantz
1303416eca feat: lazy load the lib's smart-order-router (#3624)
* feat: lazy-load the smart-order-router

* chore: guard against regression
2022-03-30 14:09:18 -07:00
Zach Pomerantz
124f6420a5 fix: lazy load en-us (#3626) 2022-03-30 13:32:49 -07:00
Zach Pomerantz
91f5fc0881 chore: upgrade redux-multicall and smart-order-router (#3623)
* chore: upgrade redux-multicall and smart-order-router

* chore: provide @ethersproject through ethers
2022-03-29 12:00:56 -07:00
Zach Pomerantz
ec831f8433 chore: destructure json imports (#3622) 2022-03-29 12:00:42 -07:00
Crowdin Bot
56bd9b68d7 chore(i18n): synchronize translations from crowdin [skip ci] 2022-03-29 06:07:44 +00:00
Crowdin Bot
865d21f039 chore(i18n): synchronize translations from crowdin [skip ci] 2022-03-28 23:07:52 +00:00
Jordan Frankfurt
dceadf8472 chore(docs): add widgets info to the repo README (#3600)
* add widgets info to the repo README

* pr feedback from z

* pr feedback from w and z

* separate README.md and INTERFACE_README.md

* Update WIDGETS_README.md

Co-authored-by: Will Hennessy <hennessywill@gmail.com>

Co-authored-by: Will Hennessy <hennessywill@gmail.com>
2022-03-28 11:35:14 -05:00
Crowdin Bot
cd3a91bca8 chore(i18n): synchronize translations from crowdin [skip ci] 2022-03-25 07:06:55 +00:00
Connor McEwen
de1f5d1adc feat: migrate to GA4 (#3599) 2022-03-24 21:44:48 -04:00
Crowdin Bot
b5d403768f chore(i18n): synchronize translations from crowdin [skip ci] 2022-03-24 20:07:03 +00:00
Zach Pomerantz
c4c811aeb3 chore: bump to v0.0.26-beta (#3593) 2022-03-24 16:02:38 -04:00
Zach Pomerantz
33c24a3f05 fix: trade displays (#3594)
* fix: show syncing over insufficient balance

* fix: mv anti-janking up a level

* feat: add error caption for completeness

* chore: clarify naming
2022-03-24 16:02:20 -04:00
Zach Pomerantz
9ef2b3a116 chore: add ethers (#3591) 2022-03-24 12:51:03 -04:00
Zach Pomerantz
afe38a2d10 fix: tick before returning quote (#3598) 2022-03-24 12:33:35 -04:00
dependabot[bot]
d28607a1c8 chore(deps-dev): bump @uniswap/default-token-list from 3.0.0 to 3.1.0 (#3564) 2022-03-24 12:09:44 -04:00
dependabot[bot]
7fb363ac46 chore(deps): bump @uniswap/token-lists (#3596) 2022-03-24 12:09:22 -04:00
Zach Pomerantz
16b0b1530d fix: memoize client side v3 router call data (#3595)
* fix: memoize client side v3 router call data

* fix: rm log
2022-03-24 10:30:28 -04:00
Zach Pomerantz
abb2696f40 fix: upgrade @web3-react/eip1193 to fallback to eth_accounts (#3590) 2022-03-23 20:45:49 -04:00
Zach Pomerantz
772178fc86 fix: add web3 debug log (#3587) 2022-03-23 20:45:22 -04:00
Crowdin Bot
9f1378f635 chore(i18n): synchronize translations from crowdin [skip ci] 2022-03-23 19:09:22 +00:00
Zach Pomerantz
84275dcce1 fix: action button jank (#3582) 2022-03-23 14:32:45 -04:00
Zach Pomerantz
a76ece6ce3 feat: prompt for interaction "in your wallet" (#3585)
* feat: prompt approval in wallet

* feat: prompt wrap in wallet

* feat: prompt confirm in wallet

* fix: animations

* fix: test typing
2022-03-23 14:12:58 -04:00
Zach Pomerantz
334e137fb3 fix: empty toolbar for empty input (#3584) 2022-03-23 14:07:56 -04:00
Zach Pomerantz
eb6c4d464a fix: do not allow zeroes (#3583) 2022-03-23 14:07:41 -04:00
Zach Pomerantz
24734e6a34 Revert "fix: retain permit validity for lesser amounts (#3580)" (#3586)
This reverts commit f1bcee3c08.
2022-03-23 10:33:50 -04:00
Zach Pomerantz
f1bcee3c08 fix: retain permit validity for lesser amounts (#3580) 2022-03-23 09:56:07 -04:00
Crowdin Bot
7a215ccdb4 chore(i18n): synchronize translations from crowdin [skip ci] 2022-03-23 08:07:22 +00:00
Crowdin Bot
c5c4f48d96 chore(i18n): synchronize translations from crowdin [skip ci] 2022-03-22 22:07:21 +00:00
Zach Pomerantz
bdcf761ddd chore: refactor swap button for maintainability (#3579)
* chore: mv SwapButton to dir

* chore: mv approval data to its own hook

* chore: mv approval actions to approvals hook

* chore: simplify SwapButton logic

* fix: pass through approval amount

* fix: mv error handling to consumer
2022-03-22 17:59:05 -04:00
Crowdin Bot
e876267d83 chore(i18n): synchronize translations from crowdin [skip ci] 2022-03-22 20:06:44 +00:00
Zach Pomerantz
76cbd82cb7 fix: bring wrap ui to spec (#3577) 2022-03-22 15:30:12 -04:00
Zach Pomerantz
6c4f7ab9a1 fix: modal border radius (#3576) 2022-03-22 13:44:33 -04:00
Zach Pomerantz
da20315724 fix: retain stale trade (#3578) 2022-03-22 13:03:02 -04:00
Zach Pomerantz
963b910552 fix: trade loading state (#3572)
* fix: invert stale callback

* fix: polling and validation logic

* fix: rm unused conditional
2022-03-22 12:23:38 -04:00
Zach Pomerantz
9e2dc9a435 fix: rm approval invariant (#3569) 2022-03-21 16:40:27 -04:00
Ian Lapham
6567f18bf5 update code formatting for prettier (#3571) 2022-03-21 16:20:59 -04:00
Zach Pomerantz
ee96973212 chore: clean dialog mounting logic (#3559)
* fix: apply scrollbar css on first render

* fix: useUnmount portability

* chore: clean up dialog ordering

* fix: dialog border-radius

* chore: cleanup dialog unmount animation
2022-03-21 15:55:46 -04:00
Zach Pomerantz
ce6c783174 fix: trade UI latency (#3563)
* fix: do not delay trade update

* fix: do not delay initial input
2022-03-21 15:03:09 -04:00
Zach Pomerantz
64e8c3ced9 fix: token img jank (#3562) 2022-03-21 15:00:03 -04:00
Zach Pomerantz
46e6c2295d fix: prevent scroll on token select focus (#3560) 2022-03-21 14:58:49 -04:00
Crowdin Bot
3626dbdeec chore(i18n): synchronize translations from crowdin [skip ci] 2022-03-21 18:12:24 +00:00
Zach Pomerantz
eb75e0dc2e fix: set approval to pending on wallet request (#3570) 2022-03-21 13:37:53 -04:00
Zach Pomerantz
c26ecdfc88 fix: use a min fresh block (#3568)
* chore: mv useFilterFresh to its own hook

* fix: use a minimum fresh block

* fix: re-poll on stale data

* chore: rename to staleCallback

* check for undefined

* chore: rename fresh->valid

Co-authored-by: ianlapham <ianlapham@gmail.com>
2022-03-21 13:18:21 -04:00
Zach Pomerantz
f508788026 fix: impact rendering (#3553)
* fix: price impact rendering

* fix: webkit-compat gradient
2022-03-21 10:26:35 -04:00
153 changed files with 3360 additions and 3495 deletions

View File

@@ -1,5 +1,4 @@
REACT_APP_INFURA_KEY="099fc58e0de9451d80b18d7c74caa7c1"
REACT_APP_PORTIS_ID="c0e2bf01-4b08-4fd5-ac7b-8e26b58cd236"
REACT_APP_FORTMATIC_KEY="pk_live_F937DF033A1666BF"
REACT_APP_GOOGLE_ANALYTICS_ID="UA-128182339-4"
REACT_APP_GOOGLE_ANALYTICS_ID="G-KDP9B6W4H8"
REACT_APP_FIREBASE_KEY="AIzaSyBcZWwTcTJHj_R6ipZcrJkXdq05PuX0Rs0"

45
INTERFACE_README.md Normal file
View 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).

View File

@@ -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).
[![Unit Tests](https://github.com/Uniswap/uniswap-interface/actions/workflows/unit-tests.yaml/badge.svg)](https://github.com/Uniswap/uniswap-interface/actions/workflows/unit-tests.yaml)
[![Integration Tests](https://github.com/Uniswap/uniswap-interface/actions/workflows/integration-tests.yaml/badge.svg)](https://github.com/Uniswap/uniswap-interface/actions/workflows/integration-tests.yaml)
[![Lint](https://github.com/Uniswap/uniswap-interface/actions/workflows/lint.yml/badge.svg)](https://github.com/Uniswap/uniswap-interface/actions/workflows/lint.yml)
[![Release](https://github.com/Uniswap/uniswap-interface/actions/workflows/release.yaml/badge.svg)](https://github.com/Uniswap/uniswap-interface/actions/workflows/release.yaml)
# Uniswap Labs Interface
[![Unit Tests](https://github.com/Uniswap/interface/actions/workflows/unit-tests.yaml/badge.svg)](https://github.com/Uniswap/interface/actions/workflows/unit-tests.yaml)
[![Integration Tests](https://github.com/Uniswap/interface/actions/workflows/integration-tests.yaml/badge.svg)](https://github.com/Uniswap/interface/actions/workflows/integration-tests.yaml)
[![Lint](https://github.com/Uniswap/interface/actions/workflows/lint.yml/badge.svg)](https://github.com/Uniswap/interface/actions/workflows/lint.yml)
[![Release](https://github.com/Uniswap/interface/actions/workflows/release.yaml/badge.svg)](https://github.com/Uniswap/interface/actions/workflows/release.yaml)
[![Crowdin](https://badges.crowdin.net/uniswap-interface/localized.svg)](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
View 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.
![swap widget screenshot](https://raw.githubusercontent.com/Uniswap/interface/main/src/assets/images/widget-screenshot.png)
You can customize the theme (colors, fonts, border radius, and more) to match the style of your application. You can also configure your own default token list and optionally set a convenience fee on swaps executed through the widget on your site.
## Installation
Install the widgets library via `npm` or `yarn`. If you do not already use the widget's peerDependencies `redux` and `react-redux`, then you'll need to add them as well.
```js
yarn add @uniswap/widgets redux react-redux
```
```js
npm i --save @uniswap/widgets redux react-redux
```
## Documentation
- [overview](https://docs.uniswap.org/sdk/widgets/swap-widget)
- [api reference](https://docs.uniswap.org/sdk/widgets/swap-widget/api)
## Example Apps
Uniswap Labs maintains two demo apps in branches of the [widgets-demo](https://github.com/Uniswap/widgets-demo) repo:
- [NextJS](https://github.com/Uniswap/widgets-demo/tree/nextjs)
- [Create React App](https://github.com/Uniswap/widgets-demo/tree/cra)
Others have also also released the widget in production to their userbase:
- [OpenSea](https://opensea.io/)
- [Friends With Benefits](https://www.fwb.help/)
- [Oasis](https://oasis.app/)
## Legal notice
Uniswap Labs encourages integrators to evaluate their own regulatory obligations when integrating this widget into their products, including, but not limited to, those related to economic or trade sanctions compliance.

View File

@@ -1,6 +1,6 @@
{
"name": "@uniswap/widgets",
"version": "0.0.25-beta",
"version": "1.0.6",
"description": "Uniswap Interface",
"homepage": ".",
"files": [
@@ -30,6 +30,7 @@
"@babel/preset-env": "^7.16.11",
"@babel/preset-react": "^7.16.7",
"@babel/preset-typescript": "^7.16.7",
"@ethersproject/experimental": "^5.4.0",
"@gnosis.pm/safe-apps-web3-react": "^0.6.0",
"@graphql-codegen/cli": "1.21.5",
"@graphql-codegen/typescript": "1.22.3",
@@ -89,8 +90,8 @@
"@uniswap/v2-periphery": "^1.1.0-beta.0",
"@uniswap/v3-core": "1.0.0",
"@uniswap/v3-periphery": "^1.1.1",
"@web3-react/metamask": "8.0.13-beta.0",
"@web3-react/walletconnect": "8.0.18-beta.0",
"@web3-react/metamask": "^8.0.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",
@@ -118,7 +119,7 @@
"react-confetti": "^6.0.0",
"react-cosmos": "^5.6.6",
"react-dom": "^17.0.1",
"react-ga": "^2.5.7",
"react-ga4": "^1.4.1",
"react-is": "^17.0.2",
"react-markdown": "^4.3.1",
"react-redux": "^7.2.2",
@@ -149,7 +150,6 @@
"web3-react-abstract-connector": "npm:@web3-react/abstract-connector@^6.0.7",
"web3-react-fortmatic-connector": "npm:@web3-react/fortmatic-connector@^6.0.9",
"web3-react-injected-connector": "npm:@web3-react/injected-connector@^6.0.7",
"web3-react-portis-connector": "npm:@web3-react/portis-connector@^6.0.9",
"web3-react-types": "npm:@web3-react/types@^6.0.7",
"web3-react-walletconnect-connector": "npm:@web3-react/walletconnect-connector@^7.0.2-alpha.0",
"web3-react-walletlink-connector": "npm:@web3-react/walletlink-connector@^6.2.13",
@@ -193,39 +193,25 @@
"license": "GPL-3.0-or-later",
"dependencies": {
"@babel/runtime": "^7.17.0",
"@ethersproject/abi": "^5.4.1",
"@ethersproject/abstract-provider": "^5.4.1",
"@ethersproject/abstract-signer": "^5.4.1",
"@ethersproject/address": "^5.4.0",
"@ethersproject/bignumber": "^5.4.2",
"@ethersproject/bytes": "^5.4.0",
"@ethersproject/constants": "^5.4.0",
"@ethersproject/contracts": "^5.4.1",
"@ethersproject/experimental": "^5.4.0",
"@ethersproject/hash": "^5.4.0",
"@ethersproject/providers": "^5.4.0",
"@ethersproject/solidity": "^5.4.0",
"@ethersproject/strings": "^5.4.0",
"@ethersproject/units": "^5.4.0",
"@ethersproject/wallet": "^5.4.0",
"@fontsource/ibm-plex-mono": "^4.5.1",
"@fontsource/inter": "^4.5.1",
"@popperjs/core": "^2.4.4",
"@reduxjs/toolkit": "^1.6.1",
"@uniswap/redux-multicall": "^1.0.0",
"@uniswap/redux-multicall": "^1.1.1",
"@uniswap/router-sdk": "^1.0.3",
"@uniswap/sdk-core": "^3.0.1",
"@uniswap/smart-order-router": "^2.5.20",
"@uniswap/smart-order-router": "^2.5.26",
"@uniswap/token-lists": "^1.0.0-beta.27",
"@uniswap/v2-sdk": "^3.0.1",
"@uniswap/v3-sdk": "^3.8.2",
"@web3-react/core": "8.0.17-beta.0",
"@web3-react/eip1193": "8.0.12-beta.0",
"@web3-react/empty": "8.0.10-beta.0",
"@web3-react/types": "8.0.10-beta.0",
"@web3-react/url": "8.0.12-beta.0",
"@web3-react/core": "^8.0.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",

View File

@@ -37,6 +37,11 @@ function isAsset(source: string) {
return extname && [...ASSET_EXTENSIONS, '.css', '.scss'].includes(extname)
}
function isEthers(source: string) {
// @ethersproject/* modules are provided by ethers, with the exception of experimental.
return source.startsWith('@ethersproject/') && !source.endsWith('experimental')
}
const TS_CONFIG = './tsconfig.lib.json'
// eslint-disable-next-line @typescript-eslint/no-var-requires
const { baseUrl, paths }: CompilerOptions = require(TS_CONFIG).compilerOptions
@@ -60,7 +65,7 @@ const plugins = [
const check = {
input: 'src/lib/index.tsx',
output: { file: 'dist/widgets.tsc', inlineDynamicImports: true },
external: isAsset,
external: (source: string) => isAsset(source) || isEthers(source),
plugins: [
externals({ exclude: ['constants'], deps: true, peerDeps: true }), // marks builtins, dependencies, and peerDependencies external
...plugins,
@@ -72,7 +77,7 @@ const check = {
const type = {
input: 'dist/dts/lib/index.d.ts',
output: { file: 'dist/index.d.ts' },
external: isAsset,
external: (source: string) => isAsset(source) || isEthers(source),
plugins: [
externals({ exclude: ['constants'], deps: true, peerDeps: true }),
dts({ compilerOptions: { baseUrl: 'dist/dts' } }),
@@ -112,6 +117,7 @@ const transpile = {
sourcemap: false,
},
],
external: isEthers,
plugins: [
externals({
exclude: [

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

View File

@@ -8,7 +8,7 @@ import styled, { ThemeContext } from 'styled-components/macro'
import { AbstractConnector } from 'web3-react-abstract-connector'
import { ReactComponent as Close } from '../../assets/images/x.svg'
import { injected, portis, walletlink } from '../../connectors'
import { injected, walletlink } from '../../connectors'
import { SUPPORTED_WALLETS } from '../../constants/wallet'
import { clearAllTransactions } from '../../state/transactions/actions'
import { ExternalLink, LinkStyledButton, ThemedText } from '../../theme'
@@ -181,15 +181,6 @@ function WrappedStatusIcon({ connector }: { connector: AbstractConnector | Conne
return (
<IconWrapper size={16}>
<StatusIcon connector={connector} />
{connector === portis && (
<MainWalletAction
onClick={() => {
portis.portis.showPortis()
}}
>
<Trans>Show Portis</Trans>
</MainWalletAction>
)}
</IconWrapper>
)
}
@@ -210,10 +201,6 @@ const WalletAction = styled(ButtonSecondary)`
}
`
const MainWalletAction = styled(WalletAction)`
color: ${({ theme }) => theme.primary1};
`
function renderTransactions(transactions: string[]) {
return (
<TransactionListWrapper>

View File

@@ -33,6 +33,7 @@ const BLOCKED_ADDRESSES: string[] = [
'0x5512d943ed1f7c8a43f3435c85f7ab68b30121b0',
'0xc455f7fd3e0e12afd51fba5c106909934d8a0e4a',
'0x629e7Da20197a5429d30da36E77d06CdF796b71A',
'0x7FF9cFad3877F21d41Da833E2F775dB0569eE3D9',
]
export default function Blocklist({ children }: { children: ReactNode }) {

View File

@@ -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() {

View File

@@ -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'

View File

@@ -10,7 +10,7 @@ export const FEE_AMOUNT_DETAIL: Record<
[FeeAmount.LOWEST]: {
label: '0.01',
description: <Trans>Best for very stable pairs.</Trans>,
supportedChains: [SupportedChainId.MAINNET],
supportedChains: [SupportedChainId.MAINNET, SupportedChainId.POLYGON, SupportedChainId.POLYGON_MUMBAI],
},
[FeeAmount.LOW]: {
label: '0.05',

View File

@@ -255,8 +255,8 @@ export default function NetworkSelector() {
const handleChainSwitch = useCallback(
(targetChain: number, skipToggle?: boolean) => {
if (!library) return
switchToNetwork({ library, chainId: targetChain })
if (!library?.provider) return
switchToNetwork({ provider: library.provider, chainId: targetChain })
.then(() => {
if (!skipToggle) {
toggle()

View File

@@ -3,9 +3,8 @@ import { AbstractConnector } from 'web3-react-abstract-connector'
import CoinbaseWalletIcon from '../../assets/images/coinbaseWalletIcon.svg'
import FortmaticIcon from '../../assets/images/fortmaticIcon.png'
import PortisIcon from '../../assets/images/portisIcon.png'
import WalletConnectIcon from '../../assets/images/walletConnectIcon.svg'
import { fortmatic, injected, portis, walletconnect, walletlink } from '../../connectors'
import { fortmatic, injected, walletconnect, walletlink } from '../../connectors'
import Identicon from '../Identicon'
export default function StatusIcon({ connector }: { connector: AbstractConnector | Connector }) {
@@ -18,8 +17,6 @@ export default function StatusIcon({ connector }: { connector: AbstractConnector
return <img src={CoinbaseWalletIcon} alt={'Coinbase Wallet'} />
case fortmatic:
return <img src={FortmaticIcon} alt={'Fortmatic'} />
case portis:
return <img src={PortisIcon} alt={'Portis'} />
default:
return null
}

View File

@@ -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 (

View File

@@ -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'

View File

@@ -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'

View File

@@ -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'

View File

@@ -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'

View File

@@ -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'

View File

@@ -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'

View File

@@ -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'

View File

@@ -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'

View File

@@ -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'
@@ -13,7 +13,7 @@ import { WalletConnectConnector } from 'web3-react-walletconnect-connector'
import MetamaskIcon from '../../assets/images/metamask.png'
import { ReactComponent as Close } from '../../assets/images/x.svg'
import { fortmatic, injected, portis } from '../../connectors'
import { fortmatic, injected } from '../../connectors'
import { OVERLAY_READY } from '../../connectors/Fortmatic'
import { SUPPORTED_WALLETS } from '../../constants/wallet'
import usePrevious from '../../hooks/usePrevious'
@@ -228,11 +228,6 @@ export default function WalletModal({
const option = SUPPORTED_WALLETS[key]
// check for mobile options
if (isMobile) {
//disable portis on mobile for now
if (option.connector === portis) {
return null
}
if (!window.web3 && !window.ethereum && option.mobile) {
return (
<Option

View File

@@ -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

View File

@@ -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 } })
}

View File

@@ -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'

View File

@@ -3,7 +3,6 @@ import { SafeAppConnector } from '@gnosis.pm/safe-apps-web3-react'
import { ALL_SUPPORTED_CHAIN_IDS, SupportedChainId } from 'constants/chains'
import { INFURA_NETWORK_URLS } from 'constants/infura'
import { InjectedConnector } from 'web3-react-injected-connector'
import { PortisConnector } from 'web3-react-portis-connector'
import { WalletConnectConnector } from 'web3-react-walletconnect-connector'
import { WalletLinkConnector } from 'web3-react-walletlink-connector'
@@ -13,7 +12,6 @@ import { FortmaticConnector } from './Fortmatic'
import { NetworkConnector } from './NetworkConnector'
const FORMATIC_KEY = process.env.REACT_APP_FORTMATIC_KEY
const PORTIS_ID = process.env.REACT_APP_PORTIS_ID
export const network = new NetworkConnector({
urls: INFURA_NETWORK_URLS,
@@ -43,12 +41,6 @@ export const fortmatic = new FortmaticConnector({
chainId: 1,
})
// mainnet only
export const portis = new PortisConnector({
dAppId: PORTIS_ID ?? '',
networks: [1],
})
export const walletlink = new WalletLinkConnector({
url: INFURA_NETWORK_URLS[SupportedChainId.MAINNET],
appName: 'Uniswap',

View File

@@ -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',

View File

@@ -1,23 +1,86 @@
import { Currency, Ether, NativeCurrency, Token, WETH9 } from '@uniswap/sdk-core'
import {
USDC_ARBITRUM,
USDC_ARBITRUM_RINKEBY,
USDC_GÖRLI,
USDC_KOVAN,
USDC_MAINNET,
USDC_OPTIMISM,
USDC_OPTIMISTIC_KOVAN,
USDC_POLYGON,
USDC_POLYGON_MUMBAI,
USDC_RINKEBY,
USDC_ROPSTEN,
} from '@uniswap/smart-order-router'
import invariant from 'tiny-invariant'
import { UNI_ADDRESS } from './addresses'
import { SupportedChainId } from './chains'
export { USDC_ARBITRUM, USDC_MAINNET, USDC_OPTIMISM, USDC_POLYGON }
export const USDC_MAINNET = new Token(
SupportedChainId.MAINNET,
'0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48',
6,
'USDC',
'USD//C'
)
export const USDC_ROPSTEN = new Token(
SupportedChainId.ROPSTEN,
'0x07865c6e87b9f70255377e024ace6630c1eaa37f',
6,
'USDC',
'USD//C'
)
export const USDC_RINKEBY = new Token(
SupportedChainId.RINKEBY,
'0x4DBCdF9B62e891a7cec5A2568C3F4FAF9E8Abe2b',
6,
'tUSDC',
'test USD//C'
)
export const USDC_GOERLI = new Token(
SupportedChainId.GOERLI,
'0x07865c6e87b9f70255377e024ace6630c1eaa37f',
6,
'USDC',
'USD//C'
)
export const USDC_KOVAN = new Token(
SupportedChainId.KOVAN,
'0x31eeb2d0f9b6fd8642914ab10f4dd473677d80df',
6,
'USDC',
'USD//C'
)
export const USDC_OPTIMISM = new Token(
SupportedChainId.OPTIMISM,
'0x7F5c764cBc14f9669B88837ca1490cCa17c31607',
6,
'USDC',
'USD//C'
)
export const USDC_OPTIMISTIC_KOVAN = new Token(
SupportedChainId.OPTIMISTIC_KOVAN,
'0x3b8e53b3ab8e01fb57d0c9e893bc4d655aa67d84',
6,
'USDC',
'USD//C'
)
export const USDC_ARBITRUM = new Token(
SupportedChainId.ARBITRUM_ONE,
'0xFF970A61A04b1cA14834A43f5dE4533eBDDB5CC8',
6,
'USDC',
'USD//C'
)
export const USDC_ARBITRUM_RINKEBY = new Token(
SupportedChainId.ARBITRUM_RINKEBY,
'0x09b98f8b2395d076514037ff7d39a091a536206c',
6,
'USDC',
'USD//C'
)
export const USDC_POLYGON = new Token(
SupportedChainId.POLYGON,
'0x2791bca1f2de4661ed88a30c99a7a9449aa84174',
6,
'USDC',
'USD//C'
)
export const USDC_POLYGON_MUMBAI = new Token(
SupportedChainId.POLYGON_MUMBAI,
'0xe11a86849d99f524cac3e7a0ec1241828e332c62',
6,
'USDC',
'USD//C'
)
export const AMPL = new Token(
SupportedChainId.MAINNET,
@@ -55,7 +118,7 @@ export const USDC: { [chainId in SupportedChainId]: Token } = {
[SupportedChainId.OPTIMISTIC_KOVAN]: USDC_OPTIMISTIC_KOVAN,
[SupportedChainId.POLYGON]: USDC_POLYGON,
[SupportedChainId.POLYGON_MUMBAI]: USDC_POLYGON_MUMBAI,
[SupportedChainId.GOERLI]: USDC_GÖRLI,
[SupportedChainId.GOERLI]: USDC_GOERLI,
[SupportedChainId.RINKEBY]: USDC_RINKEBY,
[SupportedChainId.KOVAN]: USDC_KOVAN,
[SupportedChainId.ROPSTEN]: USDC_ROPSTEN,
@@ -310,7 +373,7 @@ export const TOKEN_SHORTHANDS: { [shorthand: string]: { [chainId in SupportedCha
[SupportedChainId.OPTIMISTIC_KOVAN]: USDC_OPTIMISTIC_KOVAN.address,
[SupportedChainId.POLYGON]: USDC_POLYGON.address,
[SupportedChainId.POLYGON_MUMBAI]: USDC_POLYGON_MUMBAI.address,
[SupportedChainId.GOERLI]: USDC_GÖRLI.address,
[SupportedChainId.GOERLI]: USDC_GOERLI.address,
[SupportedChainId.RINKEBY]: USDC_RINKEBY.address,
[SupportedChainId.KOVAN]: USDC_KOVAN.address,
[SupportedChainId.ROPSTEN]: USDC_ROPSTEN.address,

View File

@@ -4,9 +4,8 @@ import INJECTED_ICON_URL from '../assets/images/arrow-right.svg'
import COINBASE_ICON_URL from '../assets/images/coinbaseWalletIcon.svg'
import FORTMATIC_ICON_URL from '../assets/images/fortmaticIcon.png'
import METAMASK_ICON_URL from '../assets/images/metamask.png'
import PORTIS_ICON_URL from '../assets/images/portisIcon.png'
import WALLETCONNECT_ICON_URL from '../assets/images/walletConnectIcon.svg'
import { fortmatic, injected, portis, walletconnect, walletlink } from '../connectors'
import { fortmatic, injected, walletconnect, walletlink } from '../connectors'
interface WalletInfo {
connector?: AbstractConnector
@@ -73,13 +72,4 @@ export const SUPPORTED_WALLETS: { [key: string]: WalletInfo } = {
color: '#6748FF',
mobile: true,
},
Portis: {
connector: portis,
name: 'Portis',
iconURL: PORTIS_ICON_URL,
description: 'Login using Portis hosted wallet',
href: null,
color: '#4A6C9B',
mobile: true,
},
}

View File

@@ -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',

View File

@@ -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'

View File

@@ -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 (

View File

@@ -3,7 +3,7 @@ import { SupportedChainId } from 'constants/chains'
import uriToHttp from 'lib/utils/uriToHttp'
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) {

View File

@@ -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,

View File

@@ -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 () => {

View File

@@ -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'

View File

@@ -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

View File

@@ -1,6 +1,6 @@
import { Interface } from '@ethersproject/abi'
import { BigintIsh, Currency, Token } from '@uniswap/sdk-core'
import IUniswapV3PoolStateJson from '@uniswap/v3-core/artifacts/contracts/interfaces/pool/IUniswapV3PoolState.sol/IUniswapV3PoolState.json'
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'
@@ -11,8 +11,6 @@ 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.

View File

@@ -1,7 +1,7 @@
import { Currency, CurrencyAmount, Price, Token, TradeType } from '@uniswap/sdk-core'
import useActiveWeb3React from 'hooks/useActiveWeb3React'
import tryParseCurrencyAmount from 'lib/utils/tryParseCurrencyAmount'
import { useMemo } from 'react'
import { useMemo, useRef } from 'react'
import { SupportedChainId } from '../constants/chains'
import { DAI_OPTIMISM, USDC_ARBITRUM, USDC_MAINNET, USDC_POLYGON } from '../constants/tokens'
@@ -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) {

View File

@@ -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 {

View File

@@ -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]

View File

@@ -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>

View File

@@ -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": [

View File

@@ -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,8 +59,8 @@ export const Overlay = styled(Row)<{ hasAction: boolean }>`
export interface Action {
message: ReactNode
icon?: Icon
onClick: () => void
children: ReactNode
onClick?: () => void
children?: ReactNode
}
export interface BaseProps {
@@ -72,11 +74,13 @@ export default function ActionButton({ color = 'accent', disabled, action, onCli
const textColor = useMemo(() => (color === 'accent' && !disabled ? 'onAccent' : 'currentColor'), [color, disabled])
return (
<Overlay hasAction={Boolean(action)} flex align="stretch">
<StyledButton color={color} disabled={disabled} onClick={action ? action.onClick : onClick}>
<ThemedText.TransitionButton buttonSize={action ? 'medium' : 'large'} color={textColor}>
{action ? action.children : children}
</ThemedText.TransitionButton>
</StyledButton>
{(action ? action.onClick : true) && (
<StyledButton color={color} disabled={disabled} onClick={action?.onClick || onClick}>
<ThemedText.TransitionButton buttonSize={action ? 'medium' : 'large'} color={textColor}>
{action?.children || children}
</ThemedText.TransitionButton>
</StyledButton>
)}
{action && (
<ActionRow gap={0.5}>
<LargeIcon color="currentColor" icon={action.icon || AlertTriangle} />

View File

@@ -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`
@@ -15,17 +15,26 @@ export const BaseButton = styled.button`
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 {
@@ -33,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;
}
`

View File

@@ -1,8 +1,8 @@
import 'wicg-inert'
import useUnmount from 'lib/hooks/useUnmount'
import { X } from 'lib/icons'
import styled, { Color, Layer, ThemeProvider } from 'lib/theme'
import { delayUnmountForAnimation } from 'lib/utils/animations'
import { createContext, ReactElement, ReactNode, useContext, useEffect, useRef, useState } from 'react'
import { createPortal } from 'react-dom'
@@ -97,8 +97,10 @@ export default function Dialog({ color, children, onClose = () => void 0 }: Dial
context.setActive(true)
return () => context.setActive(false)
}, [context])
const dialog = useRef<HTMLDivElement>(null)
useUnmount(dialog)
const modal = useRef<HTMLDivElement>(null)
useEffect(() => delayUnmountForAnimation(modal), [])
useEffect(() => {
const close = (e: KeyboardEvent) => e.key === 'Escape' && onClose?.()
document.addEventListener('keydown', close, true)
@@ -108,9 +110,11 @@ export default function Dialog({ color, children, onClose = () => void 0 }: Dial
context.element &&
createPortal(
<ThemeProvider>
<Modal color={color} ref={dialog}>
<OnCloseContext.Provider value={onClose}>{children}</OnCloseContext.Provider>
</Modal>
<OnCloseContext.Provider value={onClose}>
<Modal color={color} ref={modal}>
{children}
</Modal>
</OnCloseContext.Provider>
</ThemeProvider>,
context.element
)

View File

@@ -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}</>
}

View File

@@ -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>
)

View File

@@ -18,7 +18,7 @@ const ExpandoColumn = styled(Column)<{ height: number; open: boolean }>`
transition: height 0.25s, padding 0.25s;
:after {
background: linear-gradient(#ffffff00, ${({ theme }) => theme.dialog});
background: linear-gradient(transparent, ${({ theme }) => theme.dialog});
bottom: 0;
content: '';
height: 0.75em;

View File

@@ -1,5 +1,7 @@
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;
@@ -34,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
@@ -76,46 +88,23 @@ interface EnforcedNumericInputProps extends NumericInputProps {
enforcer: (nextUserInput: string) => string | null
}
function isNumericallyEqual(a: string, b: string) {
const [aInteger, aDecimal] = toParts(a)
const [bInteger, bDecimal] = toParts(b)
return aInteger === bInteger && aDecimal === bDecimal
function toParts(num: string) {
let [integer, decimal] = num.split('.')
integer = integer?.match(/([1-9]\d*)/)?.[1] || ''
decimal = decimal?.match(/(\d*[1-9])/)?.[1] || ''
return [integer, decimal]
}
}
const NumericInput = forwardRef<HTMLInputElement, EnforcedNumericInputProps>(function NumericInput(
{ 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"

View File

@@ -80,7 +80,7 @@ export default function Input({ disabled, focused }: InputProps) {
// extract eagerly in case of reversal
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

View File

@@ -46,7 +46,7 @@ export default function Output({ disabled, focused, children }: PropsWithChildre
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
@@ -83,7 +83,7 @@ export default function Output({ disabled, focused, children }: PropsWithChildre
<Row>
<USDC gap={0.5} isLoading={isRouteLoading}>
{outputUSDC ? `$${formatCurrencyAmount(outputUSDC, 6, 'en', 2)}` : '-'}{' '}
{impact.display && <ThemedText.Body2 color={impact.warning}>({impact.display})</ThemedText.Body2>}
{impact && <ThemedText.Body2 color={impact.warning}>({impact.toString()})</ThemedText.Body2>}
</USDC>
{balance && (
<Balance focused={focused}>

View File

@@ -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;

View File

@@ -1,6 +1,7 @@
import { Trans } from '@lingui/macro'
import ErrorDialog, { StatusHeader } from 'lib/components/Error/ErrorDialog'
import EtherscanLink from 'lib/components/EtherscanLink'
import Rule from 'lib/components/Rule'
import SwapSummary from 'lib/components/Swap/Summary'
import useInterval from 'lib/hooks/useInterval'
import { CheckCircle, Clock, Spinner } from 'lib/icons'
@@ -84,6 +85,7 @@ function TransactionStatus({ tx, onClose }: TransactionStatusProps) {
<SwapSummary input={tx.info.inputCurrencyAmount} output={tx.info.outputCurrencyAmount} />
) : null}
</StatusHeader>
<Rule />
<TransactionRow flex>
<ThemedText.ButtonSmall>
<EtherscanLink type={ExplorerDataType.TRANSACTION} data={tx.info.response.hash}>

View File

@@ -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'
@@ -41,7 +41,7 @@ function Fixture() {
return trade ? (
<Modal color="dialog">
<SummaryDialog
onConfirm={() => void 0}
onConfirm={async () => void 0}
trade={trade}
slippage={slippage}
inputUSDC={inputUSDC}
@@ -54,7 +54,8 @@ function Fixture() {
export default (
<>
<SwapInfoUpdater />
<Fixture />
<SwapInfoProvider>
<Fixture />
</SwapInfoProvider>
</>
)

View File

@@ -1,10 +1,12 @@
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 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'
@@ -36,11 +38,11 @@ function Detail({ label, value, color }: DetailProps) {
interface DetailsProps {
trade: Trade<Currency, Currency, TradeType>
slippage: { auto: boolean; allowed: Percent; warning?: Color }
priceImpact: { priceImpact?: string; warning?: Color }
slippage: Slippage
impact?: PriceImpact
}
export default function Details({ trade, slippage, priceImpact }: DetailsProps) {
export default function Details({ trade, slippage, impact }: DetailsProps) {
const { inputAmount, outputAmount } = trade
const inputCurrency = inputAmount.currency
const outputCurrency = outputAmount.currency
@@ -61,8 +63,8 @@ export default function Details({ trade, slippage, priceImpact }: DetailsProps)
}
}
if (priceImpact.priceImpact) {
rows.push([t`Price impact`, priceImpact.priceImpact, priceImpact.warning])
if (impact) {
rows.push([t`Price impact`, impact.toString(), impact.warning])
}
if (lpFeeAmount) {
@@ -85,15 +87,15 @@ export default function Details({ trade, slippage, priceImpact }: DetailsProps)
return rows
}, [
feeOptions,
priceImpact,
lpFeeAmount,
trade,
slippage,
outputAmount,
i18n.locale,
integrator,
outputCurrency,
impact,
inputCurrency,
integrator,
lpFeeAmount,
outputAmount,
outputCurrency,
slippage,
trade,
])
return (

View File

@@ -42,16 +42,16 @@ interface SummaryProps {
output: CurrencyAmount<Currency>
inputUSDC?: CurrencyAmount<Currency>
outputUSDC?: CurrencyAmount<Currency>
priceImpact?: PriceImpact
impact?: PriceImpact
}
export default function Summary({ input, output, inputUSDC, outputUSDC, priceImpact }: SummaryProps) {
export default function Summary({ input, output, inputUSDC, outputUSDC, impact }: SummaryProps) {
return (
<Row gap={priceImpact ? 1 : 0.25}>
<Row gap={impact ? 1 : 0.25}>
<TokenValue input={input} usdc={inputUSDC} />
<ArrowRight />
<TokenValue input={output} usdc={outputUSDC}>
{priceImpact && <ThemedText.Caption color={priceImpact.warning}>({priceImpact.display})</ThemedText.Caption>}
{impact && <ThemedText.Caption color={impact.warning}>({impact.toString()})</ThemedText.Caption>}
</TokenValue>
</Row>
)

View File

@@ -9,8 +9,8 @@ import Expando from 'lib/components/Expando'
import Row from 'lib/components/Row'
import { Slippage } from 'lib/hooks/useSlippage'
import { PriceImpact } from 'lib/hooks/useUSDCPriceImpact'
import { AlertTriangle, BarChart, Info } from 'lib/icons'
import styled, { Color, ThemedText } from 'lib/theme'
import { AlertTriangle, BarChart, Info, Spinner } from 'lib/icons'
import styled, { ThemedText } from 'lib/theme'
import { useCallback, useMemo, useState } from 'react'
import { formatCurrencyAmount } from 'utils/formatCurrencyAmount'
import { tradeMeaningfullyDiffers } from 'utils/tradeMeaningFullyDiffer'
@@ -41,16 +41,16 @@ const Body = styled(Column)<{ open: boolean }>`
}
`
function Subhead({ priceImpact, slippage }: { priceImpact: { warning?: Color }; slippage: Slippage }) {
function Subhead({ impact, slippage }: { impact?: PriceImpact; slippage: Slippage }) {
return (
<Row gap={0.5}>
{priceImpact.warning || slippage.warning ? (
<AlertTriangle color={priceImpact.warning || slippage.warning} />
{impact?.warning || slippage.warning ? (
<AlertTriangle color={impact?.warning || slippage.warning} />
) : (
<Info color="secondary" />
)}
<ThemedText.Subhead2 color={priceImpact.warning || slippage.warning || 'secondary'}>
{priceImpact.warning ? (
<ThemedText.Subhead2 color={impact?.warning || slippage.warning || 'secondary'}>
{impact?.warning ? (
<Trans>High price impact</Trans>
) : slippage.warning ? (
<Trans>High slippage</Trans>
@@ -94,16 +94,27 @@ function ConfirmButton({
}: {
trade: Trade<Currency, Currency, TradeType>
highPriceImpact: boolean
onConfirm: () => void
onConfirm: () => Promise<void>
}) {
const [ackPriceImpact, setAckPriceImpact] = useState(false)
const [ackTrade, setAckTrade] = useState(trade)
const doesTradeDiffer = useMemo(
() => Boolean(trade && ackTrade && tradeMeaningfullyDiffers(trade, ackTrade)),
[ackTrade, trade]
)
const [isPending, setIsPending] = useState(false)
const onClick = useCallback(async () => {
setIsPending(true)
await onConfirm()
setIsPending(false)
}, [onConfirm])
const action = useMemo((): Action | undefined => {
if (doesTradeDiffer) {
if (isPending) {
return { message: <Trans>Confirm in your wallet</Trans>, icon: Spinner }
} else if (doesTradeDiffer) {
return {
message: <Trans>Price updated</Trans>,
icon: BarChart,
@@ -118,10 +129,10 @@ function ConfirmButton({
}
}
return
}, [ackPriceImpact, doesTradeDiffer, highPriceImpact, trade])
}, [ackPriceImpact, doesTradeDiffer, highPriceImpact, isPending, trade])
return (
<ActionButton onClick={onConfirm} action={action}>
<ActionButton onClick={onClick} action={action}>
<Trans>Confirm swap</Trans>
</ActionButton>
)
@@ -132,8 +143,8 @@ interface SummaryDialogProps {
slippage: Slippage
inputUSDC?: CurrencyAmount<Currency>
outputUSDC?: CurrencyAmount<Currency>
impact: PriceImpact
onConfirm: () => void
impact?: PriceImpact
onConfirm: () => Promise<void>
}
export function SummaryDialog({ trade, slippage, inputUSDC, outputUSDC, impact, onConfirm }: SummaryDialogProps) {
@@ -152,23 +163,18 @@ export function SummaryDialog({ trade, slippage, inputUSDC, outputUSDC, impact,
output={outputAmount}
inputUSDC={inputUSDC}
outputUSDC={outputUSDC}
priceImpact={impact}
impact={impact}
/>
<Price trade={trade} />
</Heading>
<Column gap={open ? 0 : 0.75} style={{ transition: 'gap 0.25s' }}>
<Expando
title={<Subhead priceImpact={impact} slippage={slippage} />}
open={open}
onExpand={onExpand}
height={7}
>
<Details trade={trade} slippage={slippage} priceImpact={impact} />
<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} />
<ConfirmButton trade={trade} highPriceImpact={impact?.warning === 'error'} onConfirm={onConfirm} />
</Column>
</Body>
</>

View File

@@ -1,4 +1,3 @@
import { tokens } from '@uniswap/default-token-list'
import { DAI, USDC_MAINNET } from 'constants/tokens'
import { useUpdateAtom } from 'jotai/utils'
import { useEffect } from 'react'
@@ -65,7 +64,6 @@ function Fixture() {
defaultInputAmount={defaultInputAmount}
defaultOutputTokenAddress={optionsToAddressMap[defaultOutputToken]}
defaultOutputAmount={defaultOutputAmount}
tokenList={tokens}
onConnectWallet={() => console.log('onConnectWallet')} // this handler is included as a test of functionality, but only logs
/>
)

View File

@@ -1,260 +0,0 @@
import { Trans } from '@lingui/macro'
import { Token } from '@uniswap/sdk-core'
import { useAtomValue, useUpdateAtom } from 'jotai/utils'
import { WrapErrorText } from 'lib/components/Swap/WrapErrorText'
import { useSwapCurrencyAmount, useSwapInfo, useSwapTradeType } from 'lib/hooks/swap'
import {
ApproveOrPermitState,
useApproveOrPermit,
useSwapApprovalOptimizedTrade,
useSwapRouterAddress,
} from 'lib/hooks/swap/useSwapApproval'
import { useSwapCallback } from 'lib/hooks/swap/useSwapCallback'
import useWrapCallback, { WrapError, WrapType } from 'lib/hooks/swap/useWrapCallback'
import { useAddTransaction, usePendingApproval } from 'lib/hooks/transactions'
import useActiveWeb3React from 'lib/hooks/useActiveWeb3React'
import useTransactionDeadline from 'lib/hooks/useTransactionDeadline'
import { Spinner } from 'lib/icons'
import { displayTxHashAtom, feeOptionsAtom, Field } from 'lib/state/swap'
import { TransactionType } from 'lib/state/transactions'
import { useTheme } from 'lib/theme'
import { memo, useCallback, useEffect, useMemo, useState } from 'react'
import invariant from 'tiny-invariant'
import { ExplorerDataType } from 'utils/getExplorerLink'
import ActionButton, { ActionButtonProps } from '../ActionButton'
import Dialog from '../Dialog'
import EtherscanLink from '../EtherscanLink'
import { SummaryDialog } from './Summary'
interface SwapButtonProps {
disabled?: boolean
}
function useIsPendingApproval(token?: Token, spender?: string): boolean {
return Boolean(usePendingApproval(token, spender))
}
export default memo(function SwapButton({ disabled }: SwapButtonProps) {
const { account, chainId } = useActiveWeb3React()
const { tokenColorExtraction } = useTheme()
const {
[Field.INPUT]: {
currency: inputCurrency,
amount: inputTradeCurrencyAmount,
balance: inputCurrencyBalance,
usdc: inputUSDC,
},
[Field.OUTPUT]: { amount: outputTradeCurrencyAmount, usdc: outputUSDC },
trade,
slippage,
impact,
} = useSwapInfo()
const feeOptions = useAtomValue(feeOptionsAtom)
const tradeType = useSwapTradeType()
const [activeTrade, setActiveTrade] = useState<typeof trade.trade | undefined>()
useEffect(() => {
setActiveTrade((activeTrade) => activeTrade && trade.trade)
}, [trade])
// clear active trade on chain change
useEffect(() => {
setActiveTrade(undefined)
}, [chainId])
// TODO(zzmp): Return an optimized trade directly from useSwapInfo.
const optimizedTrade =
// Use trade.trade if there is no swap optimized trade. This occurs if approvals are still pending.
useSwapApprovalOptimizedTrade(trade.trade, slippage.allowed, useIsPendingApproval) || trade.trade
const approvalCurrencyAmount = useSwapCurrencyAmount(Field.INPUT)
const { approvalState, signatureData, handleApproveOrPermit } = useApproveOrPermit(
optimizedTrade,
slippage.allowed,
useIsPendingApproval,
approvalCurrencyAmount
)
const approvalHash = usePendingApproval(
inputCurrency?.isToken ? inputCurrency : undefined,
useSwapRouterAddress(optimizedTrade)
)
const addTransaction = useAddTransaction()
const onApprove = useCallback(() => {
handleApproveOrPermit().then((transaction) => {
if (transaction) {
addTransaction({ type: TransactionType.APPROVAL, ...transaction })
}
})
}, [addTransaction, handleApproveOrPermit])
const { type: wrapType, callback: wrapCallback, error: wrapError, loading: wrapLoading } = useWrapCallback()
const disableSwap = useMemo(
() =>
disabled ||
!optimizedTrade ||
!chainId ||
wrapLoading ||
(wrapType !== WrapType.NOT_APPLICABLE && wrapError) ||
approvalState === ApproveOrPermitState.PENDING_SIGNATURE ||
!(inputTradeCurrencyAmount && inputCurrencyBalance) ||
inputCurrencyBalance.lessThan(inputTradeCurrencyAmount),
[
disabled,
optimizedTrade,
chainId,
wrapLoading,
wrapType,
wrapError,
approvalState,
inputTradeCurrencyAmount,
inputCurrencyBalance,
]
)
const actionProps = useMemo((): Partial<ActionButtonProps> | undefined => {
if (disableSwap) {
return { disabled: true }
}
if (
wrapType === WrapType.NOT_APPLICABLE &&
(approvalState === ApproveOrPermitState.REQUIRES_APPROVAL ||
approvalState === ApproveOrPermitState.REQUIRES_SIGNATURE)
) {
const currency = inputCurrency || approvalCurrencyAmount?.currency
invariant(currency)
return {
action: {
message:
approvalState === ApproveOrPermitState.REQUIRES_SIGNATURE ? (
<Trans>Allow {currency.symbol} first</Trans>
) : (
<Trans>Approve {currency.symbol} first</Trans>
),
onClick: onApprove,
children:
approvalState === ApproveOrPermitState.REQUIRES_SIGNATURE ? <Trans>Allow</Trans> : <Trans>Approve</Trans>,
},
}
}
if (approvalState === ApproveOrPermitState.PENDING_APPROVAL) {
return {
disabled: true,
action: {
message: (
<EtherscanLink type={ExplorerDataType.TRANSACTION} data={approvalHash}>
<Trans>Approval pending</Trans>
</EtherscanLink>
),
icon: Spinner,
onClick: () => void 0, // @TODO: should not require an onclick
children: <Trans>Approve</Trans>,
},
}
}
return {}
}, [approvalCurrencyAmount?.currency, approvalHash, approvalState, disableSwap, inputCurrency, onApprove, wrapType])
const deadline = useTransactionDeadline()
// the callback to execute the swap
const { callback: swapCallback } = useSwapCallback({
trade: optimizedTrade,
allowedSlippage: slippage.allowed,
recipientAddressOrName: account ?? null,
signatureData,
deadline,
feeOptions,
})
//@TODO(ianlapham): add a loading state, process errors
const setDisplayTxHash = useUpdateAtom(displayTxHashAtom)
const onConfirm = useCallback(() => {
swapCallback?.()
.then((response) => {
setDisplayTxHash(response.hash)
invariant(inputTradeCurrencyAmount && outputTradeCurrencyAmount)
addTransaction({
response,
type: TransactionType.SWAP,
tradeType,
inputCurrencyAmount: inputTradeCurrencyAmount,
outputCurrencyAmount: outputTradeCurrencyAmount,
})
})
.catch((error) => {
//@TODO(ianlapham): add error handling
console.log(error)
})
.finally(() => {
setActiveTrade(undefined)
})
}, [addTransaction, inputTradeCurrencyAmount, outputTradeCurrencyAmount, setDisplayTxHash, swapCallback, tradeType])
const ButtonText = useCallback(() => {
if ((wrapType === WrapType.WRAP || wrapType === WrapType.UNWRAP) && wrapError !== WrapError.NO_ERROR) {
return <WrapErrorText wrapError={wrapError} />
}
switch (wrapType) {
case WrapType.UNWRAP:
return <Trans>Unwrap</Trans>
case WrapType.WRAP:
return <Trans>Wrap</Trans>
case WrapType.NOT_APPLICABLE:
default:
return <Trans>Review swap</Trans>
}
}, [wrapError, wrapType])
const handleDialogClose = useCallback(() => {
setActiveTrade(undefined)
}, [])
const handleActionButtonClick = useCallback(async () => {
if (wrapType === WrapType.NOT_APPLICABLE) {
setActiveTrade(trade.trade)
} else {
const transaction = await wrapCallback()
addTransaction({
response: transaction,
type: TransactionType.WRAP,
unwrapped: wrapType === WrapType.UNWRAP,
currencyAmountRaw: transaction.value?.toString() ?? '0',
chainId,
})
setDisplayTxHash(transaction.hash)
}
}, [addTransaction, chainId, setDisplayTxHash, trade.trade, wrapCallback, wrapType])
return (
<>
<ActionButton
color={tokenColorExtraction ? 'interactive' : 'accent'}
onClick={handleActionButtonClick}
{...actionProps}
>
<ButtonText />
</ActionButton>
{activeTrade && (
<Dialog color="dialog" onClose={handleDialogClose}>
<SummaryDialog
trade={activeTrade}
slippage={slippage}
inputUSDC={inputUSDC}
outputUSDC={outputUSDC}
impact={impact}
onConfirm={onConfirm}
/>
</Dialog>
)}
</>
)
})

View 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>
)}
</>
)
})

View 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 }
}

View File

@@ -18,7 +18,8 @@ const TokenInputRow = styled(Row)`
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)};

View File

@@ -4,7 +4,6 @@ import Column from 'lib/components/Column'
import Rule from 'lib/components/Rule'
import Tooltip from 'lib/components/Tooltip'
import { loadingCss } from 'lib/css/loading'
import { WrapType } from 'lib/hooks/swap/useWrapCallback'
import { PriceImpact } from 'lib/hooks/useUSDCPriceImpact'
import { AlertTriangle, Icon, Info, InlineSpinner } from 'lib/icons'
import styled, { ThemedText } from 'lib/theme'
@@ -33,12 +32,25 @@ 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 }) {
@@ -49,6 +61,10 @@ export function InsufficientLiquidity() {
return <Caption caption={<Trans>Insufficient liquidity in the pool for your trade</Trans>} />
}
export function Error() {
return <Caption caption={<Trans>Error fetching trade</Trans>} />
}
export function Empty() {
return <Caption icon={Info} caption={<Trans>Enter an amount</Trans>} />
}
@@ -66,15 +82,17 @@ export function LoadingTrade() {
)
}
export function WrapCurrency({ loading, wrapType }: { loading: boolean; wrapType: WrapType.UNWRAP | WrapType.WRAP }) {
const WrapText = useCallback(() => {
if (wrapType === WrapType.WRAP) {
return loading ? <Trans>Wrapping native currency.</Trans> : <Trans>Wrap native currency.</Trans>
}
return loading ? <Trans>Unwrapping native currency.</Trans> : <Trans>Unwrap native currency.</Trans>
}, [loading, wrapType])
export function WrapCurrency({ inputCurrency, outputCurrency }: { inputCurrency: Currency; outputCurrency: Currency }) {
const Text = useCallback(
() => (
<Trans>
Convert {inputCurrency.symbol} to {outputCurrency.symbol} with no slippage
</Trans>
),
[inputCurrency.symbol, outputCurrency.symbol]
)
return <Caption icon={Info} caption={<WrapText />} />
return <Caption icon={Info} caption={<Text />} />
}
export function Trade({
@@ -84,16 +102,17 @@ export function Trade({
}: {
trade: InterfaceTrade<Currency, Currency, TradeType>
outputUSDC?: CurrencyAmount<Currency>
impact: PriceImpact
impact?: PriceImpact
}) {
return (
<>
<Tooltip placement="bottom" icon={impact.warning ? AlertTriangle : Info}>
<Tooltip placement="bottom" icon={impact?.warning ? AlertTriangle : Info}>
<Column gap={0.75}>
{impact.warning && (
{impact?.warning && (
<>
<ThemedText.Caption>
The output amount is estimated at {impact.display} less than the input amount due to high price impact
The output amount is estimated at {impact.toString()} less than the input amount due to high price
impact
</ThemedText.Caption>
<Rule />
</>

View File

@@ -17,57 +17,61 @@ const ToolbarRow = styled(Row)`
${largeIconCss}
`
export default memo(function Toolbar({ disabled }: { disabled?: boolean }) {
const { chainId } = useActiveWeb3React()
export default memo(function Toolbar() {
const { active, activating, chainId } = useActiveWeb3React()
const {
[Field.INPUT]: { currency: inputCurrency, balance },
[Field.INPUT]: { currency: inputCurrency, balance: inputBalance, amount: inputAmount },
[Field.OUTPUT]: { currency: outputCurrency, usdc: outputUSDC },
trade: { trade, state },
impact,
} = useSwapInfo()
const isRouteLoading = state === TradeState.SYNCING || state === TradeState.LOADING
const isAmountPopulated = useIsAmountPopulated()
const { type: wrapType, loading: wrapLoading } = useWrapCallback()
const { type: wrapType } = useWrapCallback()
const caption = useMemo(() => {
if (disabled) {
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 (wrapType !== WrapType.NOT_APPLICABLE) {
return <Caption.WrapCurrency wrapType={wrapType} loading={wrapLoading} />
}
if (isRouteLoading) {
if (state === TradeState.SYNCING || state === TradeState.LOADING) {
return <Caption.LoadingTrade />
}
if (!trade?.swaps) {
if (inputBalance && inputAmount?.greaterThan(inputBalance)) {
return <Caption.InsufficientBalance currency={inputCurrency} />
}
if (wrapType !== WrapType.NONE) {
return <Caption.WrapCurrency inputCurrency={inputCurrency} outputCurrency={outputCurrency} />
}
if (state === TradeState.NO_ROUTE_FOUND || (trade && !trade.swaps)) {
return <Caption.InsufficientLiquidity />
}
if (balance && trade?.inputAmount.greaterThan(balance)) {
return <Caption.InsufficientBalance currency={trade.inputAmount.currency} />
}
if (trade.inputAmount && trade.outputAmount) {
if (trade?.inputAmount && trade.outputAmount) {
return <Caption.Trade trade={trade} outputUSDC={outputUSDC} impact={impact} />
}
if (state === TradeState.INVALID) {
return <Caption.Error />
}
}
return <Caption.Empty />
}, [
balance,
activating,
active,
chainId,
disabled,
impact,
inputAmount,
inputBalance,
inputCurrency,
isAmountPopulated,
isRouteLoading,
outputCurrency,
outputUSDC,
state,
trade,
wrapLoading,
wrapType,
])

View File

@@ -1,22 +0,0 @@
import { Trans } from '@lingui/macro'
import { WrapError } from 'lib/hooks/swap/useWrapCallback'
import useNativeCurrency from 'lib/hooks/useNativeCurrency'
export function WrapErrorText({ wrapError }: { wrapError: WrapError }) {
const native = useNativeCurrency()
const wrapped = native?.wrapped
switch (wrapError) {
case WrapError.ENTER_NATIVE_AMOUNT:
return <Trans>Enter {native?.symbol} amount</Trans>
case WrapError.ENTER_WRAPPED_AMOUNT:
return <Trans>Enter {wrapped?.symbol} amount</Trans>
case WrapError.INSUFFICIENT_NATIVE_BALANCE:
return <Trans>Insufficient {native?.symbol} balance</Trans>
case WrapError.INSUFFICIENT_WRAPPED_BALANCE:
return <Trans>Insufficient {wrapped?.symbol} balance</Trans>
case WrapError.NO_ERROR:
default:
return null
}
}

View File

@@ -1,17 +1,15 @@
import { Trans } from '@lingui/macro'
import { TokenInfo } from '@uniswap/token-lists'
import { useAtom } from 'jotai'
import { SwapInfoUpdater } from 'lib/hooks/swap/useSwapInfo'
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 useOnSupportedNetwork from 'lib/hooks/useOnSupportedNetwork'
import useTokenList, { useSyncTokenList } from 'lib/hooks/useTokenList'
import { displayTxHashAtom } from 'lib/state/swap'
import { SwapTransactionInfo, Transaction, TransactionType, WrapTransactionInfo } from 'lib/state/transactions'
import { useMemo, useState } from 'react'
import { useState } from 'react'
import Dialog from '../Dialog'
import Header from '../Header'
@@ -23,8 +21,8 @@ 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'
function getTransactionFromMap(
txs: { [hash: string]: Transaction },
@@ -43,14 +41,13 @@ function getTransactionFromMap(
}
export interface SwapProps extends TokenDefaults, FeeOptions {
tokenList?: string | TokenInfo[]
onConnectWallet?: () => void
}
export default function Swap(props: SwapProps) {
useSyncTokenList(props.tokenList)
useSyncTokenDefaults(props)
useValidate(props)
useSyncConvenienceFee(props)
useSyncTokenDefaults(props)
const { active, account } = useActiveWeb3React()
const [wrapper, setWrapper] = useState<HTMLDivElement | null>(null)
@@ -59,32 +56,27 @@ export default function Swap(props: SwapProps) {
const pendingTxs = usePendingTransactions()
const displayTx = getTransactionFromMap(pendingTxs, displayTxHash)
const tokenList = useTokenList()
const onSupportedNetwork = useOnSupportedNetwork()
const isSwapSupported = useMemo(
() => Boolean(active && onSupportedNetwork && tokenList?.length),
[active, onSupportedNetwork, tokenList?.length]
)
const isDisabled = !(active && onSupportedNetwork)
const focused = useHasFocus(wrapper)
const isInteractive = Boolean(active && onSupportedNetwork)
return (
<SwapPropValidator {...props}>
{isSwapSupported && <SwapInfoUpdater />}
<>
<Header title={<Trans>Swap</Trans>}>
{active && <Wallet disabled={!account} onClick={props.onConnectWallet} />}
<Settings disabled={!isInteractive} />
<Settings disabled={isDisabled} />
</Header>
<div ref={setWrapper}>
<BoundaryProvider value={wrapper}>
<Input disabled={!isInteractive} focused={focused} />
<ReverseButton disabled={!isInteractive} />
<Output disabled={!isInteractive} focused={focused}>
<Toolbar disabled={!active} />
<SwapButton disabled={!isSwapSupported} />
</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 && (
@@ -92,6 +84,6 @@ export default function Swap(props: SwapProps) {
<StatusDialog tx={displayTx} onClose={() => setDisplayTxHash()} />
</Dialog>
)}
</SwapPropValidator>
</>
)
}

View File

@@ -1,4 +1,3 @@
import { BigNumber } from '@ethersproject/bignumber'
import { IntegrationError } from 'lib/errors'
import { FeeOptions } from 'lib/hooks/swap/useSyncConvenienceFee'
import { DefaultAddress, TokenDefaults } from 'lib/hooks/swap/useSyncTokenDefaults'
@@ -18,12 +17,12 @@ function isAddressOrAddressMap(addressOrMap: DefaultAddress): boolean {
type ValidatorProps = PropsWithChildren<TokenDefaults & FeeOptions>
export default function SwapPropValidator(props: ValidatorProps) {
export default function useValidate(props: ValidatorProps) {
const { convenienceFee, convenienceFeeRecipient } = props
useEffect(() => {
if (convenienceFee) {
if (convenienceFee > 100 || convenienceFee < 0) {
throw new IntegrationError(`convenienceFee must be between 0 and 100. (You set it to ${convenienceFee})`)
throw new IntegrationError(`convenienceFee must be between 0 and 100 (you set it to ${convenienceFee}).`)
}
if (!convenienceFeeRecipient) {
throw new IntegrationError('convenienceFeeRecipient is required when convenienceFee is set.')
@@ -32,7 +31,7 @@ export default function SwapPropValidator(props: ValidatorProps) {
if (typeof convenienceFeeRecipient === 'string') {
if (!isAddress(convenienceFeeRecipient)) {
throw new IntegrationError(
`convenienceFeeRecipient must be a valid address. (You set it to ${convenienceFeeRecipient}.)`
`convenienceFeeRecipient must be a valid address (you set it to ${convenienceFeeRecipient}).`
)
}
} else if (typeof convenienceFeeRecipient === 'object') {
@@ -40,7 +39,7 @@ export default function SwapPropValidator(props: ValidatorProps) {
if (!isAddress(recipient)) {
const values = Object.values(convenienceFeeRecipient).join(', ')
throw new IntegrationError(
`All values in convenienceFeeRecipient object must be valid addresses. (You used ${values}.)`
`All values in convenienceFeeRecipient object must be valid addresses (you used ${values}).`
)
}
})
@@ -48,26 +47,30 @@ export default function SwapPropValidator(props: ValidatorProps) {
}
}, [convenienceFee, convenienceFeeRecipient])
const { defaultInputTokenAddress, defaultInputAmount, defaultOutputTokenAddress, defaultOutputAmount } = props
const { defaultInputAmount, defaultOutputAmount } = props
useEffect(() => {
if (defaultOutputAmount && defaultInputAmount) {
throw new IntegrationError('defaultInputAmount and defaultOutputAmount may not both be defined.')
}
if (defaultInputAmount && BigNumber.from(defaultInputAmount).lt(0)) {
throw new IntegrationError(`defaultInputAmount must be a positive number. (You set it to ${defaultInputAmount})`)
if (defaultInputAmount && (isNaN(+defaultInputAmount) || defaultInputAmount < 0)) {
throw new IntegrationError(`defaultInputAmount must be a positive number (you set it to ${defaultInputAmount})`)
}
if (defaultOutputAmount && BigNumber.from(defaultOutputAmount).lt(0)) {
if (defaultOutputAmount && (isNaN(+defaultOutputAmount) || defaultOutputAmount < 0)) {
throw new IntegrationError(
`defaultOutputAmount must be a positive number. (You set it to ${defaultOutputAmount})`
`defaultOutputAmount must be a positive number (you set it to ${defaultOutputAmount}).`
)
}
}, [defaultInputAmount, defaultOutputAmount])
const { defaultInputTokenAddress, defaultOutputTokenAddress } = props
useEffect(() => {
if (
defaultInputTokenAddress &&
!isAddressOrAddressMap(defaultInputTokenAddress) &&
defaultInputTokenAddress !== 'NATIVE'
) {
throw new IntegrationError(
`defaultInputTokenAddress(es) must be a valid address or "NATIVE". (You set it to ${defaultInputTokenAddress}`
`defaultInputTokenAddress must be a valid address or "NATIVE" (you set it to ${defaultInputTokenAddress}).`
)
}
if (
@@ -76,10 +79,8 @@ export default function SwapPropValidator(props: ValidatorProps) {
defaultOutputTokenAddress !== 'NATIVE'
) {
throw new IntegrationError(
`defaultOutputTokenAddress(es) must be a valid address or "NATIVE". (You set it to ${defaultOutputTokenAddress}`
`defaultOutputTokenAddress must be a valid address or "NATIVE" (you set it to ${defaultOutputTokenAddress}).`
)
}
}, [defaultInputTokenAddress, defaultInputAmount, defaultOutputTokenAddress, defaultOutputAmount])
return <>{props.children}</>
}, [defaultInputTokenAddress, defaultOutputTokenAddress])
}

View File

@@ -3,7 +3,7 @@ import { useToken } from 'lib/hooks/useCurrency'
import useCurrencyLogoURIs from 'lib/hooks/useCurrencyLogoURIs'
import { MissingToken } from 'lib/icons'
import styled from 'lib/theme'
import { useCallback, useEffect, useState } from 'react'
import { useCallback, useMemo, useState } from 'react'
const badSrcs = new Set<string>()
@@ -17,20 +17,26 @@ function TokenImg({ token, ...rest }: TokenImgProps) {
// Use the wrapped token info so that it includes the logoURI.
const tokenInfo = useToken(token.isToken ? token.wrapped.address : undefined) ?? token
// TODO(zzmp): TokenImg takes a frame to switch.
const srcs = useCurrencyLogoURIs(tokenInfo)
const [src, setSrc] = useState<string | undefined>()
useEffect(() => {
setSrc(srcs.find((src) => !badSrcs.has(src)))
}, [srcs])
const onError = useCallback(() => {
if (src) badSrcs.add(src)
setSrc(srcs.find((src) => !badSrcs.has(src)))
}, [src, srcs])
if (src) {
return <img src={src} alt={tokenInfo.name || tokenInfo.symbol} onError={onError} {...rest} />
}
return <MissingToken color="secondary" {...rest} />
const [attempt, setAttempt] = useState(0)
const onError = useCallback((e) => {
if (e.target.src) badSrcs.add(e.target.src)
setAttempt((attempt) => ++attempt)
}, [])
const src = useMemo(() => {
// Trigger a re-render when an error occurs.
void attempt
return srcs.find((src) => !badSrcs.has(src))
}, [attempt, srcs])
if (!src) return <MissingToken color="secondary" {...rest} />
const alt = tokenInfo.name || tokenInfo.symbol
return <img src={src} alt={alt} key={alt} onError={onError} {...rest} />
}
export default styled(TokenImg)<{ size?: number }>`

View File

@@ -1,15 +1,15 @@
import DEFAULT_TOKEN_LIST from '@uniswap/default-token-list'
import { useSyncTokenList } from 'lib/hooks/useTokenList'
import { TokenListProvider } from 'lib/hooks/useTokenList'
import { Modal } from './Dialog'
import { TokenSelectDialog } from './TokenSelect'
export default function Fixture() {
useSyncTokenList(DEFAULT_TOKEN_LIST.tokens)
return (
<Modal color="module">
<TokenSelectDialog onSelect={() => void 0} />
<TokenListProvider list={DEFAULT_TOKEN_LIST.tokens}>
<TokenSelectDialog onSelect={() => void 0} />
</TokenListProvider>
</Modal>
)
}

View File

@@ -1,29 +1,33 @@
import { Trans } from '@lingui/macro'
import { Currency } from '@uniswap/sdk-core'
import { ChevronDown } from 'lib/icons'
import styled, { ThemedText } from 'lib/theme'
import { useMemo } from 'react'
import styled, { css, ThemedText } from 'lib/theme'
import { useEffect, useMemo, useState } from 'react'
import Button from '../Button'
import Row from '../Row'
import TokenImg from '../TokenImg'
const StyledTokenButton = styled(Button)<{ empty?: boolean }>`
const transitionCss = css`
transition: background-color 0.125s linear, border-color 0.125s linear, filter 0.125s linear, width 0.125s ease-out;
`
const StyledTokenButton = styled(Button)`
border-radius: ${({ theme }) => theme.borderRadius}em;
padding: 0.25em;
padding-left: ${({ empty }) => (empty ? 0.75 : 0.25)}em;
:disabled {
// prevents border from expanding the button's box size
padding: calc(0.25em - 1px);
padding-left: calc(${({ empty }) => (empty ? 0.75 : 0.25)}em - 1px);
:enabled {
${({ transition }) => transition && transitionCss};
}
`
const TokenButtonRow = styled(Row)<{ collapsed: boolean }>`
const TokenButtonRow = styled(Row)<{ empty: boolean; collapsed: boolean }>`
float: right;
height: 1.2em;
// max-width must have an absolute value in order to transition.
max-width: ${({ collapsed }) => (collapsed ? '1.2em' : '12em')};
padding-left: ${({ empty }) => empty && 0.5}em;
width: fit-content;
overflow: hidden;
transition: max-width 0.25s linear;
@@ -42,10 +46,42 @@ interface TokenButtonProps {
export default function TokenButton({ value, collapsed, disabled, onClick }: TokenButtonProps) {
const buttonBackgroundColor = useMemo(() => (value ? 'interactive' : 'accent'), [value])
const contentColor = useMemo(() => (value || disabled ? 'onInteractive' : 'onAccent'), [value, disabled])
// Transition the button only if transitioning from a disabled state.
// This makes initialization cleaner without adding distracting UX to normal swap flows.
const [shouldTransition, setShouldTransition] = useState(disabled)
useEffect(() => {
if (disabled) {
setShouldTransition(true)
}
}, [disabled])
// width must have an absolute value in order to transition, so it is taken from the row ref.
const [row, setRow] = useState<HTMLDivElement | null>(null)
const style = useMemo(() => {
if (!shouldTransition) return
return { width: row ? row.clientWidth + /* padding= */ 8 + /* border= */ 2 : undefined }
}, [row, shouldTransition])
return (
<StyledTokenButton onClick={onClick} empty={!value} color={buttonBackgroundColor} disabled={disabled}>
<StyledTokenButton
onClick={onClick}
color={buttonBackgroundColor}
disabled={disabled}
style={style}
transition={shouldTransition}
onTransitionEnd={() => setShouldTransition(false)}
>
<ThemedText.ButtonLarge color={contentColor}>
<TokenButtonRow gap={0.4} collapsed={Boolean(value) && collapsed}>
<TokenButtonRow
gap={0.4}
empty={!value}
collapsed={collapsed}
// ref is used to set an absolute width, so it must be reset for each value passed.
// To force this, value?.symbol is passed as a key.
ref={setRow}
key={value?.symbol}
>
{value ? (
<>
<TokenImg token={value} size={1.2} />

View File

@@ -25,9 +25,9 @@ const SearchInput = styled(StringInput)`
function usePrefetchBalances() {
const { account } = useActiveWeb3React()
const tokenList = useTokenList()
const [prefetchedTokenList, setPrefetchedTokenList] = useState(tokenList)
useEffect(() => setPrefetchedTokenList(tokenList), [tokenList])
useCurrencyBalances(account, tokenList !== prefetchedTokenList ? tokenList : undefined)
const prefetchedTokenList = useRef<typeof tokenList>()
useCurrencyBalances(account, tokenList !== prefetchedTokenList.current ? tokenList : undefined)
prefetchedTokenList.current = tokenList
}
function useAreBalancesLoaded(): boolean {
@@ -68,7 +68,7 @@ export function TokenSelectDialog({ value, onSelect }: TokenSelectDialogProps) {
const baseTokens: Currency[] = [] // TODO(zzmp): Add base tokens to token list functionality
const input = useRef<HTMLInputElement>(null)
useEffect(() => input.current?.focus(), [input])
useEffect(() => input.current?.focus({ preventScroll: true }), [input])
const [options, setOptions] = useState<ElementRef<typeof TokenOptions> | null>(null)

View File

@@ -1,21 +1,21 @@
import { Provider as EthersProvider } from '@ethersproject/abstract-provider'
import { JsonRpcProvider } from '@ethersproject/providers'
import { TokenInfo } from '@uniswap/token-lists'
import { Provider as Eip1193Provider } from '@web3-react/types'
import { DEFAULT_LOCALE, SupportedLocale } from 'constants/locales'
import { DEFAULT_LOCALE, SUPPORTED_LOCALES, SupportedLocale } from 'constants/locales'
import { Provider as AtomProvider } from 'jotai'
import { TransactionsUpdater } from 'lib/hooks/transactions'
import { Web3Provider } from 'lib/hooks/useActiveWeb3React'
import { BlockUpdater } from 'lib/hooks/useBlockNumber'
import useEip1193Provider from 'lib/hooks/useEip1193Provider'
import { UNMOUNTING } from 'lib/hooks/useUnmount'
import { ActiveWeb3Provider } from 'lib/hooks/useActiveWeb3React'
import { BlockNumberProvider } from 'lib/hooks/useBlockNumber'
import { TokenListProvider } from 'lib/hooks/useTokenList'
import { Provider as I18nProvider } from 'lib/i18n'
import { MulticallUpdater, store as multicallStore } from 'lib/state/multicall'
import styled, { keyframes, Theme, ThemeProvider } from 'lib/theme'
import { PropsWithChildren, StrictMode, useState } from 'react'
import { UNMOUNTING } from 'lib/utils/animations'
import { PropsWithChildren, StrictMode, useMemo, useState } from 'react'
import { Provider as ReduxProvider } from 'react-redux'
import { Modal, Provider as DialogProvider } from './Dialog'
import ErrorBoundary, { ErrorHandler } from './Error/ErrorBoundary'
import WidgetPropValidator from './Error/WidgetsPropsValidator'
const WidgetWrapper = styled.div<{ width?: number | string }>`
-moz-osx-font-smoothing: grayscale;
@@ -31,7 +31,7 @@ const WidgetWrapper = styled.div<{ width?: number | string }>`
font-size: 16px;
font-smooth: always;
font-variant: none;
height: 356px;
height: 360px;
min-width: 300px;
padding: 0.25em;
position: relative;
@@ -48,18 +48,19 @@ const WidgetWrapper = styled.div<{ width?: number | string }>`
}
`
const slideDown = keyframes`
to {
const slideIn = keyframes`
from {
transform: translateY(calc(100% - 0.25em));
}
`
const slideUp = keyframes`
from {
const slideOut = keyframes`
to {
transform: translateY(calc(100% - 0.25em));
}
`
const DialogWrapper = styled.div`
border-radius: ${({ theme }) => theme.borderRadius * 0.75}em;
height: calc(100% - 0.5em);
left: 0;
margin: 0.25em;
@@ -73,29 +74,20 @@ const DialogWrapper = styled.div`
}
${Modal} {
animation: ${slideUp} 0.25s ease-in-out;
}
animation: ${slideIn} 0.25s ease-in;
${Modal}.${UNMOUNTING} {
animation: ${slideDown} 0.25s ease-in-out;
&.${UNMOUNTING} {
animation: ${slideOut} 0.25s ease-out;
}
}
`
function Updaters() {
return (
<>
<BlockUpdater />
<MulticallUpdater />
<TransactionsUpdater />
</>
)
}
export type WidgetProps = {
theme?: Theme
locale?: SupportedLocale
provider?: Eip1193Provider | EthersProvider
jsonRpcEndpoint?: string
provider?: Eip1193Provider | JsonRpcProvider
jsonRpcEndpoint?: string | JsonRpcProvider
tokenList?: string | TokenInfo[]
width?: string | number
dialog?: HTMLElement | null
className?: string
@@ -103,42 +95,47 @@ export type WidgetProps = {
}
export default function Widget(props: PropsWithChildren<WidgetProps>) {
const {
children,
theme,
locale = DEFAULT_LOCALE,
provider,
jsonRpcEndpoint,
width = 360,
dialog: userDialog,
className,
onError,
} = props
const eip1193 = useEip1193Provider(provider)
const { children, theme, provider, jsonRpcEndpoint, dialog: userDialog, className, onError } = props
const width = useMemo(() => {
if (props.width && props.width < 300) {
console.warn(`Widget width must be at least 300px (you set it to ${props.width}). Falling back to 300px.`)
return 300
}
return props.width ?? 360
}, [props.width])
const locale = useMemo(() => {
if (props.locale && ![...SUPPORTED_LOCALES, 'pseudo'].includes(props.locale)) {
console.warn(`Unsupported locale: ${props.locale}. Falling back to ${DEFAULT_LOCALE}.`)
return DEFAULT_LOCALE
}
return props.locale ?? DEFAULT_LOCALE
}, [props.locale])
const [dialog, setDialog] = useState<HTMLDivElement | null>(null)
return (
<StrictMode>
<I18nProvider locale={locale}>
<ThemeProvider theme={theme}>
<WidgetWrapper width={width} className={className}>
<ThemeProvider theme={theme}>
<WidgetWrapper width={width} className={className}>
<I18nProvider locale={locale}>
<DialogWrapper ref={setDialog} />
<DialogProvider value={userDialog || dialog}>
<ErrorBoundary onError={onError}>
<WidgetPropValidator {...props}>
<ReduxProvider store={multicallStore}>
<AtomProvider>
<Web3Provider provider={eip1193} jsonRpcEndpoint={jsonRpcEndpoint}>
<Updaters />
{children}
</Web3Provider>
</AtomProvider>
</ReduxProvider>
</WidgetPropValidator>
<ReduxProvider store={multicallStore}>
<AtomProvider>
<ActiveWeb3Provider provider={provider} jsonRpcEndpoint={jsonRpcEndpoint}>
<BlockNumberProvider>
<MulticallUpdater />
<TransactionsUpdater />
<TokenListProvider list={props.tokenList}>{children}</TokenListProvider>
</BlockNumberProvider>
</ActiveWeb3Provider>
</AtomProvider>
</ReduxProvider>
</ErrorBoundary>
</DialogProvider>
</WidgetWrapper>
</ThemeProvider>
</I18nProvider>
</I18nProvider>
</WidgetWrapper>
</ThemeProvider>
</StrictMode>
)
}

View File

@@ -1,3 +1,4 @@
import { tokens } from '@uniswap/default-token-list'
import { initializeConnector } from '@web3-react/core'
import { MetaMask } from '@web3-react/metamask'
import { Connector } from '@web3-react/types'
@@ -17,14 +18,15 @@ const [walletConnect] = initializeConnector<WalletConnect>(
export default function Wrapper({ children }: { children: ReactNode }) {
const [width] = useValue('width', { defaultValue: 360 })
const [locale] = useSelect('locale', { defaultValue: DEFAULT_LOCALE, options: ['pseudo', ...SUPPORTED_LOCALES] })
const [locale] = useSelect('locale', {
defaultValue: DEFAULT_LOCALE,
options: ['fa-KE (unsupported)', 'pseudo', ...SUPPORTED_LOCALES],
})
const [darkMode] = useValue('dark mode', { defaultValue: false })
const [theme, setTheme] = useValue('theme', { defaultValue: { ...defaultTheme, ...lightTheme } })
useEffect(() => {
setTheme({ ...defaultTheme, ...(darkMode ? darkTheme : lightTheme) })
// cosmos does not maintain referential equality for setters
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [darkMode])
}, [darkMode, setTheme])
const NO_JSON_RPC = 'None'
const [jsonRpcEndpoint] = useSelect('JSON-RPC', {
@@ -74,6 +76,7 @@ export default function Wrapper({ children }: { children: ReactNode }) {
locale={locale}
jsonRpcEndpoint={jsonRpcEndpoint === NO_JSON_RPC ? undefined : jsonRpcEndpoint}
provider={connector?.provider}
tokenList={tokens}
>
{children}
</Widget>

View File

@@ -9,6 +9,6 @@ export const loadingCss = css`
// need to use isLoading as `loading` is a reserved prop
export const loadingTransitionCss = css<{ isLoading: boolean }>`
${({ isLoading }) => isLoading && loadingCss};
transition: opacity ${({ isLoading }) => (isLoading ? 0 : 0.2)}s ease-in-out;
opacity: ${({ isLoading }) => isLoading && loadingOpacity};
transition: color 0.125s linear, opacity ${({ isLoading }) => (isLoading ? 0 : 0.25)}s ease-in-out;
`

View File

@@ -1,7 +1,8 @@
import { BigintIsh, CurrencyAmount, Token, TradeType } from '@uniswap/sdk-core'
// This file is lazy-loaded, so the import of smart-order-router is intentional.
// eslint-disable-next-line no-restricted-imports
import { AlphaRouter, AlphaRouterConfig, AlphaRouterParams, ChainId } from '@uniswap/smart-order-router'
import JSBI from 'jsbi'
import useBlockNumber from 'lib/hooks/useBlockNumber'
import { GetQuoteResult } from 'state/routing/types'
import { transformSwapRouteToGetQuoteResult } from 'utils/transformSwapRouteToGetQuoteResult'
@@ -99,14 +100,3 @@ export async function getClientSideQuote(
routerConfig
)
}
/** Used to keep quotes up to date given a certain block age. Returns undefined if past limit. */
export function useFilterFreshQuote(
quoteResult: GetQuoteResult | undefined,
maxBlockAge = 10
): GetQuoteResult | undefined {
const block = useBlockNumber()
if (!block || !quoteResult) return undefined
if (block - (Number(quoteResult.blockNumber) || 0) > maxBlockAge) return undefined
return quoteResult
}

View File

@@ -1,8 +1,9 @@
import 'setimmediate'
import { Protocol } from '@uniswap/router-sdk'
import { Currency, CurrencyAmount, TradeType } from '@uniswap/sdk-core'
import { ChainId } from '@uniswap/smart-order-router'
import { SupportedChainId } from 'constants/chains'
import useDebounce from 'hooks/useDebounce'
import useLast from 'hooks/useLast'
import { useStablecoinAmountFromFiatValue } from 'hooks/useUSDCPrice'
import { useCallback, useMemo } from 'react'
import { GetQuoteResult, InterfaceTrade, TradeState } from 'state/routing/types'
@@ -10,8 +11,8 @@ import { computeRoutes, transformRoutesToTrade } from 'state/routing/utils'
import useWrapCallback, { WrapType } from '../swap/useWrapCallback'
import useActiveWeb3React from '../useActiveWeb3React'
import { useGetIsValidBlock } from '../useIsValidBlock'
import usePoll from '../usePoll'
import { getClientSideQuote, useFilterFreshQuote } from './clientSideSmartOrderRouter'
import { useRoutingAPIArguments } from './useRoutingAPIArguments'
/**
@@ -19,14 +20,14 @@ import { useRoutingAPIArguments } from './useRoutingAPIArguments'
* Defaults are defined in https://github.com/Uniswap/smart-order-router/blob/309e6f6603984d3b5aef0733b0cfaf129c29f602/src/routers/alpha-router/config.ts#L83.
*/
const DistributionPercents: { [key: number]: number } = {
[ChainId.MAINNET]: 10,
[ChainId.OPTIMISM]: 10,
[ChainId.OPTIMISTIC_KOVAN]: 10,
[ChainId.ARBITRUM_ONE]: 25,
[ChainId.ARBITRUM_RINKEBY]: 25,
[SupportedChainId.MAINNET]: 10,
[SupportedChainId.OPTIMISM]: 10,
[SupportedChainId.OPTIMISTIC_KOVAN]: 10,
[SupportedChainId.ARBITRUM_ONE]: 25,
[SupportedChainId.ARBITRUM_RINKEBY]: 25,
}
const DEFAULT_DISTRIBUTION_PERCENT = 10
function getConfig(chainId: ChainId | undefined) {
function getConfig(chainId: SupportedChainId | undefined) {
return {
// Limit to only V2 and V3.
protocols: [Protocol.V2, Protocol.V3],
@@ -71,60 +72,58 @@ export default function useClientSideSmartOrderRouterTrade<TTradeType extends Tr
const { type: wrapType } = useWrapCallback()
const getQuoteResult = useCallback(async (): Promise<{ data?: GetQuoteResult; error?: unknown }> => {
if (wrapType !== WrapType.NOT_APPLICABLE) return { error: undefined }
if (wrapType !== WrapType.NONE) return { error: undefined }
if (!queryArgs || !params) return { error: undefined }
try {
return await getClientSideQuote(queryArgs, params, config)
// Lazy-load the smart order router to improve initial pageload times.
const quoteResult = await (
await import('./clientSideSmartOrderRouter')
).getClientSideQuote(queryArgs, params, config)
// There is significant post-fetch processing, so delay a tick to prevent dropped frames.
// This is only important in the context of integrations - if we control the whole site,
// then we can afford to drop a few frames.
return new Promise((resolve) => setImmediate(() => resolve(quoteResult)))
} catch {
return { error: true }
}
}, [config, params, queryArgs, wrapType])
const { data, error } = usePoll(getQuoteResult, JSON.stringify(queryArgs), isDebouncing) ?? {
const getIsValidBlock = useGetIsValidBlock()
const { data: quoteResult, error } = usePoll(getQuoteResult, JSON.stringify(queryArgs), {
debounce: isDebouncing,
isStale: useCallback(({ data }) => !getIsValidBlock(Number(data?.blockNumber) || 0), [getIsValidBlock]),
}) ?? {
error: undefined,
}
const quoteResult = useFilterFreshQuote(data)
const isLoading = !quoteResult
const isValid = getIsValidBlock(Number(quoteResult?.blockNumber) || 0)
const route = useMemo(
() => computeRoutes(currencyIn, currencyOut, tradeType, quoteResult),
[currencyIn, currencyOut, quoteResult, tradeType]
)
const gasUseEstimateUSD = useStablecoinAmountFromFiatValue(quoteResult?.gasUseEstimateUSD) ?? null
const trade =
useLast(
useMemo(() => {
if (route) {
try {
return route && transformRoutesToTrade(route, tradeType, gasUseEstimateUSD)
} catch (e: unknown) {
console.debug('transformRoutesToTrade failed: ', e)
}
}
return
}, [gasUseEstimateUSD, route, tradeType]),
Boolean
) ?? undefined
// Dont return old trade if currencies dont match.
const isStale =
(currencyIn && !trade?.inputAmount?.currency.equals(currencyIn)) ||
(currencyOut && !trade?.outputAmount?.currency.equals(currencyOut))
const trade = useMemo(() => {
if (route) {
try {
return route && transformRoutesToTrade(route, tradeType, gasUseEstimateUSD)
} catch (e: unknown) {
console.debug('transformRoutesToTrade failed: ', e)
}
}
return
}, [gasUseEstimateUSD, route, tradeType])
return useMemo(() => {
if (!currencyIn || !currencyOut) {
return { state: TradeState.INVALID, trade: undefined }
}
// Returns the last trade state while syncing/loading to avoid jank from clearing the last trade while loading.
if (!quoteResult && !error) {
if (isStale) {
if (!trade && !error) {
if (isDebouncing) {
return { state: TradeState.SYNCING, trade: undefined }
} else if (!isValid) {
return { state: TradeState.LOADING, trade: undefined }
} else if (isDebouncing) {
return { state: TradeState.SYNCING, trade }
} else if (isLoading) {
return { state: TradeState.LOADING, trade }
}
}
@@ -140,7 +139,7 @@ export default function useClientSideSmartOrderRouterTrade<TTradeType extends Tr
}
}
if (error || !otherAmount || !route || route.length === 0 || !queryArgs) {
if (error || !otherAmount || !route || route.length === 0) {
return { state: TradeState.NO_ROUTE_FOUND, trade: undefined }
}
@@ -148,17 +147,5 @@ export default function useClientSideSmartOrderRouterTrade<TTradeType extends Tr
return { state: TradeState.VALID, trade }
}
return { state: TradeState.INVALID, trade: undefined }
}, [
currencyIn,
currencyOut,
error,
quoteResult,
route,
queryArgs,
trade,
isStale,
isDebouncing,
isLoading,
tradeType,
])
}, [currencyIn, currencyOut, trade, error, isValid, quoteResult, route, isDebouncing, tradeType])
}

View File

@@ -1,4 +1,4 @@
import { Currency, CurrencyAmount, TradeType } from '@uniswap/sdk-core'
import { Currency, CurrencyAmount } from '@uniswap/sdk-core'
import { useAtom } from 'jotai'
import { useAtomValue, useUpdateAtom } from 'jotai/utils'
import { pickAtom } from 'lib/state/atoms'
@@ -63,16 +63,6 @@ export function useIsSwapFieldIndependent(field: Field): boolean {
return independentField === field
}
export function useSwapTradeType(): TradeType {
const independentField = useAtomValue(independentFieldAtom)
switch (independentField) {
case Field.INPUT:
return TradeType.EXACT_INPUT
case Field.OUTPUT:
return TradeType.EXACT_OUTPUT
}
}
const amountAtom = pickAtom(swapAtom, 'amount')
// check if any amount has been entered by user

View File

@@ -1,9 +1,13 @@
import { Currency, CurrencyAmount, TradeType } from '@uniswap/sdk-core'
import { useClientSideV3Trade } from 'hooks/useClientSideV3Trade'
import useLast from 'hooks/useLast'
import { useMemo } from 'react'
import { InterfaceTrade, TradeState } from 'state/routing/types'
import useClientSideSmartOrderRouterTrade from '../routing/useClientSideSmartOrderRouterTrade'
export const INVALID_TRADE = { state: TradeState.INVALID, trade: undefined }
/**
* Returns the best v2+v3 trade for a desired swap.
* @param tradeType whether the swap is an exact in/out
@@ -18,15 +22,39 @@ export function useBestTrade(
state: TradeState
trade: InterfaceTrade<Currency, Currency, TradeType> | undefined
} {
const clientSORTrade = useClientSideSmartOrderRouterTrade(tradeType, amountSpecified, otherCurrency)
const clientSORTradeObject = useClientSideSmartOrderRouterTrade(tradeType, amountSpecified, otherCurrency)
// Use a simple client side logic as backup if SOR is not available.
const useFallback = clientSORTrade.state === TradeState.NO_ROUTE_FOUND || clientSORTrade.state === TradeState.INVALID
const fallbackTrade = useClientSideV3Trade(
const useFallback =
clientSORTradeObject.state === TradeState.NO_ROUTE_FOUND || clientSORTradeObject.state === TradeState.INVALID
const fallbackTradeObject = useClientSideV3Trade(
tradeType,
useFallback ? amountSpecified : undefined,
useFallback ? otherCurrency : undefined
)
return useFallback ? fallbackTrade : clientSORTrade
const tradeObject = useFallback ? fallbackTradeObject : clientSORTradeObject
const lastTrade = useLast(tradeObject.trade, Boolean) ?? undefined
// Return the last trade while syncing/loading to avoid jank from clearing the last trade while loading.
// If the trade is unsettled and not stale, return the last trade as a placeholder during settling.
return useMemo(() => {
const { state, trade } = tradeObject
// If the trade is in a settled state, return it.
if (state === TradeState.INVALID) return INVALID_TRADE
if ((state !== TradeState.LOADING && state !== TradeState.SYNCING) || trade) return tradeObject
const [currencyIn, currencyOut] =
tradeType === TradeType.EXACT_INPUT
? [amountSpecified?.currency, otherCurrency]
: [otherCurrency, amountSpecified?.currency]
// If the trade currencies have switched, consider it stale - do not return the last trade.
const isStale =
(currencyIn && !lastTrade?.inputAmount?.currency.equals(currencyIn)) ||
(currencyOut && !lastTrade?.outputAmount?.currency.equals(currencyOut))
if (isStale) return tradeObject
return { state, trade: lastTrade }
}, [amountSpecified?.currency, lastTrade, otherCurrency, tradeObject, tradeType])
}

View File

@@ -1,5 +1,5 @@
import { BigNumber } from '@ethersproject/bignumber'
import { TransactionResponse, Web3Provider } from '@ethersproject/providers'
import { JsonRpcProvider, TransactionResponse } from '@ethersproject/providers'
// eslint-disable-next-line no-restricted-imports
import { t, Trans } from '@lingui/macro'
import { Trade } from '@uniswap/router-sdk'
@@ -40,7 +40,7 @@ interface FailedCall extends SwapCallEstimate {
export default function useSendSwapTransaction(
account: string | null | undefined,
chainId: number | undefined,
library: Web3Provider | undefined,
library: JsonRpcProvider | undefined,
trade: AnyTrade | undefined, // trade to execute, required
swapCalls: SwapCall[]
): { callback: null | (() => Promise<TransactionResponse>) } {

View File

@@ -7,7 +7,6 @@ import useActiveWeb3React from 'hooks/useActiveWeb3React'
import { useERC20PermitFromTrade, UseERC20PermitState } from 'hooks/useERC20Permit'
import useTransactionDeadline from 'lib/hooks/useTransactionDeadline'
import { useCallback, useMemo } from 'react'
import invariant from 'tiny-invariant'
import { getTxOptimizedSwapRouter, SwapRouterVersion } from 'utils/getTxOptimizedSwapRouter'
import { ApprovalState, useApproval, useApprovalStateForSpender } from '../useApproval'
@@ -75,10 +74,6 @@ export default function useSwapApproval(
const spender = useSwapRouterAddress(trade)
const approval = useApproval(amountToApprove, spender, useIsPendingApproval)
if (trade instanceof V2Trade || trade instanceof V3Trade) {
const approvalState = approval[0]
invariant(approvalState === ApprovalState.APPROVED, 'Trying to approve legacy router')
}
return approval
}
@@ -171,39 +166,39 @@ export const useApproveOrPermit = (
gatherPermitSignature,
} = useERC20PermitFromTrade(trade, allowedSlippage, deadline)
const notApproved = approval === ApprovalState.NOT_APPROVED && !(signatureState === UseERC20PermitState.SIGNED)
// If permit is supported, trigger a signature, if not create approval transaction.
const handleApproveOrPermit = useCallback(async () => {
if (signatureState === UseERC20PermitState.NOT_SIGNED && gatherPermitSignature) {
try {
return await gatherPermitSignature()
} catch (error) {
// Try to approve if gatherPermitSignature failed for any reason other than the user rejecting it.
if (error?.code !== 4001) {
return getApproval()
try {
if (signatureState === UseERC20PermitState.NOT_SIGNED && gatherPermitSignature) {
try {
return await gatherPermitSignature()
} catch (error) {
// Try to approve if gatherPermitSignature failed for any reason other than the user rejecting it.
if (error?.code !== 4001) {
return await getApproval()
}
}
} else {
return await getApproval()
}
} else {
return getApproval()
} catch (e) {
// Swallow approval errors - user rejections do not need to be displayed.
}
}, [signatureState, gatherPermitSignature, getApproval])
const approvalState = useMemo(() => {
if (approval === ApprovalState.PENDING) {
return ApproveOrPermitState.PENDING_APPROVAL
}
if (signatureState === UseERC20PermitState.LOADING) {
} else if (signatureState === UseERC20PermitState.LOADING) {
return ApproveOrPermitState.PENDING_SIGNATURE
}
if (notApproved && Boolean(gatherPermitSignature)) {
} else if (approval !== ApprovalState.NOT_APPROVED || signatureState === UseERC20PermitState.SIGNED) {
return ApproveOrPermitState.APPROVED
} else if (gatherPermitSignature) {
return ApproveOrPermitState.REQUIRES_SIGNATURE
}
if (notApproved) {
} else {
return ApproveOrPermitState.REQUIRES_APPROVAL
}
return ApproveOrPermitState.APPROVED
}, [approval, gatherPermitSignature, notApproved, signatureState])
}, [approval, gatherPermitSignature, signatureState])
return {
approvalState,

View File

@@ -20,8 +20,8 @@ export enum SwapCallbackState {
interface UseSwapCallbackReturns {
state: SwapCallbackState
callback: null | (() => Promise<TransactionResponse>)
error: ReactNode | null
callback?: () => Promise<TransactionResponse>
error?: ReactNode
}
interface UseSwapCallbackArgs {
trade: AnyTrade | undefined // trade to execute, required
@@ -59,24 +59,19 @@ export function useSwapCallback({
return useMemo(() => {
if (!trade || !library || !account || !chainId || !callback) {
return { state: SwapCallbackState.INVALID, callback: null, error: <Trans>Missing dependencies</Trans> }
return { state: SwapCallbackState.INVALID, error: <Trans>Missing dependencies</Trans> }
}
if (!recipient) {
if (recipientAddressOrName !== null) {
return { state: SwapCallbackState.INVALID, callback: null, error: <Trans>Invalid recipient</Trans> }
return { state: SwapCallbackState.INVALID, error: <Trans>Invalid recipient</Trans> }
} else {
return { state: SwapCallbackState.LOADING, callback: null, error: null }
return { state: SwapCallbackState.LOADING }
}
}
return {
state: SwapCallbackState.VALID,
callback: async function onSwap(): Promise<TransactionResponse> {
return callback().then((response) => {
return response
})
},
error: null,
callback: async () => callback(),
}
}, [trade, library, account, chainId, callback, recipient, recipientAddressOrName])
}

View File

@@ -1,16 +1,15 @@
import { Currency, CurrencyAmount, Percent, TradeType } from '@uniswap/sdk-core'
import { atom } from 'jotai'
import { useAtomValue, useUpdateAtom } from 'jotai/utils'
import { Currency, CurrencyAmount, TradeType } from '@uniswap/sdk-core'
import { useAtomValue } from 'jotai/utils'
import useActiveWeb3React from 'lib/hooks/useActiveWeb3React'
import { useCurrencyBalances } from 'lib/hooks/useCurrencyBalance'
import useSlippage, { DEFAULT_SLIPPAGE, Slippage } from 'lib/hooks/useSlippage'
import useUSDCPriceImpact, { PriceImpact } from 'lib/hooks/useUSDCPriceImpact'
import { Field, swapAtom } from 'lib/state/swap'
import tryParseCurrencyAmount from 'lib/utils/tryParseCurrencyAmount'
import { useEffect, useMemo } from 'react'
import { createContext, PropsWithChildren, useContext, useMemo } from 'react'
import { InterfaceTrade, TradeState } from 'state/routing/types'
import useActiveWeb3React from '../useActiveWeb3React'
import useSlippage, { Slippage } from '../useSlippage'
import useUSDCPriceImpact, { PriceImpact } from '../useUSDCPriceImpact'
import { useBestTrade } from './useBestTrade'
import { INVALID_TRADE, useBestTrade } from './useBestTrade'
import useWrapCallback, { WrapType } from './useWrapCallback'
interface SwapField {
@@ -28,12 +27,11 @@ interface SwapInfo {
state: TradeState
}
slippage: Slippage
impact: PriceImpact
impact?: PriceImpact
}
// from the current swap inputs, compute the best trade and return it.
function useComputeSwapInfo(): SwapInfo {
const { account } = useActiveWeb3React()
const { type: wrapType } = useWrapCallback()
const isWrapping = wrapType === WrapType.WRAP || wrapType === WrapType.UNWRAP
const { independentField, amount, [Field.INPUT]: currencyIn, [Field.OUTPUT]: currencyOut } = useAtomValue(swapAtom)
@@ -43,10 +41,11 @@ function useComputeSwapInfo(): SwapInfo {
() => tryParseCurrencyAmount(amount, (isExactIn ? currencyIn : currencyOut) ?? undefined),
[amount, isExactIn, currencyIn, currencyOut]
)
const hasAmounts = currencyIn && currencyOut && parsedAmount && !isWrapping
const trade = useBestTrade(
isExactIn ? TradeType.EXACT_INPUT : TradeType.EXACT_OUTPUT,
parsedAmount,
(isExactIn ? currencyOut : currencyIn) ?? undefined
hasAmounts ? parsedAmount : undefined,
hasAmounts ? (isExactIn ? currencyOut : currencyIn) : undefined
)
const amountIn = useMemo(
@@ -57,6 +56,8 @@ function useComputeSwapInfo(): SwapInfo {
() => (isWrapping || !isExactIn ? parsedAmount : trade.trade?.outputAmount),
[isExactIn, isWrapping, parsedAmount, trade.trade?.outputAmount]
)
const { account } = useActiveWeb3React()
const [balanceIn, balanceOut] = useCurrencyBalances(
account,
useMemo(() => [currencyIn, currencyOut], [currencyIn, currencyOut])
@@ -65,11 +66,7 @@ function useComputeSwapInfo(): SwapInfo {
// Compute slippage and impact off of the trade so that it refreshes with the trade.
// (Using amountIn/amountOut would show (incorrect) intermediate values.)
const slippage = useSlippage(trade.trade)
const {
inputUSDC: usdcIn,
outputUSDC: usdcOut,
priceImpact: impact,
} = useUSDCPriceImpact(trade.trade?.inputAmount, trade.trade?.outputAmount)
const { inputUSDC, outputUSDC, impact } = useUSDCPriceImpact(trade.trade?.inputAmount, trade.trade?.outputAmount)
return useMemo(
() => ({
@@ -77,38 +74,52 @@ function useComputeSwapInfo(): SwapInfo {
currency: currencyIn,
amount: amountIn,
balance: balanceIn,
usdc: usdcIn,
usdc: inputUSDC,
},
[Field.OUTPUT]: {
currency: currencyOut,
amount: amountOut,
balance: balanceOut,
usdc: usdcOut,
usdc: outputUSDC,
},
trade,
slippage,
impact,
}),
[amountIn, amountOut, balanceIn, balanceOut, currencyIn, currencyOut, impact, slippage, trade, usdcIn, usdcOut]
[
amountIn,
amountOut,
balanceIn,
balanceOut,
currencyIn,
currencyOut,
impact,
inputUSDC,
outputUSDC,
slippage,
trade,
]
)
}
const swapInfoAtom = atom<SwapInfo>({
const DEFAULT_SWAP_INFO: SwapInfo = {
[Field.INPUT]: {},
[Field.OUTPUT]: {},
trade: { state: TradeState.INVALID },
slippage: { auto: true, allowed: new Percent(0) },
impact: {},
})
trade: INVALID_TRADE,
slippage: DEFAULT_SLIPPAGE,
}
export function SwapInfoUpdater() {
const setSwapInfo = useUpdateAtom(swapInfoAtom)
const SwapInfoContext = createContext(DEFAULT_SWAP_INFO)
export function SwapInfoProvider({ children, disabled }: PropsWithChildren<{ disabled?: boolean }>) {
const swapInfo = useComputeSwapInfo()
useEffect(() => setSwapInfo(swapInfo), [swapInfo, setSwapInfo])
return null
if (disabled) {
return <SwapInfoContext.Provider value={DEFAULT_SWAP_INFO}>{children}</SwapInfoContext.Provider>
}
return <SwapInfoContext.Provider value={swapInfo}>{children}</SwapInfoContext.Provider>
}
/** Requires that SwapInfoUpdater be installed in the DOM tree. **/
export default function useSwapInfo(): SwapInfo {
return useAtomValue(swapInfoAtom)
return useContext(SwapInfoContext)
}

View File

@@ -5,9 +5,10 @@ import useActiveWeb3React from 'lib/hooks/useActiveWeb3React'
import { useToken } from 'lib/hooks/useCurrency'
import useNativeCurrency from 'lib/hooks/useNativeCurrency'
import { Field, Swap, swapAtom } from 'lib/state/swap'
import { useCallback, useLayoutEffect, useState } from 'react'
import { useCallback, useRef } from 'react'
import useOnSupportedNetwork from '../useOnSupportedNetwork'
import { useIsTokenListLoaded } from '../useTokenList'
export type DefaultAddress = string | { [chainId: number]: string | 'NATIVE' } | 'NATIVE'
@@ -71,13 +72,10 @@ export default function useSyncTokenDefaults({
updateSwap((swap) => ({ ...swap, ...defaultSwapState }))
}, [defaultInputAmount, defaultInputToken, defaultOutputAmount, defaultOutputToken, updateSwap])
const [previousChainId, setPreviousChainId] = useState(chainId)
useLayoutEffect(() => {
setPreviousChainId(chainId)
}, [chainId])
useLayoutEffect(() => {
if (chainId && chainId !== previousChainId) {
setToDefaults()
}
}, [chainId, previousChainId, setToDefaults])
const lastChainId = useRef<number | undefined>(undefined)
const shouldSync = useIsTokenListLoaded() && chainId && chainId !== lastChainId.current
if (shouldSync) {
setToDefaults()
lastChainId.current = chainId
}
}

View File

@@ -1,133 +1,63 @@
import { ContractTransaction } from '@ethersproject/contracts'
import { useWETHContract } from 'hooks/useContract'
import { atom, useAtom } from 'jotai'
import { useAtomValue } from 'jotai/utils'
import { Field, swapAtom } from 'lib/state/swap'
import tryParseCurrencyAmount from 'lib/utils/tryParseCurrencyAmount'
import { useCallback, useEffect, useMemo } from 'react'
import { useMemo } from 'react'
import { WRAPPED_NATIVE_CURRENCY } from '../../../constants/tokens'
import useActiveWeb3React from '../useActiveWeb3React'
import { useCurrencyBalances } from '../useCurrencyBalance'
import useCurrencyBalance from '../useCurrencyBalance'
export enum WrapType {
NOT_APPLICABLE,
NONE,
WRAP,
UNWRAP,
}
interface UseWrapCallbackReturns {
callback: () => Promise<ContractTransaction>
error: WrapError
loading: boolean
callback?: () => Promise<ContractTransaction>
type: WrapType
}
export enum WrapError {
NO_ERROR = 0, // must be equal to 0 so all other errors are truthy
ENTER_NATIVE_AMOUNT,
ENTER_WRAPPED_AMOUNT,
INSUFFICIENT_NATIVE_BALANCE,
INSUFFICIENT_WRAPPED_BALANCE,
}
interface WrapState {
loading: boolean
error: WrapError
}
const wrapState = atom<WrapState>({
loading: false,
error: WrapError.NO_ERROR,
})
export default function useWrapCallback(): UseWrapCallbackReturns {
const { account, chainId } = useActiveWeb3React()
const [{ loading, error }, setWrapState] = useAtom(wrapState)
const wrappedNativeCurrencyContract = useWETHContract()
const { amount, [Field.INPUT]: inputCurrency, [Field.OUTPUT]: outputCurrency } = useAtomValue(swapAtom)
const wrapType = useMemo(() => {
if (!inputCurrency || !outputCurrency || !chainId) {
return WrapType.NOT_APPLICABLE
if (chainId && inputCurrency && outputCurrency) {
if (inputCurrency.isNative && WRAPPED_NATIVE_CURRENCY[chainId]?.equals(outputCurrency)) {
return WrapType.WRAP
}
if (outputCurrency.isNative && WRAPPED_NATIVE_CURRENCY[chainId]?.equals(inputCurrency)) {
return WrapType.UNWRAP
}
}
if (inputCurrency.isNative && WRAPPED_NATIVE_CURRENCY[chainId]?.equals(outputCurrency)) {
return WrapType.WRAP
}
if (WRAPPED_NATIVE_CURRENCY[chainId]?.equals(inputCurrency) && outputCurrency.isNative) {
return WrapType.UNWRAP
}
return WrapType.NOT_APPLICABLE
return WrapType.NONE
}, [chainId, inputCurrency, outputCurrency])
const parsedAmountIn = useMemo(
() => tryParseCurrencyAmount(amount, inputCurrency ?? undefined),
[inputCurrency, amount]
)
const balanceIn = useCurrencyBalance(account, inputCurrency)
const relevantTokenBalances = useCurrencyBalances(
account,
useMemo(() => [inputCurrency ?? undefined, outputCurrency ?? undefined], [inputCurrency, outputCurrency])
)
const currencyBalances = useMemo(
() => ({
[Field.INPUT]: relevantTokenBalances[0],
[Field.OUTPUT]: relevantTokenBalances[1],
}),
[relevantTokenBalances]
)
const callback = useMemo(() => {
if (
wrapType === WrapType.NONE ||
!parsedAmountIn ||
!balanceIn ||
balanceIn.lessThan(parsedAmountIn) ||
!wrappedNativeCurrencyContract
) {
return
}
const hasInputAmount = Boolean(parsedAmountIn?.greaterThan('0'))
const sufficientBalance = parsedAmountIn && !currencyBalances[Field.INPUT]?.lessThan(parsedAmountIn)
return async () =>
wrapType === WrapType.WRAP
? wrappedNativeCurrencyContract.deposit({ value: `0x${parsedAmountIn.quotient.toString(16)}` })
: wrappedNativeCurrencyContract.withdraw(`0x${parsedAmountIn.quotient.toString(16)}`)
}, [wrapType, parsedAmountIn, balanceIn, wrappedNativeCurrencyContract])
useEffect(() => {
if (sufficientBalance) {
setWrapState((state) => ({ ...state, error: WrapError.NO_ERROR }))
} else if (wrapType === WrapType.WRAP) {
setWrapState((state) => ({
...state,
error: hasInputAmount ? WrapError.INSUFFICIENT_NATIVE_BALANCE : WrapError.ENTER_NATIVE_AMOUNT,
}))
} else if (wrapType === WrapType.UNWRAP) {
setWrapState((state) => ({
...state,
error: hasInputAmount ? WrapError.INSUFFICIENT_WRAPPED_BALANCE : WrapError.ENTER_WRAPPED_AMOUNT,
}))
}
}, [hasInputAmount, setWrapState, sufficientBalance, wrapType])
const callback = useCallback(async () => {
if (!parsedAmountIn) {
return Promise.reject('Must provide an input amount to wrap.')
}
if (wrapType === WrapType.NOT_APPLICABLE) {
return Promise.reject('Wrapping not applicable to this asset.')
}
if (!sufficientBalance) {
return Promise.reject('Insufficient balance to wrap desired amount.')
}
if (!wrappedNativeCurrencyContract) {
return Promise.reject('Wrap contract not found.')
}
setWrapState((state) => ({ ...state, loading: true }))
const result = await (wrapType === WrapType.WRAP
? wrappedNativeCurrencyContract.deposit({ value: `0x${parsedAmountIn.quotient.toString(16)}` })
: wrappedNativeCurrencyContract.withdraw(`0x${parsedAmountIn.quotient.toString(16)}`)
).catch((e: unknown) => {
setWrapState((state) => ({ ...state, loading: false }))
throw e
})
// resolve loading state after one confirmation
result.wait(1).finally(() => setWrapState((state) => ({ ...state, loading: false })))
return Promise.resolve(result)
}, [wrappedNativeCurrencyContract, sufficientBalance, parsedAmountIn, wrapType, setWrapState])
return useMemo(
() => ({
callback,
error,
loading,
type: wrapType,
}),
[callback, error, loading, wrapType]
)
return useMemo(() => ({ callback, type: wrapType }), [callback, wrapType])
}

View File

@@ -1,63 +1,107 @@
import { getPriorityConnector, initializeConnector, Web3ReactHooks } from '@web3-react/core'
import { ExternalProvider, JsonRpcProvider, Web3Provider } from '@ethersproject/providers'
import { initializeConnector, Web3ReactHooks } from '@web3-react/core'
import { EIP1193 } from '@web3-react/eip1193'
import { EMPTY } from '@web3-react/empty'
import { Actions, Connector, Provider as Eip1193Provider } from '@web3-react/types'
import { Actions, Connector, Provider as Eip1193Provider, Web3ReactStore } from '@web3-react/types'
import { Url } from '@web3-react/url'
import { useAtom, WritableAtom } from 'jotai'
import { useAtomValue } from 'jotai/utils'
import { atomWithDefault, RESET, useUpdateAtom } from 'jotai/utils'
import { PropsWithChildren, useEffect } from 'react'
import { atom } from 'jotai'
import JsonRpcConnector from 'lib/utils/JsonRpcConnector'
import { createContext, PropsWithChildren, useContext, useEffect, useMemo } from 'react'
const [connector, hooks] = initializeConnector(() => EMPTY)
const EMPTY_CONNECTOR: [Connector, Web3ReactHooks] = [connector, hooks]
const urlConnectorAtom = atomWithDefault<[Connector, Web3ReactHooks]>(() => EMPTY_CONNECTOR)
const injectedConnectorAtom = atomWithDefault<[Connector, Web3ReactHooks]>(() => EMPTY_CONNECTOR)
const web3Atom = atomWithDefault<ReturnType<typeof hooks.useWeb3React>>(() => ({
connector: EMPTY_CONNECTOR[0],
library: undefined,
chainId: undefined,
account: undefined,
active: false,
error: undefined,
}))
type Web3ContextType = {
connector: Connector
library?: (JsonRpcProvider & { provider?: ExternalProvider }) | Web3Provider
chainId?: ReturnType<Web3ReactHooks['useChainId']>
accounts?: ReturnType<Web3ReactHooks['useAccounts']>
account?: ReturnType<Web3ReactHooks['useAccount']>
active?: ReturnType<Web3ReactHooks['useIsActive']>
activating?: ReturnType<Web3ReactHooks['useIsActivating']>
error?: ReturnType<Web3ReactHooks['useError']>
ensNames?: ReturnType<Web3ReactHooks['useENSNames']>
ensName?: ReturnType<Web3ReactHooks['useENSName']>
}
const EMPTY_CONNECTOR = initializeConnector(() => EMPTY)
const EMPTY_CONTEXT: Web3ContextType = { connector: EMPTY }
const jsonRpcConnectorAtom = atom<[Connector, Web3ReactHooks, Web3ReactStore]>(EMPTY_CONNECTOR)
const injectedConnectorAtom = atom<[Connector, Web3ReactHooks, Web3ReactStore]>(EMPTY_CONNECTOR)
const Web3Context = createContext(EMPTY_CONTEXT)
export default function useActiveWeb3React() {
return useAtomValue(web3Atom)
return useContext(Web3Context)
}
function useConnector<T extends { new (actions: Actions, initializer: I): Connector }, I>(
connectorAtom: WritableAtom<[Connector, Web3ReactHooks], typeof RESET | [Connector, Web3ReactHooks]>,
connectorAtom: WritableAtom<[Connector, Web3ReactHooks, Web3ReactStore], [Connector, Web3ReactHooks, Web3ReactStore]>,
Connector: T,
initializer: I | undefined
) {
const [connector, setConnector] = useAtom(connectorAtom)
useEffect(() => {
if (initializer) {
const [connector, hooks] = initializeConnector((actions) => new Connector(actions, initializer))
const [connector, hooks, store] = initializeConnector((actions) => new Connector(actions, initializer))
connector.activate()
setConnector([connector, hooks])
setConnector([connector, hooks, store])
} else {
setConnector(RESET)
setConnector(EMPTY_CONNECTOR)
}
}, [Connector, initializer, setConnector])
return connector
}
interface Web3ProviderProps {
provider?: Eip1193Provider
jsonRpcEndpoint?: string
interface ActiveWeb3ProviderProps {
provider?: Eip1193Provider | JsonRpcProvider
jsonRpcEndpoint?: string | JsonRpcProvider
}
export function Web3Provider({ provider, jsonRpcEndpoint, children }: PropsWithChildren<Web3ProviderProps>) {
const injectedConnector = useConnector(injectedConnectorAtom, EIP1193, provider)
const urlConnector = useConnector(urlConnectorAtom, Url, jsonRpcEndpoint)
const priorityConnector = getPriorityConnector(injectedConnector, urlConnector)
const priorityProvider = priorityConnector.usePriorityProvider()
const priorityWeb3React = priorityConnector.usePriorityWeb3React(priorityProvider)
const setWeb3 = useUpdateAtom(web3Atom)
export function ActiveWeb3Provider({
provider,
jsonRpcEndpoint,
children,
}: PropsWithChildren<ActiveWeb3ProviderProps>) {
const Injected = useMemo(() => {
if (provider) {
if (JsonRpcProvider.isProvider(provider)) return JsonRpcConnector
if (JsonRpcProvider.isProvider((provider as any).provider)) {
throw new Error('Eip1193Bridge is experimental: pass your ethers Provider directly')
}
}
return EIP1193
}, [provider]) as { new (actions: Actions, initializer: typeof provider): Connector }
const injectedConnector = useConnector(injectedConnectorAtom, Injected, provider)
const JsonRpc = useMemo(() => {
if (JsonRpcProvider.isProvider(jsonRpcEndpoint)) return JsonRpcConnector
return Url
}, [jsonRpcEndpoint]) as { new (actions: Actions, initializer: typeof jsonRpcEndpoint): Connector }
const jsonRpcConnector = useConnector(jsonRpcConnectorAtom, JsonRpc, jsonRpcEndpoint)
const [connector, hooks] = injectedConnector[1].useIsActive()
? injectedConnector
: jsonRpcConnector ?? EMPTY_CONNECTOR
const library = hooks.useProvider()
const accounts = hooks.useAccounts()
const account = hooks.useAccount()
const activating = hooks.useIsActivating()
const active = hooks.useIsActive()
const chainId = hooks.useChainId()
const ensNames = hooks.useENSNames()
const ensName = hooks.useENSName()
const error = hooks.useError()
const web3 = useMemo(() => {
if (connector === EMPTY || !(active || activating)) {
return EMPTY_CONTEXT
}
return { connector, library, chainId, accounts, account, active, activating, error, ensNames, ensName }
}, [account, accounts, activating, active, chainId, connector, ensName, ensNames, error, library])
// Log web3 errors to facilitate debugging.
useEffect(() => {
setWeb3(priorityWeb3React)
}, [priorityWeb3React, setWeb3])
if (error) {
console.error('web3 error:', error)
}
}, [error])
return <>{children}</>
return <Web3Context.Provider value={web3}>{children}</Web3Context.Provider>
}

View File

@@ -1,70 +0,0 @@
import useActiveWeb3React from 'hooks/useActiveWeb3React'
import useDebounce from 'hooks/useDebounce'
import useIsWindowVisible from 'hooks/useIsWindowVisible'
import { atom } from 'jotai'
import { useAtomValue, useUpdateAtom } from 'jotai/utils'
import { useCallback, useEffect, useState } from 'react'
function useBlock() {
const { chainId, library } = useActiveWeb3React()
const windowVisible = useIsWindowVisible()
const [state, setState] = useState<{ chainId?: number; block?: number }>({ chainId })
const onBlock = useCallback(
(block: number) => {
setState((state) => {
if (state.chainId === chainId) {
if (typeof state.block !== 'number') return { chainId, block }
return { chainId, block: Math.max(block, state.block) }
}
return state
})
},
[chainId]
)
useEffect(() => {
if (library && chainId && windowVisible) {
// If chainId hasn't changed, don't clear the block. This prevents re-fetching still valid data.
setState((state) => (state.chainId === chainId ? state : { chainId }))
library
.getBlockNumber()
.then(onBlock)
.catch((error) => {
console.error(`Failed to get block number for chainId ${chainId}`, error)
})
library.on('block', onBlock)
return () => {
library.removeListener('block', onBlock)
}
}
return undefined
}, [chainId, library, onBlock, windowVisible])
const debouncedBlock = useDebounce(state.block, 100)
return state.block ? debouncedBlock : undefined
}
const blockAtom = atom<number | undefined>(undefined)
export function BlockUpdater() {
const setBlock = useUpdateAtom(blockAtom)
const block = useBlock()
useEffect(() => {
setBlock(block)
}, [block, setBlock])
return null
}
/** Requires that BlockUpdater be installed in the DOM tree. */
export default function useBlockNumber(): number | undefined {
const { chainId } = useActiveWeb3React()
const block = useAtomValue(blockAtom)
return chainId ? block : undefined
}
export function useFastForwardBlockNumber(): (block: number) => void {
return useUpdateAtom(blockAtom)
}

View File

@@ -0,0 +1,78 @@
import useActiveWeb3React from 'hooks/useActiveWeb3React'
import useIsWindowVisible from 'hooks/useIsWindowVisible'
import { createContext, ReactNode, useCallback, useContext, useEffect, useMemo, useState } from 'react'
const MISSING_PROVIDER = Symbol()
const BlockNumberContext = createContext<
| {
value?: number
fastForward(block: number): void
}
| typeof MISSING_PROVIDER
>(MISSING_PROVIDER)
function useBlockNumberContext() {
const blockNumber = useContext(BlockNumberContext)
if (blockNumber === MISSING_PROVIDER) {
throw new Error('BlockNumber hooks must be wrapped in a <BlockNumberProvider>')
}
return blockNumber
}
/** Requires that BlockUpdater be installed in the DOM tree. */
export default function useBlockNumber(): number | undefined {
return useBlockNumberContext().value
}
export function useFastForwardBlockNumber(): (block: number) => void {
return useBlockNumberContext().fastForward
}
export function BlockNumberProvider({ children }: { children: ReactNode }) {
const { chainId: activeChainId, library } = useActiveWeb3React()
const [{ chainId, block }, setChainBlock] = useState<{ chainId?: number; block?: number }>({ chainId: activeChainId })
const onBlock = useCallback(
(block: number) => {
setChainBlock((chainBlock) => {
if (chainBlock.chainId === activeChainId) {
if (!chainBlock.block || chainBlock.block < block) {
return { chainId: activeChainId, block }
}
}
return chainBlock
})
},
[activeChainId, setChainBlock]
)
const windowVisible = useIsWindowVisible()
useEffect(() => {
if (library && activeChainId && windowVisible) {
// If chainId hasn't changed, don't clear the block. This prevents re-fetching still valid data.
setChainBlock((chainBlock) => (chainBlock.chainId === activeChainId ? chainBlock : { chainId: activeChainId }))
library
.getBlockNumber()
.then(onBlock)
.catch((error) => {
console.error(`Failed to get block number for chainId ${activeChainId}`, error)
})
library.on('block', onBlock)
return () => {
library.removeListener('block', onBlock)
}
}
return undefined
}, [activeChainId, library, onBlock, setChainBlock, windowVisible])
const value = useMemo(
() => ({
value: chainId === activeChainId ? block : undefined,
fastForward: (block: number) => setChainBlock({ chainId: activeChainId, block }),
}),
[activeChainId, block, chainId]
)
return <BlockNumberContext.Provider value={value}>{children}</BlockNumberContext.Provider>
}

View File

@@ -47,7 +47,7 @@ export function useNativeCurrencyBalances(uncheckedAddresses?: (string | undefin
}
const ERC20Interface = new Interface(ERC20ABI) as Erc20Interface
const tokenBalancesGasRequirement = { gasRequired: 125_000 }
const tokenBalancesGasRequirement = { gasRequired: 185_000 }
/**
* Returns a map of token addresses to their eventually consistent token balances for a single account.

View File

@@ -1,7 +1,7 @@
import { Currency } from '@uniswap/sdk-core'
import { useTheme } from 'lib/theme'
import Vibrant from 'node-vibrant/lib/bundle.js'
import { useEffect, useLayoutEffect, useState } from 'react'
import { useEffect, useState } from 'react'
import useCurrencyLogoURIs from './useCurrencyLogoURIs'
@@ -57,7 +57,7 @@ export default function useCurrencyColor(token?: Currency) {
const theme = useTheme()
const logoURIs = useCurrencyLogoURIs(token)
useLayoutEffect(() => {
useEffect(() => {
let stale = false
if (theme.tokenColorExtraction && token) {

View File

@@ -1,63 +0,0 @@
import { Provider as EthersProvider } from '@ethersproject/abstract-provider'
import { VoidSigner } from '@ethersproject/abstract-signer'
import { Eip1193Bridge as ExperimentalEip1193Bridge } from '@ethersproject/experimental'
import { JsonRpcProvider, JsonRpcSigner } from '@ethersproject/providers'
import { Provider as Eip1193Provider } from '@web3-react/types'
import { ZERO_ADDRESS } from 'constants/misc'
import { useMemo } from 'react'
const voidSigner = new VoidSigner(ZERO_ADDRESS)
class Eip1193Bridge extends ExperimentalEip1193Bridge {
async send(method: string, params?: Array<any>): Promise<any> {
switch (method) {
case 'eth_chainId': {
// TODO(https://github.com/ethers-io/ethers.js/pull/2711): Returns eth_chainId as a hexadecimal.
const result = await this.provider.getNetwork()
return '0x' + result.chainId.toString(16)
}
case 'eth_requestAccounts':
try {
return await super.send(method, params)
} catch (e) {
return this.send('eth_accounts')
}
case 'eth_sendTransaction': {
if (!this.signer) break
// TODO(zzmp): JsonRpcProvider filters from/gas fields from the params.
const req = JsonRpcProvider.hexlifyTransaction(params?.[0], { from: true, gas: true })
const tx = await this.signer.sendTransaction(req)
return tx.hash
}
default:
return super.send(method, params)
}
}
}
interface EthersSigningProvider extends EthersProvider {
getSigner(): JsonRpcSigner
}
export default function useEip1193Provider(
provider?: Eip1193Provider | EthersSigningProvider | EthersProvider
): Eip1193Provider | undefined {
return useMemo(() => {
if (provider) {
if (EthersProvider.isProvider(provider)) {
const signer = 'getSigner' in provider ? provider.getSigner() : null ?? voidSigner
return new Eip1193Bridge(signer, provider)
} else if (EthersProvider.isProvider((provider as ExperimentalEip1193Bridge).provider)) {
/*
* Direct users to use our own wrapper to avoid any pitfalls:
* - Eip1193Bridge is experimental
* - signer is not straightforward
* - bugs out if chainId>8
*/
throw new Error('Eip1193Bridge is experimental: pass your ethers Provider directly')
}
}
return provider
}, [provider])
}

View File

@@ -0,0 +1,45 @@
import { atomWithImmer } from 'jotai/immer'
import { useAtomValue, useUpdateAtom } from 'jotai/utils'
import { useCallback } from 'react'
import useActiveWeb3React from './useActiveWeb3React'
import useBlockNumber from './useBlockNumber'
// The oldest block (per chain) to be considered valid.
const oldestBlockMapAtom = atomWithImmer<{ [chainId: number]: number }>({})
const DEFAULT_MAX_BLOCK_AGE = 10
export function useSetOldestValidBlock(): (block: number) => void {
const { chainId } = useActiveWeb3React()
const updateValidBlock = useUpdateAtom(oldestBlockMapAtom)
return useCallback(
(block: number) => {
if (!chainId) return
updateValidBlock((oldestBlockMap) => {
oldestBlockMap[chainId] = Math.max(block, oldestBlockMap[chainId] || 0)
})
},
[chainId, updateValidBlock]
)
}
export function useGetIsValidBlock(maxBlockAge = DEFAULT_MAX_BLOCK_AGE): (block: number) => boolean {
const { chainId } = useActiveWeb3React()
const currentBlock = useBlockNumber()
const oldestBlockMap = useAtomValue(oldestBlockMapAtom)
const oldestBlock = chainId ? oldestBlockMap[chainId] : 0
return useCallback(
(block: number) => {
if (!currentBlock) return false
if (currentBlock - block > maxBlockAge) return false
if (currentBlock < oldestBlock) return false
return true
},
[currentBlock, maxBlockAge, oldestBlock]
)
}
export default function useIsValidBlock(block: number): boolean {
return useGetIsValidBlock()(block)
}

View File

@@ -5,7 +5,7 @@ import useActiveWeb3React from './useActiveWeb3React'
function useOnSupportedNetwork() {
const { chainId } = useActiveWeb3React()
return useMemo(() => chainId && ALL_SUPPORTED_CHAIN_IDS.includes(chainId), [chainId])
return useMemo(() => Boolean(chainId && ALL_SUPPORTED_CHAIN_IDS.includes(chainId)), [chainId])
}
export default useOnSupportedNetwork

View File

@@ -4,47 +4,71 @@ import { useEffect, useMemo, useState } from 'react'
const DEFAULT_POLLING_INTERVAL = ms`15s`
const DEFAULT_KEEP_UNUSED_DATA_FOR = ms`10s`
interface PollingOptions<T> {
// If true, any cached result will be returned, but no new fetch will be initiated.
debounce?: boolean
// If stale, any cached result will be returned, and a new fetch will be initiated.
isStale?: (value: T) => boolean
pollingInterval?: number
keepUnusedDataFor?: number
}
interface CacheEntry<T> {
ttl: number | null // null denotes a pending fetch
result?: T
}
export default function usePoll<T>(
fetch: () => Promise<T>,
key = '',
check = false, // set to true to check the cache without initiating a new request
pollingInterval = DEFAULT_POLLING_INTERVAL,
keepUnusedDataFor = DEFAULT_KEEP_UNUSED_DATA_FOR
{
debounce = false,
isStale,
pollingInterval = DEFAULT_POLLING_INTERVAL,
keepUnusedDataFor = DEFAULT_KEEP_UNUSED_DATA_FOR,
}: PollingOptions<T>
): T | undefined {
const cache = useMemo(() => new Map<string, { ttl: number; result?: T }>(), [])
const cache = useMemo(() => new Map<string, CacheEntry<T>>(), [])
const [, setData] = useState<{ key: string; result?: T }>({ key })
useEffect(() => {
if (check) return
if (debounce) return
let timeout: number
const entry = cache.get(key)
if (entry && entry.ttl + keepUnusedDataFor > Date.now()) {
// If there is a fresh entry, return it and queue the next poll.
setData({ key, result: entry.result })
timeout = setTimeout(poll, Math.max(0, entry.ttl - Date.now()))
if (entry) {
// If there is not a pending fetch (and there should be), queue one.
if (entry.ttl) {
if (isStale && entry?.result !== undefined ? isStale(entry.result) : false) {
poll() // stale results should be refetched immediately
} else if (entry.ttl && entry.ttl + keepUnusedDataFor > Date.now()) {
timeout = setTimeout(poll, Math.max(0, entry.ttl - Date.now()))
}
}
} else {
// Otherwise, set a new entry (to avoid duplicate polling) and trigger a poll immediately.
cache.set(key, { ttl: Date.now() + pollingInterval })
setData({ key })
// If there is no cached entry, trigger a poll immediately.
poll()
}
setData({ key, result: entry?.result })
return () => {
clearTimeout(timeout)
timeout = 0
}
async function poll(ttl = Date.now() + pollingInterval) {
timeout = setTimeout(poll, pollingInterval)
const result = await fetch()
timeout = setTimeout(poll, pollingInterval) // queue the next poll
cache.set(key, { ttl: null, ...cache.get(key) }) // mark the entry as a pending fetch
// Always set the result in the cache, but only set it as data if the key is still being queried.
const result = await fetch()
cache.set(key, { ttl, result })
setData((data) => {
return data.key === key ? { key, result } : data
})
if (timeout) setData((data) => (data.key === key ? { key, result } : data))
}
}, [cache, check, fetch, keepUnusedDataFor, key, pollingInterval])
}, [cache, debounce, fetch, isStale, keepUnusedDataFor, key, pollingInterval])
useEffect(() => {
// Cleanup stale entries when a new key is used.
@@ -52,7 +76,7 @@ export default function usePoll<T>(
const now = Date.now()
cache.forEach(({ ttl }, key) => {
if (ttl + keepUnusedDataFor <= now) {
if (ttl && ttl + keepUnusedDataFor <= now) {
cache.delete(key)
}
})

View File

@@ -1,7 +1,5 @@
import { css } from 'lib/theme'
import { useCallback, useEffect, useMemo, useState } from 'react'
import useNativeEvent from './useNativeEvent'
import { useMemo } from 'react'
const overflowCss = css`
overflow-y: scroll;
@@ -48,21 +46,15 @@ interface ScrollbarOptions {
}
export default function useScrollbar(element: HTMLElement | null, { padded = false }: ScrollbarOptions = {}) {
const [overflow, setOverflow] = useState(true)
useEffect(() => {
setOverflow(hasOverflow(element))
}, [element])
useNativeEvent(
element,
'transitionend',
useCallback(() => setOverflow(hasOverflow(element)), [element])
return useMemo(
// NB: The css must be applied on an element's first render. WebKit will not re-apply overflow
// properties until any transitions have ended, so waiting a frame for state would cause jank.
() => (hasOverflow(element) ? scrollbarCss(padded) : overflowCss),
[element, padded]
)
return useMemo(() => (overflow ? scrollbarCss(padded) : overflowCss), [overflow, padded])
function hasOverflow(element: HTMLElement | null) {
if (!element) {
return true
}
if (!element) return true
return element.scrollHeight > element.clientHeight
}
}

View File

@@ -1,5 +1,5 @@
import { Currency, Percent, TradeType } from '@uniswap/sdk-core'
import useAutoSlippageTolerance from 'hooks/useAutoSlippageTolerance'
import useAutoSlippageTolerance, { DEFAULT_AUTO_SLIPPAGE } from 'hooks/useAutoSlippageTolerance'
import { useAtomValue } from 'jotai/utils'
import { autoSlippageAtom, maxSlippageAtom } from 'lib/state/settings'
import { useMemo } from 'react'
@@ -17,6 +17,8 @@ export interface Slippage {
warning?: 'warning' | 'error'
}
export const DEFAULT_SLIPPAGE = { auto: true, allowed: DEFAULT_AUTO_SLIPPAGE }
/** Returns the allowed slippage, and whether it is auto-slippage. */
export default function useSlippage(trade: InterfaceTrade<Currency, Currency, TradeType> | undefined): Slippage {
const shouldUseAutoSlippage = useAtomValue(autoSlippageAtom)
@@ -27,6 +29,9 @@ export default function useSlippage(trade: InterfaceTrade<Currency, Currency, Tr
const auto = shouldUseAutoSlippage || !maxSlippage
const allowed = shouldUseAutoSlippage ? autoSlippage : maxSlippage ?? autoSlippage
const warning = auto ? undefined : getSlippageWarning(allowed)
if (auto && allowed === DEFAULT_AUTO_SLIPPAGE) {
return DEFAULT_SLIPPAGE
}
return { auto, allowed, warning }
}, [autoSlippage, maxSlippage, shouldUseAutoSlippage])
}

View File

@@ -1,10 +1,8 @@
import { NativeCurrency, Token } from '@uniswap/sdk-core'
import { TokenInfo, TokenList } from '@uniswap/token-lists'
import { atom, useAtom } from 'jotai'
import { useAtomValue } from 'jotai/utils'
import useActiveWeb3React from 'lib/hooks/useActiveWeb3React'
import resolveENSContentHash from 'lib/utils/resolveENSContentHash'
import { useCallback, useEffect, useMemo, useState } from 'react'
import { createContext, PropsWithChildren, useCallback, useContext, useEffect, useMemo, useState } from 'react'
import { WrappedTokenInfo } from 'state/lists/wrappedTokenInfo'
import fetchTokenList from './fetchTokenList'
@@ -14,19 +12,61 @@ import { validateTokens } from './validateTokenList'
export const DEFAULT_TOKEN_LIST = 'https://gateway.ipfs.io/ipns/tokens.uniswap.org'
const chainTokenMapAtom = atom<ChainTokenMap | null>(null)
const MISSING_PROVIDER = Symbol()
const ChainTokenMapContext = createContext<ChainTokenMap | undefined | typeof MISSING_PROVIDER>(MISSING_PROVIDER)
export function useIsTokenListLoaded() {
return Boolean(useAtomValue(chainTokenMapAtom))
function useChainTokenMapContext() {
const chainTokenMap = useContext(ChainTokenMapContext)
if (chainTokenMap === MISSING_PROVIDER) {
throw new Error('TokenList hooks must be wrapped in a <TokenListProvider>')
}
return chainTokenMap
}
export function useSyncTokenList(list: string | TokenInfo[] = DEFAULT_TOKEN_LIST): void {
export function useIsTokenListLoaded() {
return Boolean(useChainTokenMapContext())
}
export default function useTokenList(): WrappedTokenInfo[] {
const { chainId } = useActiveWeb3React()
const chainTokenMap = useChainTokenMapContext()
const tokenMap = chainId && chainTokenMap?.[chainId]
return useMemo(() => {
if (!tokenMap) return []
return Object.values(tokenMap).map(({ token }) => token)
}, [tokenMap])
}
export type TokenMap = { [address: string]: Token }
export function useTokenMap(): TokenMap {
const { chainId } = useActiveWeb3React()
const chainTokenMap = useChainTokenMapContext()
const tokenMap = chainId && chainTokenMap?.[chainId]
return useMemo(() => {
if (!tokenMap) return {}
return Object.entries(tokenMap).reduce((map, [address, { token }]) => {
map[address] = token
return map
}, {} as TokenMap)
}, [tokenMap])
}
export function useQueryCurrencies(query = ''): (WrappedTokenInfo | NativeCurrency)[] {
return useQueryTokens(query, useTokenList())
}
export function TokenListProvider({
list = DEFAULT_TOKEN_LIST,
children,
}: PropsWithChildren<{ list?: string | TokenInfo[] }>) {
// Error boundaries will not catch (non-rendering) async errors, but it should still be shown
const [error, setError] = useState<Error>()
if (error) throw error
const [chainTokenMap, setChainTokenMap] = useAtom(chainTokenMapAtom)
useEffect(() => setChainTokenMap(null), [list, setChainTokenMap])
const [chainTokenMap, setChainTokenMap] = useState<ChainTokenMap>()
useEffect(() => setChainTokenMap(undefined), [list])
const { chainId, library } = useActiveWeb3React()
const resolver = useCallback(
@@ -70,34 +110,7 @@ export function useSyncTokenList(list: string | TokenInfo[] = DEFAULT_TOKEN_LIST
}
}
}
}, [chainTokenMap, list, resolver, setChainTokenMap])
}
}, [chainTokenMap, list, resolver])
export default function useTokenList(): WrappedTokenInfo[] {
const { chainId } = useActiveWeb3React()
const chainTokenMap = useAtomValue(chainTokenMapAtom)
const tokenMap = chainId && chainTokenMap?.[chainId]
return useMemo(() => {
if (!tokenMap) return []
return Object.values(tokenMap).map(({ token }) => token)
}, [tokenMap])
}
export type TokenMap = { [address: string]: Token }
export function useTokenMap(): TokenMap {
const { chainId } = useActiveWeb3React()
const chainTokenMap = useAtomValue(chainTokenMapAtom)
const tokenMap = chainId && chainTokenMap?.[chainId]
return useMemo(() => {
if (!tokenMap) return {}
return Object.entries(tokenMap).reduce((map, [address, { token }]) => {
map[address] = token
return map
}, {} as TokenMap)
}, [tokenMap])
}
export function useQueryCurrencies(query = ''): (WrappedTokenInfo | NativeCurrency)[] {
return useQueryTokens(query, useTokenList())
return <ChainTokenMapContext.Provider value={chainTokenMap}>{children}</ChainTokenMapContext.Provider>
}

View File

@@ -5,8 +5,9 @@ import { computeFiatValuePriceImpact } from 'utils/computeFiatValuePriceImpact'
import { getPriceImpactWarning } from 'utils/prices'
export interface PriceImpact {
display?: string
percent: Percent
warning?: 'warning' | 'error'
toString(): string
}
/**
@@ -19,22 +20,20 @@ export default function useUSDCPriceImpact(
): {
inputUSDC?: CurrencyAmount<Token>
outputUSDC?: CurrencyAmount<Token>
priceImpact: PriceImpact
impact?: PriceImpact
} {
const inputUSDC = useUSDCValue(inputAmount) ?? undefined
const outputUSDC = useUSDCValue(outputAmount) ?? undefined
return useMemo(() => {
const priceImpact = computeFiatValuePriceImpact(inputUSDC, outputUSDC)
const warning = getPriceImpactWarning(priceImpact)
return {
inputUSDC,
outputUSDC,
priceImpact: {
priceImpact,
display: priceImpact && toHumanReadablePriceImpact(priceImpact),
warning,
},
}
const impact = priceImpact
? {
percent: priceImpact,
warning: getPriceImpactWarning(priceImpact),
toString: () => toHumanReadablePriceImpact(priceImpact),
}
: undefined
return { inputUSDC, outputUSDC, impact }
}, [inputUSDC, outputUSDC])
}

Some files were not shown because too many files have changed in this diff Show More