Compare commits

...

102 Commits

Author SHA1 Message Date
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
Zach Pomerantz
377026bca8 fix: summary expando height (#3556) 2022-03-19 17:05:56 -05:00
Zach Pomerantz
9470c49d11 fix: fix loading delays for cached data (#3549)
* fix: loading transition

* fix: add check to usePoll
2022-03-18 11:03:26 -07:00
Ian Lapham
e1abd81a1d fix: add syncing state to trade loading (#3545)
* add syncing state to trade loading

* remove log statement

* update loading state

* update useLast trade logic

* nit fixes
2022-03-18 10:11:16 -07:00
Zach Pomerantz
7d9657867d fix: polling lag (#3543) 2022-03-18 08:52:55 -07:00
Zach Pomerantz
7cc52abb96 fix: cache computed pool addresses (#3537) 2022-03-18 08:00:35 -07:00
Crowdin Bot
5ac41417b0 chore(i18n): synchronize translations from crowdin [skip ci] 2022-03-18 00:15:22 +00:00
Zach Pomerantz
2c74c5f2df chore: include usdc in swap info (#3539)
* chore: refactor useComputeSwapInfo to include usdc

* chore: use passed usdc if able

* fix: fixture
2022-03-17 16:51:41 -07:00
Ian Lapham
cbc2ff668e disable swap button if trade not populated (#3542) 2022-03-17 16:26:37 -07:00
Zach Pomerantz
a73f59b4ff fix: only debounce input amounts (#3540) 2022-03-17 15:42:32 -07:00
Zach Pomerantz
7a75626c31 chore: bump to v0.0.25-beta (#3538) 2022-03-17 14:17:58 -07:00
Jordan Frankfurt
a0e14bef10 fix(vote): allow 0-vote proposals to render (#3536) 2022-03-17 15:37:36 -05:00
Yadong Zhang
9b5a53b2e8 fix: typeError: Cannot read properties of undefined (reading 'split') (#3518) 2022-03-17 14:30:13 -04:00
Crowdin Bot
50fdb36b6f chore(i18n): synchronize translations from crowdin [skip ci] 2022-03-17 04:06:49 +00:00
Ian Lapham
b993902c73 remove donation popup (#3532) 2022-03-16 20:42:06 -07:00
Zach Pomerantz
828bf540ba feat: usePoll (#3530)
* feat: usePoll

* chore: comments

* fix: cleanup

* fix: review updates

* fix: lint nits
2022-03-16 14:54:17 -07:00
Zach Pomerantz
7c88a5a008 chore: bump to v0.0.24-beta (#3524) 2022-03-15 22:31:41 -07:00
Zach Pomerantz
360c5e2c96 fix: fallback to eth_accounts in eip1193 bridge (#3516) 2022-03-15 09:42:05 -07:00
Ian Lapham
72678ee667 Update token list (#3520) 2022-03-15 10:53:20 -04:00
Zach Pomerantz
4dd74f2144 fix: token img overflow (#3517) 2022-03-15 08:01:32 -05:00
Zach Pomerantz
e45c104135 fix: computed token img (#3519) 2022-03-15 08:00:46 -05:00
Crowdin Bot
98fcaacd9b chore(i18n): synchronize translations from crowdin [skip ci] 2022-03-15 09:06:57 +00:00
Crowdin Bot
93551579e4 chore(i18n): synchronize translations from crowdin [skip ci] 2022-03-14 20:11:46 +00:00
Zach Pomerantz
8a9388ed81 feat: make Expando element (#3515)
* feat: make Expando element

* fix: cleanup

* fix: simplify margin

* fix: summary height

* fix: special case gap transition
2022-03-14 12:03:40 -07:00
Ian Lapham
884bf41da7 fix: add check for undefined slot0 and liquidity call results (#3513)
* add check for undefined slot0 and liquidity call results

* update null check

* update code order
2022-03-14 14:02:03 -04:00
Zach Pomerantz
5b686aea97 fix: exhaustive dep (#3514) 2022-03-14 10:03:19 -07:00
Zach Pomerantz
c4a456a085 chore: simplify Price in its own component (#3508)
* chore: simplify Price in its own component

* fix: restore reversal

* fix: output usdc naming
2022-03-14 09:54:04 -07:00
Zach Pomerantz
4b9098a7bf fix: token button overflow (#3504)
Removes the overflow property from TokenButton, which prevents the vertical scrollbar from rendering.
2022-03-14 09:42:38 -07:00
Zach Pomerantz
71a246f25c fix: expando icon direction (#3507) 2022-03-14 09:42:24 -07:00
Zach Pomerantz
8de048bc84 fix: input amount lag (#3503)
Propagates the exact amount to the trade currency amount without waiting for the trade to be computed. This allows things like insufficient balance or approval to be reflected in the UI while typing, instead of having a lag.
2022-03-13 22:45:14 -07:00
Zach Pomerantz
163e2d5560 fix: override Rule width (#3502)
* fix: override Rule width

* fix: include max-width
2022-03-11 17:26:02 -06:00
Zach Pomerantz
0edb0fe5e2 fix: memoize on-chain results (#3493)
* fix: memo-ize onchain results

* fix: typeof omission
2022-03-11 14:56:05 -08:00
Zach Pomerantz
496408b3db fix: token list fetching performance (#3480)
* fix: token list fetching

* fix: simplify naming
2022-03-11 11:57:49 -08:00
Zach Pomerantz
78b6f5c72d fix: destructure currencies for ref equality (#3498) 2022-03-11 11:25:17 -08:00
Zach Pomerantz
f9fb71a803 fix(perf): cache pools to avoid cost of instantiation (#3499) 2022-03-11 08:20:16 -08:00
Crowdin Bot
59d0046411 chore(i18n): synchronize translations from crowdin [skip ci] 2022-03-11 03:07:34 +00:00
Zach Pomerantz
b4e0234d07 fix: avoid summary wrap (#3497) 2022-03-10 15:59:45 -08:00
Zach Pomerantz
4a8dbda0b8 fix: memoize nested components (#3483) 2022-03-10 15:48:45 -08:00
Crowdin Bot
0cbb24c614 chore(i18n): synchronize translations from crowdin [skip ci] 2022-03-10 23:06:32 +00:00
Ian Lapham
a9dba258ff fix: update wrap fail state callback and input values (#3495)
* update wrap fail state callback and input values

* add error check for wrap status

* update error catching logic

* update error catching
2022-03-10 17:07:52 -05:00
Zach Pomerantz
fa163cb938 feat: price impact update (#3496)
* feat: green text on neg price impact

* chore: propagate all of usdc price impact

* chore: pass price impact to summary details

* chore: propagate slippage and impact warnings

* feat: update warnings on summary dialog

* chore: rm todo
2022-03-10 13:07:37 -08:00
Curly Brackets
b52273932a fix(ui): Auto Router icon not appearing on safari/ios (#3444)
* fix(ui): Auto Router icon not appearing on safari/ios

Closes #3138

* refactor: remove auto-router svg that are no longer in use
2022-03-10 14:37:32 -05:00
Crowdin Bot
9ad8f80e4e chore(i18n): synchronize translations from crowdin [skip ci] 2022-03-10 18:07:32 +00:00
Zach Pomerantz
69bc598dea feat: display "no results found" message (#3467)
* feat: display when no results

* chore: use body not subhead
2022-03-10 09:53:56 -08:00
Ian Lapham
7feba045fc fix: add formatting to USDC values (#3478)
* add formatting to USDC values

* update formatting of USDC amounts to en

* apply same formatting to summary
2022-03-10 12:38:59 -05:00
Zach Pomerantz
04cee0a07d chore: bump to v0.0.23-beta (#3492) 2022-03-10 09:02:17 -08:00
Zach Pomerantz
ea73260e56 fix: accomodate longer locales (#3468) 2022-03-10 08:57:36 -08:00
Zach Pomerantz
b4bd2973a9 fix: update input when prefixing 0 to decimal (#3487) 2022-03-10 08:56:06 -08:00
Zach Pomerantz
a071b8adb0 fix: summary line break (#3486) 2022-03-10 08:55:55 -08:00
Zach Pomerantz
610acb0191 fix: font family overrides (#3485) 2022-03-10 08:55:40 -08:00
Zach Pomerantz
63bad8f890 fix: summary expando height (#3484) 2022-03-10 08:54:42 -08:00
Zach Pomerantz
32f955693f fix: restructure web3 to memoize (#3472) 2022-03-09 15:08:04 -08:00
Zach Pomerantz
96c66a5846 fix: incorrectly memoized hooks (#3471)
* fix: incorrectly memoized hooks

* fix: finish memoizing useUSDCPrice
2022-03-09 12:33:35 -08:00
Sara Reynolds
8c269a6d39 feat: add optimism and arbitrum to gas estimate support (#3434)
* add optimism and arbitrum to gas estimate support

* update sor version

* move to dependencies
2022-03-09 14:52:19 -05:00
Justin Domingue
36f111fa6f chore: upgrade to redux-toolkit 1.8 (#3464) 2022-03-09 12:19:24 -05:00
158 changed files with 4191 additions and 4164 deletions

View File

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

45
INTERFACE_README.md Normal file
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, font, border radius, and more) to match the style of your application. You can also configure your own default token list and optionally set a convenience fee on swaps executed through the widget on your site.
## Installation
Install the widgets library via `npm` or `yarn`. If you do not already use the widget's peerDependencies `redux` and `react-redux`, then you'll need to add them as well.
```js
yarn add @uniswap/widgets redux react-redux
```
```js
npm i --save @uniswap/widgets redux react-redux
```
## Documentation
- [overview](https://docs.uniswap.org/sdk/widgets/swap-widget)
- [api reference](https://docs.uniswap.org/sdk/widgets/swap-widget/api)
## Example Apps
Uniswap Labs maintains two demo apps in branches of the [widgets-demo](https://github.com/Uniswap/widgets-demo) repo:
- [NextJS](https://github.com/Uniswap/widgets-demo/tree/nextjs)
- [Create React App](https://github.com/Uniswap/widgets-demo/tree/cra)
Others have also also released the widget in production to their userbase:
- [OpenSea](https://opensea.io/)
- [Friends With Benefits](https://www.fwb.help/)
- [Oasis](https://oasis.app/)
## Legal notice
Uniswap Labs encourages integrators to evaluate their own regulatory obligations when integrating this widget into their products, including, but not limited to, those related to economic or trade sanctions compliance.

View File

@@ -1,6 +1,6 @@
{
"name": "@uniswap/widgets",
"version": "0.0.22-beta",
"version": "0.0.26-beta",
"description": "Uniswap Interface",
"homepage": ".",
"files": [
@@ -30,6 +30,7 @@
"@babel/preset-env": "^7.16.11",
"@babel/preset-react": "^7.16.7",
"@babel/preset-typescript": "^7.16.7",
"@ethersproject/experimental": "^5.4.0",
"@gnosis.pm/safe-apps-web3-react": "^0.6.0",
"@graphql-codegen/cli": "1.21.5",
"@graphql-codegen/typescript": "1.22.3",
@@ -89,8 +90,8 @@
"@uniswap/v2-periphery": "^1.1.0-beta.0",
"@uniswap/v3-core": "1.0.0",
"@uniswap/v3-periphery": "^1.1.1",
"@web3-react/metamask": "8.0.13-beta.0",
"@web3-react/walletconnect": "8.0.18-beta.0",
"@web3-react/metamask": "^8.0.18-beta.0",
"@web3-react/walletconnect": "^8.0.25-beta.0",
"array.prototype.flat": "^1.2.4",
"array.prototype.flatmap": "^1.2.4",
"babel-plugin-macros": "^3.1.0",
@@ -118,7 +119,7 @@
"react-confetti": "^6.0.0",
"react-cosmos": "^5.6.6",
"react-dom": "^17.0.1",
"react-ga": "^2.5.7",
"react-ga4": "^1.4.1",
"react-is": "^17.0.2",
"react-markdown": "^4.3.1",
"react-redux": "^7.2.2",
@@ -193,39 +194,25 @@
"license": "GPL-3.0-or-later",
"dependencies": {
"@babel/runtime": "^7.17.0",
"@ethersproject/abi": "^5.4.1",
"@ethersproject/abstract-provider": "^5.4.1",
"@ethersproject/abstract-signer": "^5.4.1",
"@ethersproject/address": "^5.4.0",
"@ethersproject/bignumber": "^5.4.2",
"@ethersproject/bytes": "^5.4.0",
"@ethersproject/constants": "^5.4.0",
"@ethersproject/contracts": "^5.4.1",
"@ethersproject/experimental": "^5.4.0",
"@ethersproject/hash": "^5.4.0",
"@ethersproject/providers": "^5.4.0",
"@ethersproject/solidity": "^5.4.0",
"@ethersproject/strings": "^5.4.0",
"@ethersproject/units": "^5.4.0",
"@ethersproject/wallet": "^5.4.0",
"@fontsource/ibm-plex-mono": "^4.5.1",
"@fontsource/inter": "^4.5.1",
"@popperjs/core": "^2.4.4",
"@reduxjs/toolkit": "^1.6.1",
"@uniswap/redux-multicall": "^1.0.0",
"@uniswap/redux-multicall": "^1.1.1",
"@uniswap/router-sdk": "^1.0.3",
"@uniswap/sdk-core": "^3.0.1",
"@uniswap/smart-order-router": "^2.5.10",
"@uniswap/smart-order-router": "^2.5.26",
"@uniswap/token-lists": "^1.0.0-beta.27",
"@uniswap/v2-sdk": "^3.0.1",
"@uniswap/v3-sdk": "^3.8.2",
"@web3-react/core": "8.0.17-beta.0",
"@web3-react/eip1193": "8.0.12-beta.0",
"@web3-react/empty": "8.0.10-beta.0",
"@web3-react/types": "8.0.10-beta.0",
"@web3-react/url": "8.0.12-beta.0",
"@web3-react/core": "^8.0.22-beta.0",
"@web3-react/eip1193": "^8.0.17-beta.0",
"@web3-react/empty": "^8.0.11-beta.0",
"@web3-react/types": "^8.0.11-beta.0",
"@web3-react/url": "^8.0.16-beta.0",
"ajv": "^6.12.3",
"cids": "^1.0.0",
"ethers": "^5.1.4",
"immer": "^9.0.6",
"jotai": "^1.3.7",
"jsbi": "^3.1.4",

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

@@ -1,10 +0,0 @@
<svg width="23" height="20" viewBox="0 0 23 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<defs>
<linearGradient id="gradient1" x1="0" y1="0" x2="1" y2="0" gradientTransform="rotate(95)">
<stop id="stop1" offset="0" stop-color="#2274E2"/>
<stop id="stop1" offset="0.5" stop-color="#2274E2"/>
<stop id="stop2" offset="1" stop-color="#3FB672" />
</linearGradient>
</defs>
<path d="M16 16C10 16 9 10 5 10M16 16C16 17.6569 17.3431 19 19 19C20.6569 19 22 17.6569 22 16C22 14.3431 20.6569 13 19 13C17.3431 13 16 14.3431 16 16ZM5 10C9 10 10 4 16 4M5 10H1.5M16 4C16 5.65685 17.3431 7 19 7C20.6569 7 22 5.65685 22 4C22 2.34315 20.6569 1 19 1C17.3431 1 16 2.34315 16 4Z" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" stroke="url(#gradient1)" />
</svg>

Before

Width:  |  Height:  |  Size: 782 B

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,11 @@ type ErrorBoundaryState = {
const IS_UNISWAP = window.location.hostname === 'app.uniswap.org'
async function updateServiceWorker(): Promise<void> {
const ready = await navigator.serviceWorker.ready
await ready.update()
}
export default class ErrorBoundary extends React.Component<unknown, ErrorBoundaryState> {
constructor(props: unknown) {
super(props)
@@ -56,15 +61,18 @@ export default class ErrorBoundary extends React.Component<unknown, ErrorBoundar
}
static getDerivedStateFromError(error: Error): ErrorBoundaryState {
updateServiceWorker()
.then(() => {
window.location.reload()
})
.catch((error) => {
console.error('Failed to update service worker', error)
})
return { error }
}
componentDidCatch(error: Error, errorInfo: ErrorInfo) {
ReactGA.exception({
...error,
...errorInfo,
fatal: true,
})
ReactGA.event('exception', { description: error.toString() + errorInfo.toString(), fatal: true })
}
render() {

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

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

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

@@ -1,139 +0,0 @@
import { Trans } from '@lingui/macro'
import FlagImage from 'assets/images/ukraine.png'
import { AutoColumn } from 'components/Column'
import { RowBetween, RowFixed } from 'components/Row'
import { X } from 'react-feather'
import ReactGA from 'react-ga'
import { useDarkModeManager, useShowDonationLink } from 'state/user/hooks'
import styled from 'styled-components/macro'
import { ExternalLink, ThemedText, Z_INDEX } from 'theme'
const darkGradient = `radial-gradient(87.53% 3032.45% at 5.16% 10.13%, rgba(255, 255, 255, 0.2) 0%, rgba(255, 255, 255, 0) 100%),
linear-gradient(0deg, rgba(0, 91, 187, 0.35), rgba(0, 91, 187, 0.35)), #000000;`
const lightGradient = `radial-gradient(87.53% 3032.45% at 5.16% 10.13%, rgba(255, 255, 255, 0.2) 0%, rgba(255, 255, 255, 0) 100%), linear-gradient(0deg, #CBE4FF, #CBE4FF), linear-gradient(0deg, rgba(255, 255, 255, 0.09), rgba(255, 255, 255, 0.09)), radial-gradient(100% 93.36% at 0% 6.64%, #8BC4FF 0%, #FFF5BF 100%);`
const Wrapper = styled(AutoColumn)<{ darkMode: boolean }>`
background: #edeef2;
position: fixed;
bottom: 40px;
border-radius: 12px;
padding: 18px;
max-width: 360px;
background: ${({ darkMode }) => (darkMode ? darkGradient : lightGradient)};
color: ${({ theme }) => theme.text1};
z-index: ${Z_INDEX.deprecated_content};
:hover {
opacity: 0.8;
}
& > * {
z-index: ${Z_INDEX.fixed};
}
overflow: hidden;
:before {
background-image: url(${FlagImage});
background-repeat: no-repeat;
overflow: hidden;
background-size: 300px;
content: '';
height: 1200px;
width: 400px;
opacity: 0.1;
position: absolute;
transform: rotate(25deg) translate(-140px, -60px);
width: 300px;
z-index: ${Z_INDEX.deprecated_zero};
}
${({ theme }) => theme.mediaWidth.upToSmall`
max-width: 100%;
`}
${({ theme }) => theme.mediaWidth.upToMedium`
position: relative;
bottom: unset;
`}
`
const WrappedCloseIcon = styled(X)`
stroke: ${({ theme }) => theme.text2};
z-index: ${Z_INDEX.tooltip};
:hover {
cursor: pointer;
opacity: 0.8;
}
`
export const StyledFlagImage = styled.div`
margin-right: 12px;
width: 18px;
height: 18px;
border-radius: 100%;
&:before,
&:after {
content: '';
width: 9px;
height: 18px;
}
&:before {
float: left;
border-top-left-radius: 9px;
border-bottom-left-radius: 9px;
background: #005bbb;
}
&:after {
float: right;
border-top-right-radius: 9px;
border-bottom-right-radius: 9px;
background: #ffd500;
}
transform: rotate(90deg);
`
const StyledLink = styled(ExternalLink)`
text-decoration: none !important;
`
export default function DonationLink() {
const [darkMode] = useDarkModeManager()
const [, setVisible] = useShowDonationLink()
return (
<Wrapper
gap="10px"
darkMode={darkMode}
as={StyledLink}
target="https://donate.uniswap.org/#/swap"
href="https://donate.uniswap.org/#/swap"
onClickCapture={() => {
ReactGA.event({
category: 'Donate',
action: 'Link to Ukraine site.',
})
}}
>
<RowBetween>
<RowFixed>
<StyledFlagImage />
<ThemedText.Body fontWeight={600} fontSize={'18px'}>
<Trans>Donate to Ukraine</Trans>
</ThemedText.Body>
</RowFixed>
<WrappedCloseIcon
onClick={(e) => {
e.preventDefault()
e.stopPropagation()
setVisible(false)
return false
}}
/>
</RowBetween>
<ThemedText.Body fontWeight={400} fontSize="12px" color="text2">
<Trans>Directly support the Ukrainian government by donating tokens.</Trans>
</ThemedText.Body>
</Wrapper>
)
}

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

@@ -1,14 +1,12 @@
import { SupportedChainId } from 'constants/chains'
import useActiveWeb3React from 'hooks/useActiveWeb3React'
import { useLocation } from 'react-router-dom'
import styled from 'styled-components/macro'
import { MEDIA_WIDTHS } from 'theme'
import { useActivePopups } from '../../state/application/hooks'
import { useShowDonationLink, useURLWarningVisible } from '../../state/user/hooks'
import { useURLWarningVisible } from '../../state/user/hooks'
import { AutoColumn } from '../Column'
import ClaimPopup from './ClaimPopup'
import DonationLink from './DonationLink'
import PopupItem from './PopupItem'
const MobilePopupWrapper = styled.div<{ height: string | number }>`
@@ -68,11 +66,6 @@ export default function Popups() {
const { chainId } = useActiveWeb3React()
const isNotOnMainnet = Boolean(chainId && chainId !== SupportedChainId.MAINNET)
const location = useLocation()
const isOnSwapPage = location.pathname.includes('swap')
const [donationVisible] = useShowDonationLink()
const showDonation = donationVisible && isOnSwapPage
return (
<>
<FixedPopupColumn gap="20px" extraPadding={urlWarningActive} xlPadding={isNotOnMainnet}>
@@ -80,11 +73,9 @@ export default function Popups() {
{activePopups.map((item) => (
<PopupItem key={item.key} content={item.content} popKey={item.key} removeAfterMs={item.removeAfterMs} />
))}
{showDonation ? <DonationLink /> : null}
</FixedPopupColumn>
<MobilePopupWrapper height={activePopups?.length > 0 || showDonation ? 'fit-content' : 0}>
<MobilePopupWrapper height={activePopups?.length > 0 ? 'fit-content' : 0}>
<MobilePopupInner>
{showDonation ? <DonationLink /> : null}
{activePopups // reverse so new items up front
.slice(0)
.reverse()

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'

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

@@ -0,0 +1,35 @@
import { useRef } from 'react'
let uniqueId = 0
const getUniqueId = () => uniqueId++
export default function AutoRouterIcon({ className, id }: { className?: string; id?: string }) {
const componentIdRef = useRef(id ?? getUniqueId())
const componentId = `AutoRouterIconGradient${componentIdRef.current}`
return (
<svg
width="23"
height="20"
viewBox="0 0 23 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className={className}
>
<defs>
<linearGradient id={componentId} x1="0" y1="0" x2="1" y2="0" gradientTransform="rotate(95)">
<stop id="stop1" offset="0" stopColor="#2274E2" />
<stop id="stop1" offset="0.5" stopColor="#2274E2" />
<stop id="stop2" offset="1" stopColor="#3FB672" />
</linearGradient>
</defs>
<path
d="M16 16C10 16 9 10 5 10M16 16C16 17.6569 17.3431 19 19 19C20.6569 19 22 17.6569 22 16C22 14.3431 20.6569 13 19 13C17.3431 13 16 14.3431 16 16ZM5 10C9 10 10 4 16 4M5 10H1.5M16 4C16 5.65685 17.3431 7 19 7C20.6569 7 22 5.65685 22 4C22 2.34315 20.6569 1 19 1C17.3431 1 16 2.34315 16 4Z"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
stroke={`url(#${componentId})`}
/>
</svg>
)
}

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,8 +3,8 @@ import useAutoRouterSupported from 'hooks/useAutoRouterSupported'
import styled from 'styled-components/macro'
import { ThemedText } from 'theme'
import { ReactComponent as AutoRouterIcon } from '../../assets/svg/auto_router.svg'
import { ReactComponent as StaticRouterIcon } from '../../assets/svg/static_route.svg'
import AutoRouterIcon from './AutoRouterIcon'
const StyledAutoRouterIcon = styled(AutoRouterIcon)`
height: 16px;

View File

@@ -39,7 +39,12 @@ export const ALL_SUPPORTED_CHAIN_IDS: SupportedChainId[] = Object.values(Support
(id) => typeof id === 'number'
) as SupportedChainId[]
export const SUPPORTED_GAS_ESTIMATE_CHAIN_IDS = [SupportedChainId.MAINNET, SupportedChainId.POLYGON]
export const SUPPORTED_GAS_ESTIMATE_CHAIN_IDS = [
SupportedChainId.MAINNET,
SupportedChainId.POLYGON,
SupportedChainId.OPTIMISM,
SupportedChainId.ARBITRUM_ONE,
]
/**
* All the chain IDs that are running the Ethereum protocol.

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

@@ -1108,6 +1108,13 @@
"name": "Ukraine DAO",
"symbol": "Ukraine",
"decimals": 18
},
{
"chainId": 1,
"address": "0x8290D7a64F25e6b5002d98367E8367c1b532b534",
"name": "oneUNI",
"symbol": "oneUNI",
"decimals": 18
}
]
}

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

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

@@ -36,21 +36,8 @@ export function useBestTrade(
debouncedOtherCurrency
)
const isLoading = amountSpecified !== undefined && debouncedAmount === undefined
// consider trade debouncing when inputs/outputs do not match
const debouncing =
routingAPITrade.trade &&
amountSpecified &&
(tradeType === TradeType.EXACT_INPUT
? !routingAPITrade.trade.inputAmount.equalTo(amountSpecified) ||
!amountSpecified.currency.equals(routingAPITrade.trade.inputAmount.currency) ||
!debouncedOtherCurrency?.equals(routingAPITrade.trade.outputAmount.currency)
: !routingAPITrade.trade.outputAmount.equalTo(amountSpecified) ||
!amountSpecified.currency.equals(routingAPITrade.trade.outputAmount.currency) ||
!debouncedOtherCurrency?.equals(routingAPITrade.trade.inputAmount.currency))
const useFallback = !autoRouterSupported || (!debouncing && routingAPITrade.state === TradeState.NO_ROUTE_FOUND)
const isLoading = routingAPITrade.state === TradeState.LOADING
const useFallback = !autoRouterSupported || routingAPITrade.state === TradeState.NO_ROUTE_FOUND
// only use client side router if routing api trade failed or is not supported
const bestV3Trade = useClientSideV3Trade(
@@ -63,9 +50,8 @@ export function useBestTrade(
return useMemo(
() => ({
...(useFallback ? bestV3Trade : routingAPITrade),
...(debouncing ? { state: TradeState.SYNCING } : {}),
...(isLoading ? { state: TradeState.LOADING } : {}),
}),
[bestV3Trade, debouncing, isLoading, routingAPITrade, useFallback]
[bestV3Trade, isLoading, routingAPITrade, useFallback]
)
}

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

@@ -1,10 +1,15 @@
import { BigNumber } from '@ethersproject/bignumber'
import { useSingleCallResult } from 'lib/hooks/multicall'
import { useMemo } from 'react'
import { useInterfaceMulticall } from './useContract'
// gets the current timestamp from the blockchain
export default function useCurrentBlockTimestamp(): BigNumber | undefined {
const multicall = useInterfaceMulticall()
return useSingleCallResult(multicall, 'getCurrentBlockTimestamp')?.result?.[0]
const resultStr: string | undefined = useSingleCallResult(
multicall,
'getCurrentBlockTimestamp'
)?.result?.[0]?.toString()
return useMemo(() => (typeof resultStr === 'string' ? BigNumber.from(resultStr) : undefined), [resultStr])
}

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

@@ -1,5 +1,6 @@
import JSBI from 'jsbi'
import { useSingleCallResult } from 'lib/hooks/multicall'
import { useMemo } from 'react'
import { useContract } from './useContract'
import useENSAddress from './useENSAddress'
@@ -22,5 +23,5 @@ export default function useGasPrice(): JSBI | undefined {
const contract = useContract(address ?? undefined, CHAIN_DATA_ABI, false)
const resultStr = useSingleCallResult(contract, 'latestAnswer').result?.[0]?.toString()
return typeof resultStr === 'string' ? JSBI.BigInt(resultStr) : undefined
return useMemo(() => (typeof resultStr === 'string' ? JSBI.BigInt(resultStr) : undefined), [resultStr])
}

View File

@@ -9,5 +9,5 @@ export default function useIsArgentWallet(): boolean {
const argentWalletDetector = useArgentWalletDetectorContract()
const inputs = useMemo(() => [account ?? undefined], [account])
const call = useSingleCallResult(argentWalletDetector, 'isArgentWallet', inputs, NEVER_RELOAD)
return call?.result?.[0] ?? false
return Boolean(call?.result?.[0])
}

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,19 +1,81 @@
import { Interface } from '@ethersproject/abi'
import { Currency, Token } from '@uniswap/sdk-core'
import IUniswapV3PoolStateJson from '@uniswap/v3-core/artifacts/contracts/interfaces/pool/IUniswapV3PoolState.sol/IUniswapV3PoolState.json'
import { BigintIsh, Currency, Token } from '@uniswap/sdk-core'
import { abi as IUniswapV3PoolStateABI } from '@uniswap/v3-core/artifacts/contracts/interfaces/pool/IUniswapV3PoolState.sol/IUniswapV3PoolState.json'
import { computePoolAddress } from '@uniswap/v3-sdk'
import { FeeAmount, Pool } from '@uniswap/v3-sdk'
import useActiveWeb3React from 'hooks/useActiveWeb3React'
import JSBI from 'jsbi'
import { useMultipleContractSingleData } from 'lib/hooks/multicall'
import { useMemo } from 'react'
import { V3_CORE_FACTORY_ADDRESSES } from '../constants/addresses'
import { IUniswapV3PoolStateInterface } from '../types/v3/IUniswapV3PoolState'
const { abi: IUniswapV3PoolStateABI } = IUniswapV3PoolStateJson
const POOL_STATE_INTERFACE = new Interface(IUniswapV3PoolStateABI) as IUniswapV3PoolStateInterface
// Classes are expensive to instantiate, so this caches the recently instantiated pools.
// This avoids re-instantiating pools as the other pools in the same request are loaded.
class PoolCache {
// Evict after 128 entries. Empirically, a swap uses 64 entries.
private static MAX_ENTRIES = 128
// These are FIFOs, using unshift/pop. This makes recent entries faster to find.
private static pools: Pool[] = []
private static addresses: { key: string; address: string }[] = []
static getPoolAddress(factoryAddress: string, tokenA: Token, tokenB: Token, fee: FeeAmount): string {
if (this.addresses.length > this.MAX_ENTRIES) {
this.addresses = this.addresses.slice(0, this.MAX_ENTRIES / 2)
}
const { address: addressA } = tokenA
const { address: addressB } = tokenB
const key = `${factoryAddress}:${addressA}:${addressB}:${fee.toString()}`
const found = this.addresses.find((address) => address.key === key)
if (found) return found.address
const address = {
key,
address: computePoolAddress({
factoryAddress,
tokenA,
tokenB,
fee,
}),
}
this.addresses.unshift(address)
return address.address
}
static getPool(
tokenA: Token,
tokenB: Token,
fee: FeeAmount,
sqrtPriceX96: BigintIsh,
liquidity: BigintIsh,
tick: number
): Pool {
if (this.pools.length > this.MAX_ENTRIES) {
this.pools = this.pools.slice(0, this.MAX_ENTRIES / 2)
}
const found = this.pools.find(
(pool) =>
pool.token0 === tokenA &&
pool.token1 === tokenB &&
pool.fee === fee &&
JSBI.EQ(pool.sqrtRatioX96, sqrtPriceX96) &&
JSBI.EQ(pool.liquidity, liquidity) &&
pool.tickCurrent === tick
)
if (found) return found
const pool = new Pool(tokenA, tokenB, fee, sqrtPriceX96, liquidity, tick)
this.pools.unshift(pool)
return pool
}
}
export enum PoolState {
LOADING,
NOT_EXISTS,
@@ -26,58 +88,57 @@ export function usePools(
): [PoolState, Pool | null][] {
const { chainId } = useActiveWeb3React()
const transformed: ([Token, Token, FeeAmount] | null)[] = useMemo(() => {
return poolKeys.map(([currencyA, currencyB, feeAmount]) => {
if (!chainId || !currencyA || !currencyB || !feeAmount) return null
const poolTokens: ([Token, Token, FeeAmount] | undefined)[] = useMemo(() => {
if (!chainId) return new Array(poolKeys.length)
const tokenA = currencyA?.wrapped
const tokenB = currencyB?.wrapped
if (!tokenA || !tokenB || tokenA.equals(tokenB)) return null
const [token0, token1] = tokenA.sortsBefore(tokenB) ? [tokenA, tokenB] : [tokenB, tokenA]
return [token0, token1, feeAmount]
return poolKeys.map(([currencyA, currencyB, feeAmount]) => {
if (currencyA && currencyB && feeAmount) {
const tokenA = currencyA.wrapped
const tokenB = currencyB.wrapped
if (tokenA.equals(tokenB)) return undefined
return tokenA.sortsBefore(tokenB) ? [tokenA, tokenB, feeAmount] : [tokenB, tokenA, feeAmount]
}
return undefined
})
}, [chainId, poolKeys])
const poolAddresses: (string | undefined)[] = useMemo(() => {
const v3CoreFactoryAddress = chainId && V3_CORE_FACTORY_ADDRESSES[chainId]
if (!v3CoreFactoryAddress) return new Array(poolTokens.length)
return transformed.map((value) => {
if (!v3CoreFactoryAddress || !value) return undefined
return computePoolAddress({
factoryAddress: v3CoreFactoryAddress,
tokenA: value[0],
tokenB: value[1],
fee: value[2],
})
})
}, [chainId, transformed])
return poolTokens.map((value) => value && PoolCache.getPoolAddress(v3CoreFactoryAddress, ...value))
}, [chainId, poolTokens])
const slot0s = useMultipleContractSingleData(poolAddresses, POOL_STATE_INTERFACE, 'slot0')
const liquidities = useMultipleContractSingleData(poolAddresses, POOL_STATE_INTERFACE, 'liquidity')
return useMemo(() => {
return poolKeys.map((_key, index) => {
const [token0, token1, fee] = transformed[index] ?? []
if (!token0 || !token1 || !fee) return [PoolState.INVALID, null]
const tokens = poolTokens[index]
if (!tokens) return [PoolState.INVALID, null]
const [token0, token1, fee] = tokens
if (!slot0s[index]) return [PoolState.INVALID, null]
const { result: slot0, loading: slot0Loading, valid: slot0Valid } = slot0s[index]
if (!liquidities[index]) return [PoolState.INVALID, null]
const { result: liquidity, loading: liquidityLoading, valid: liquidityValid } = liquidities[index]
if (!slot0Valid || !liquidityValid) return [PoolState.INVALID, null]
if (!tokens || !slot0Valid || !liquidityValid) return [PoolState.INVALID, null]
if (slot0Loading || liquidityLoading) return [PoolState.LOADING, null]
if (!slot0 || !liquidity) return [PoolState.NOT_EXISTS, null]
if (!slot0.sqrtPriceX96 || slot0.sqrtPriceX96.eq(0)) return [PoolState.NOT_EXISTS, null]
try {
return [PoolState.EXISTS, new Pool(token0, token1, fee, slot0.sqrtPriceX96, liquidity[0], slot0.tick)]
const pool = PoolCache.getPool(token0, token1, fee, slot0.sqrtPriceX96, liquidity[0], slot0.tick)
return [PoolState.EXISTS, pool]
} catch (error) {
console.error('Error when constructing the pool', error)
return [PoolState.NOT_EXISTS, null]
}
})
}, [liquidities, poolKeys, slot0s, transformed])
}, [liquidities, poolKeys, slot0s, poolTokens])
}
export function usePool(

View File

@@ -1,6 +1,6 @@
import { BigNumber } from '@ethersproject/bignumber'
import { Currency, CurrencyAmount, Token } from '@uniswap/sdk-core'
import { useSingleCallResult } from 'lib/hooks/multicall'
import { useMemo } from 'react'
import { useTokenContract } from './useContract'
@@ -9,7 +9,10 @@ import { useTokenContract } from './useContract'
export function useTotalSupply(token?: Currency): CurrencyAmount<Token> | undefined {
const contract = useTokenContract(token?.isToken ? token.address : undefined, false)
const totalSupply: BigNumber = useSingleCallResult(contract, 'totalSupply')?.result?.[0]
const totalSupplyStr: string | undefined = useSingleCallResult(contract, 'totalSupply')?.result?.[0]?.toString()
return token?.isToken && totalSupply ? CurrencyAmount.fromRawAmount(token, totalSupply.toString()) : undefined
return useMemo(
() => (token?.isToken && totalSupplyStr ? CurrencyAmount.fromRawAmount(token, totalSupplyStr) : undefined),
[token, totalSupplyStr]
)
}

View File

@@ -78,17 +78,18 @@ export function useStablecoinAmountFromFiatValue(fiatValue: string | null | unde
const { chainId } = useActiveWeb3React()
const stablecoin = chainId ? STABLECOIN_AMOUNT_OUT[chainId]?.currency : undefined
if (fiatValue === null || fiatValue === undefined || !chainId || !stablecoin) {
return undefined
}
return useMemo(() => {
if (fiatValue === null || fiatValue === undefined || !chainId || !stablecoin) {
return undefined
}
// trim for decimal precision when parsing
const parsedForDecimals = parseFloat(fiatValue).toFixed(stablecoin.decimals).toString()
try {
// parse USD string into CurrencyAmount based on stablecoin decimals
return tryParseCurrencyAmount(parsedForDecimals, stablecoin)
} catch (error) {
return undefined
}
// trim for decimal precision when parsing
const parsedForDecimals = parseFloat(fiatValue).toFixed(stablecoin.decimals).toString()
try {
// parse USD string into CurrencyAmount based on stablecoin decimals
return tryParseCurrencyAmount(parsedForDecimals, stablecoin)
} catch (error) {
return undefined
}
}, [chainId, fiatValue, stablecoin])
}

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

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

@@ -5,10 +5,20 @@ import { ReactNode, useMemo } from 'react'
import Button from './Button'
import Row from './Row'
const fadeIn = keyframes`
from {
opacity: 0;
}
to {
opacity: 1;
}
`
const StyledButton = styled(Button)`
border-radius: ${({ theme }) => theme.borderRadius}em;
animation: ${fadeIn} 0.25s ease-in;
border-radius: ${({ theme }) => theme.borderRadius * 0.75}em;
flex-grow: 1;
transition: background-color 0.25s ease-out, flex-grow 0.25s ease-out, padding 0.25s ease-out;
transition: background-color 0.25s ease-out, border-radius 0.25s ease-out, flex-grow 0.25s ease-out;
:disabled {
margin: -1px;
@@ -35,11 +45,13 @@ const actionCss = css`
${ActionRow} {
animation: ${grow} 0.25s ease-in;
flex-grow: 1;
justify-content: flex-start;
white-space: nowrap;
}
${StyledButton} {
border-radius: ${({ theme }) => theme.borderRadius * 0.75}em;
border-radius: ${({ theme }) => theme.borderRadius}em;
flex-grow: 0;
padding: 1em;
}
@@ -57,8 +69,8 @@ export const Overlay = styled(Row)<{ hasAction: boolean }>`
export interface Action {
message: ReactNode
icon?: Icon
onClick: () => void
children: ReactNode
onClick?: () => void
children?: ReactNode
}
export interface BaseProps {
@@ -72,11 +84,13 @@ export default function ActionButton({ color = 'accent', disabled, action, onCli
const textColor = useMemo(() => (color === 'accent' && !disabled ? 'onAccent' : 'currentColor'), [color, disabled])
return (
<Overlay hasAction={Boolean(action)} flex align="stretch">
<StyledButton color={color} disabled={disabled} onClick={action ? action.onClick : onClick}>
<ThemedText.TransitionButton buttonSize={action ? 'medium' : 'large'} color={textColor}>
{action ? action.children : children}
</ThemedText.TransitionButton>
</StyledButton>
{(action ? action.onClick : true) && (
<StyledButton color={color} disabled={disabled} onClick={action?.onClick || onClick}>
<ThemedText.TransitionButton buttonSize={action ? 'medium' : 'large'} color={textColor}>
{action?.children || children}
</ThemedText.TransitionButton>
</StyledButton>
)}
{action && (
<ActionRow gap={0.5}>
<LargeIcon color="currentColor" icon={action.icon || AlertTriangle} />

View File

@@ -2,6 +2,7 @@ import { Trans } from '@lingui/macro'
import Row from 'lib/components/Row'
import { Logo } from 'lib/icons'
import styled, { brand, ThemedText } from 'lib/theme'
import { memo } from 'react'
import ExternalLink from './ExternalLink'
@@ -24,7 +25,7 @@ const UniswapA = styled(ExternalLink)`
}
`
export default function BrandedFooter() {
export default memo(function BrandedFooter() {
return (
<Row justify="center">
<UniswapA href={`https://uniswap.org/`}>
@@ -37,4 +38,4 @@ export default function BrandedFooter() {
</UniswapA>
</Row>
)
}
})

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,14 +1,10 @@
import { Trans } from '@lingui/macro'
import useScrollbar from 'lib/hooks/useScrollbar'
import { AlertTriangle, Expando, Icon, Info, LargeIcon } from 'lib/icons'
import ActionButton from 'lib/components/ActionButton'
import Column from 'lib/components/Column'
import Expando from 'lib/components/Expando'
import { AlertTriangle, Icon, LargeIcon } from 'lib/icons'
import styled, { Color, ThemedText } from 'lib/theme'
import { ReactNode, useState } from 'react'
import ActionButton from '../ActionButton'
import { IconButton } from '../Button'
import Column from '../Column'
import Row from '../Row'
import Rule from '../Rule'
import { ReactNode, useCallback, useState } from 'react'
const HeaderIcon = styled(LargeIcon)`
flex-grow: 1;
@@ -35,7 +31,6 @@ export function StatusHeader({ icon: Icon, iconColor, iconSize = 4, children }:
{children}
</Column>
</Column>
<Rule />
</>
)
}
@@ -49,40 +44,6 @@ const ErrorHeader = styled(Column)<{ open: boolean }>`
transition: max-height 0.25s;
}
`
const ErrorColumn = styled(Column)``
const ExpandoColumn = styled(Column)<{ open: boolean }>`
flex-grow: ${({ open }) => (open ? 2 : 0)};
transition: flex-grow 0.25s, gap 0.25s;
${Rule} {
margin-bottom: ${({ open }) => (open ? 0 : 0.75)}em;
transition: margin-bottom 0.25s;
}
${ErrorColumn} {
flex-basis: 0;
flex-grow: ${({ open }) => (open ? 1 : 0)};
overflow-y: hidden;
position: relative;
transition: flex-grow 0.25s;
${Column} {
height: 6.825em;
padding: ${({ open }) => (open ? '0.5em 0' : 0)};
transition: padding 0.25s;
:after {
background: linear-gradient(#ffffff00, ${({ theme }) => theme.dialog});
bottom: 0;
content: '';
height: 0.75em;
pointer-events: none;
position: absolute;
width: calc(100% - 1em);
}
}
}
`
interface ErrorDialogProps {
header?: ReactNode
@@ -93,8 +54,8 @@ interface ErrorDialogProps {
export default function ErrorDialog({ header, error, action, onClick }: ErrorDialogProps) {
const [open, setOpen] = useState(false)
const [details, setDetails] = useState<HTMLDivElement | null>(null)
const scrollbar = useScrollbar(details)
const onExpand = useCallback(() => setOpen((open) => !open), [])
return (
<Column flex padded gap={0.75} align="stretch" style={{ height: '100%' }}>
<StatusHeader icon={AlertTriangle} iconColor="error" iconSize={open ? 3 : 4}>
@@ -105,27 +66,15 @@ export default function ErrorDialog({ header, error, action, onClick }: ErrorDia
<ThemedText.Body2>{header}</ThemedText.Body2>
</ErrorHeader>
</StatusHeader>
<Row>
<Row gap={0.5}>
<Info color="secondary" />
<ThemedText.Subhead2 color="secondary">
<Trans>Error details</Trans>
</ThemedText.Subhead2>
</Row>
<IconButton color="secondary" onClick={() => setOpen(!open)} icon={Expando} iconProps={{ open }} />
</Row>
<ExpandoColumn flex align="stretch" open={open}>
<Rule />
<ErrorColumn>
<Column gap={0.5} ref={setDetails} css={scrollbar}>
<ThemedText.Code userSelect>
{error.name}
{error.message ? `: ${error.message}` : ''}
</ThemedText.Code>
</Column>
</ErrorColumn>
<Column gap={open ? 0 : 0.75} style={{ transition: 'gap 0.25s' }}>
<Expando title={<Trans>Error details</Trans>} open={open} onExpand={onExpand} height={7.5}>
<ThemedText.Code userSelect>
{error.name}
{error.message ? `: ${error.message}` : ''}
</ThemedText.Code>
</Expando>
<ActionButton onClick={onClick}>{action}</ActionButton>
</ExpandoColumn>
</Column>
</Column>
)
}

View File

@@ -26,5 +26,5 @@ export default function WidgetsPropsValidator(props: PropsWithChildren<WidgetPro
}
}, [locale])
return <>{props.children}</>
return null
}

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

@@ -0,0 +1,66 @@
import { IconButton } from 'lib/components/Button'
import Column from 'lib/components/Column'
import Row from 'lib/components/Row'
import Rule from 'lib/components/Rule'
import useScrollbar from 'lib/hooks/useScrollbar'
import { Expando as ExpandoIcon } from 'lib/icons'
import styled from 'lib/theme'
import { PropsWithChildren, ReactNode, useState } from 'react'
const HeaderColumn = styled(Column)`
transition: gap 0.25s;
`
const ExpandoColumn = styled(Column)<{ height: number; open: boolean }>`
height: ${({ height, open }) => (open ? height : 0)}em;
overflow: hidden;
position: relative;
transition: height 0.25s, padding 0.25s;
:after {
background: linear-gradient(transparent, ${({ theme }) => theme.dialog});
bottom: 0;
content: '';
height: 0.75em;
pointer-events: none;
position: absolute;
width: calc(100% - 1em);
}
`
const InnerColumn = styled(Column)<{ height: number }>`
height: ${({ height }) => height}em;
padding: 0.5em 0;
`
interface ExpandoProps {
title: ReactNode
open: boolean
onExpand: () => void
// The absolute height of the expanded container, in em.
height: number
}
/** A scrollable Expando with an absolute height. */
export default function Expando({ title, open, onExpand, height, children }: PropsWithChildren<ExpandoProps>) {
const [scrollingEl, setScrollingEl] = useState<HTMLDivElement | null>(null)
const scrollbar = useScrollbar(scrollingEl)
return (
<Column>
<HeaderColumn gap={open ? 0.5 : 0.75}>
<Rule />
<Row>
{title}
<IconButton color="secondary" onClick={onExpand} icon={ExpandoIcon} iconProps={{ open }} />
</Row>
<Rule />
</HeaderColumn>
<ExpandoColumn open={open} height={height}>
<InnerColumn flex align="stretch" height={height} ref={setScrollingEl} css={scrollbar}>
{children}
</InnerColumn>
</ExpandoColumn>
</Column>
)
return null
}

View File

@@ -1,6 +1,5 @@
import JSBI from 'jsbi'
import styled, { css } from 'lib/theme'
import { forwardRef, HTMLProps, useCallback, useEffect, useState } from 'react'
import { ChangeEvent, forwardRef, HTMLProps, useCallback } from 'react'
const Input = styled.input`
-webkit-appearance: textfield;
@@ -77,42 +76,23 @@ interface EnforcedNumericInputProps extends NumericInputProps {
enforcer: (nextUserInput: string) => string | null
}
function isNumericallyEqual(a: string, b: string) {
const [aInteger, aDecimal] = a.split('.')
const [bInteger, bDecimal] = b.split('.')
return (
JSBI.equal(JSBI.BigInt(aInteger ?? 0), JSBI.BigInt(bInteger ?? 0)) &&
JSBI.equal(JSBI.BigInt(aDecimal ?? 0), JSBI.BigInt(bDecimal ?? 0))
)
}
const NumericInput = forwardRef<HTMLInputElement, EnforcedNumericInputProps>(function NumericInput(
{ value, onChange, enforcer, pattern, ...props }: EnforcedNumericInputProps,
ref
) {
const [state, setState] = useState(value ?? '')
useEffect(() => {
if (!isNumericallyEqual(state, value)) {
setState(value ?? '')
}
}, [value, state, setState])
const validateChange = useCallback(
(event) => {
const nextInput = enforcer(event.target.value.replace(/,/g, '.'))
if (nextInput !== null) {
setState(nextInput ?? '')
if (!isNumericallyEqual(nextInput, value)) {
onChange(nextInput)
}
(event: ChangeEvent<HTMLInputElement>) => {
const nextInput = enforcer(event.target.value.replace(/,/g, '.'))?.replace(/^0+$/, '0')
if (nextInput !== undefined) {
onChange(nextInput)
}
},
[value, onChange, enforcer]
[enforcer, onChange]
)
return (
<Input
value={state}
value={value}
onChange={validateChange}
// universal input options
inputMode="decimal"

View File

@@ -6,6 +6,10 @@ const Rule = styled.hr<{ padded?: true; scrollingEdge?: 'top' | 'bottom' }>`
margin: 0 ${({ padded }) => (padded ? '0.75em' : 0)};
margin-bottom: ${({ scrollingEdge }) => (scrollingEdge === 'bottom' ? -1 : 0)}px;
margin-top: ${({ scrollingEdge }) => (scrollingEdge !== 'bottom' ? -1 : 0)}px;
// Integrators will commonly modify hr width - this overrides any modifications within the widget.
max-width: auto;
width: auto;
`
export default Rule

View File

@@ -1,6 +1,5 @@
import { useLingui } from '@lingui/react'
import { Currency, CurrencyAmount } from '@uniswap/sdk-core'
import { useUSDCValue } from 'hooks/useUSDCPrice'
import { loadingTransitionCss } from 'lib/css/loading'
import {
useIsSwapFieldIndependent,
@@ -70,18 +69,16 @@ export function useFormattedFieldAmount({ disabled, currencyAmount, fieldAmount
export default function Input({ disabled, focused }: InputProps) {
const { i18n } = useLingui()
const {
currencyBalances: { [Field.INPUT]: balance },
[Field.INPUT]: { balance, amount: tradeCurrencyAmount, usdc },
trade: { state: tradeState },
tradeCurrencyAmounts: { [Field.INPUT]: swapInputCurrencyAmount },
} = useSwapInfo()
const inputUSDC = useUSDCValue(swapInputCurrencyAmount)
const [swapInputAmount, updateSwapInputAmount] = useSwapAmount(Field.INPUT)
const [swapInputCurrency, updateSwapInputCurrency] = useSwapCurrency(Field.INPUT)
const [inputAmount, updateInputAmount] = useSwapAmount(Field.INPUT)
const [inputCurrency, updateInputCurrency] = useSwapCurrency(Field.INPUT)
const inputCurrencyAmount = useSwapCurrencyAmount(Field.INPUT)
// extract eagerly in case of reversal
usePrefetchCurrencyColor(swapInputCurrency)
usePrefetchCurrencyColor(inputCurrency)
const isRouteLoading = tradeState === TradeState.SYNCING || tradeState === TradeState.LOADING
const isDependentField = !useIsSwapFieldIndependent(Field.INPUT)
@@ -99,30 +96,30 @@ export default function Input({ disabled, focused }: InputProps) {
const balanceColor = useMemo(() => {
const insufficientBalance =
balance &&
(inputCurrencyAmount ? inputCurrencyAmount.greaterThan(balance) : swapInputCurrencyAmount?.greaterThan(balance))
(inputCurrencyAmount ? inputCurrencyAmount.greaterThan(balance) : tradeCurrencyAmount?.greaterThan(balance))
return insufficientBalance ? 'error' : undefined
}, [balance, inputCurrencyAmount, swapInputCurrencyAmount])
}, [balance, inputCurrencyAmount, tradeCurrencyAmount])
const amount = useFormattedFieldAmount({
disabled,
currencyAmount: swapInputCurrencyAmount,
fieldAmount: swapInputAmount,
currencyAmount: tradeCurrencyAmount,
fieldAmount: inputAmount,
})
return (
<InputColumn gap={0.5} approved={mockApproved}>
<TokenInput
currency={swapInputCurrency}
currency={inputCurrency}
amount={amount}
max={max}
disabled={disabled}
onChangeInput={updateSwapInputAmount}
onChangeCurrency={updateSwapInputCurrency}
onChangeInput={updateInputAmount}
onChangeCurrency={updateInputCurrency}
loading={isLoading}
>
<ThemedText.Body2 color="secondary" userSelect>
<Row>
<USDC isLoading={isRouteLoading}>{inputUSDC ? `$${inputUSDC.toFixed(2)}` : '-'}</USDC>
<USDC isLoading={isRouteLoading}>{usdc ? `$${formatCurrencyAmount(usdc, 6, 'en', 2)}` : '-'}</USDC>
{balance && (
<Balance color={balanceColor} focused={focused}>
Balance: <span>{formatCurrencyAmount(balance, 4, i18n.locale)}</span>

View File

@@ -5,13 +5,11 @@ import { useAtomValue } from 'jotai/utils'
import BrandedFooter from 'lib/components/BrandedFooter'
import { useIsSwapFieldIndependent, useSwapAmount, useSwapCurrency, useSwapInfo } from 'lib/hooks/swap'
import useCurrencyColor from 'lib/hooks/useCurrencyColor'
import useUSDCPriceImpact, { toHumanReadablePriceImpact } from 'lib/hooks/useUSDCPriceImpact'
import { Field } from 'lib/state/swap'
import styled, { DynamicThemeProvider, ThemedText } from 'lib/theme'
import { PropsWithChildren, useMemo } from 'react'
import { PropsWithChildren } from 'react'
import { TradeState } from 'state/routing/types'
import { formatCurrencyAmount } from 'utils/formatCurrencyAmount'
import { getPriceImpactWarning } from 'utils/prices'
import Column from '../Column'
import Row from '../Row'
@@ -40,9 +38,9 @@ export default function Output({ disabled, focused, children }: PropsWithChildre
const { i18n } = useLingui()
const {
currencyBalances: { [Field.OUTPUT]: balance },
[Field.OUTPUT]: { balance, amount: outputCurrencyAmount, usdc: outputUSDC },
trade: { state: tradeState },
tradeCurrencyAmounts: { [Field.INPUT]: inputCurrencyAmount, [Field.OUTPUT]: outputCurrencyAmount },
impact,
} = useSwapInfo()
const [swapOutputAmount, updateSwapOutputAmount] = useSwapAmount(Field.OUTPUT)
@@ -59,9 +57,6 @@ export default function Output({ disabled, focused, children }: PropsWithChildre
// different state true/null/false allow smoother color transition
const hasColor = swapOutputCurrency ? Boolean(color) || null : false
const { outputUSDC, priceImpact } = useUSDCPriceImpact(inputCurrencyAmount, outputCurrencyAmount)
const priceImpactWarning = useMemo(() => getPriceImpactWarning(priceImpact), [priceImpact])
const amount = useFormattedFieldAmount({
disabled,
currencyAmount: outputCurrencyAmount,
@@ -87,12 +82,8 @@ export default function Output({ disabled, focused, children }: PropsWithChildre
<ThemedText.Body2 color="secondary" userSelect>
<Row>
<USDC gap={0.5} isLoading={isRouteLoading}>
{outputUSDC ? `$${outputUSDC.toFixed(2)}` : '-'}{' '}
{priceImpact && (
<ThemedText.Body2 color={priceImpactWarning}>
({toHumanReadablePriceImpact(priceImpact)})
</ThemedText.Body2>
)}
{outputUSDC ? `$${formatCurrencyAmount(outputUSDC, 6, 'en', 2)}` : '-'}{' '}
{impact && <ThemedText.Body2 color={impact.warning}>({impact.toString()})</ThemedText.Body2>}
</USDC>
{balance && (
<Balance focused={focused}>

View File

@@ -0,0 +1,54 @@
import { useLingui } from '@lingui/react'
import { Trade } from '@uniswap/router-sdk'
import { Currency, CurrencyAmount, TradeType } from '@uniswap/sdk-core'
import Row from 'lib/components/Row'
import { ThemedText } from 'lib/theme'
import formatLocaleNumber from 'lib/utils/formatLocaleNumber'
import { useCallback, useMemo, useState } from 'react'
import { formatCurrencyAmount, formatPrice } from 'utils/formatCurrencyAmount'
import { TextButton } from '../Button'
interface PriceProps {
trade: Trade<Currency, Currency, TradeType>
outputUSDC?: CurrencyAmount<Currency>
}
/** Displays the price of a trade. If outputUSDC is included, also displays the unit price. */
export default function Price({ trade, outputUSDC }: PriceProps) {
const { i18n } = useLingui()
const { inputAmount, outputAmount, executionPrice } = trade
const [base, setBase] = useState<'input' | 'output'>('input')
const onClick = useCallback(() => setBase((base) => (base === 'input' ? 'output' : 'input')), [])
// Compute the usdc price from the output price, so that it aligns with the displayed price.
const { price, usdcPrice } = useMemo(() => {
switch (base) {
case 'input':
return {
price: executionPrice,
usdcPrice: outputUSDC?.multiply(inputAmount.decimalScale).divide(inputAmount),
}
case 'output':
return {
price: executionPrice.invert(),
usdcPrice: outputUSDC?.multiply(outputAmount.decimalScale).divide(outputAmount),
}
}
}, [base, executionPrice, inputAmount, outputAmount, outputUSDC])
return (
<TextButton color="primary" onClick={onClick}>
<ThemedText.Caption>
<Row gap={0.25}>
{formatLocaleNumber({ number: 1, sigFigs: 1, locale: i18n.locale })} {price.baseCurrency.symbol} ={' '}
{formatPrice(price, 6, i18n.locale)} {price.quoteCurrency.symbol}
{usdcPrice && (
<ThemedText.Caption color="secondary">(${formatCurrencyAmount(usdcPrice, 6, 'en', 2)})</ThemedText.Caption>
)}
</Row>
</ThemedText.Caption>
</TextButton>
)
}

View File

@@ -2,7 +2,7 @@ import { Trans } from '@lingui/macro'
import { useAtom } from 'jotai'
import Popover from 'lib/components/Popover'
import { useTooltip } from 'lib/components/Tooltip'
import { getSlippageWarning, toPercent } from 'lib/hooks/useAllowedSlippage'
import { getSlippageWarning, toPercent } from 'lib/hooks/useSlippage'
import { AlertTriangle, Check, Icon, LargeIcon, XOctagon } from 'lib/icons'
import { autoSlippageAtom, maxSlippageAtom } from 'lib/state/settings'
import styled, { ThemedText } from 'lib/theme'

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

@@ -22,8 +22,11 @@ const UNI = (function () {
function Fixture() {
const setState = useUpdateAtom(swapAtom)
const {
allowedSlippage,
[Field.INPUT]: { usdc: inputUSDC },
[Field.OUTPUT]: { usdc: outputUSDC },
trade: { trade },
slippage,
impact,
} = useSwapInfo()
useEffect(() => {
@@ -37,7 +40,14 @@ function Fixture() {
return trade ? (
<Modal color="dialog">
<SummaryDialog onConfirm={() => void 0} trade={trade} allowedSlippage={allowedSlippage} />
<SummaryDialog
onConfirm={async () => void 0}
trade={trade}
slippage={slippage}
inputUSDC={inputUSDC}
outputUSDC={outputUSDC}
impact={impact}
/>
</Modal>
) : null
}

View File

@@ -1,17 +1,18 @@
import { t } from '@lingui/macro'
import { useLingui } from '@lingui/react'
import { Trade } from '@uniswap/router-sdk'
import { Currency, Percent, TradeType } from '@uniswap/sdk-core'
import { Currency, TradeType } from '@uniswap/sdk-core'
import { useAtomValue } from 'jotai/utils'
import { getSlippageWarning } from 'lib/hooks/useAllowedSlippage'
import Column from 'lib/components/Column'
import Row from 'lib/components/Row'
import { Slippage } from 'lib/hooks/useSlippage'
import { PriceImpact } from 'lib/hooks/useUSDCPriceImpact'
import { feeOptionsAtom } from 'lib/state/swap'
import styled, { Color, ThemedText } from 'lib/theme'
import { useMemo } from 'react'
import { currencyId } from 'utils/currencyId'
import { formatCurrencyAmount } from 'utils/formatCurrencyAmount'
import { computeRealizedLPFeeAmount, computeRealizedPriceImpact, getPriceImpactWarning } from 'utils/prices'
import Row from '../../Row'
import { computeRealizedLPFeeAmount } from 'utils/prices'
const Value = styled.span<{ color?: Color }>`
color: ${({ color, theme }) => color && theme[color]};
@@ -37,16 +38,16 @@ function Detail({ label, value, color }: DetailProps) {
interface DetailsProps {
trade: Trade<Currency, Currency, TradeType>
allowedSlippage: Percent
slippage: Slippage
impact?: PriceImpact
}
export default function Details({ trade, allowedSlippage }: DetailsProps) {
export default function Details({ trade, slippage, impact }: DetailsProps) {
const { inputAmount, outputAmount } = trade
const inputCurrency = inputAmount.currency
const outputCurrency = outputAmount.currency
const integrator = window.location.hostname
const feeOptions = useAtomValue(feeOptionsAtom)
const priceImpact = useMemo(() => computeRealizedPriceImpact(trade), [trade])
const lpFeeAmount = useMemo(() => computeRealizedLPFeeAmount(trade), [trade])
const { i18n } = useLingui()
@@ -62,7 +63,9 @@ export default function Details({ trade, allowedSlippage }: DetailsProps) {
}
}
rows.push([t`Price impact`, `${priceImpact.toFixed(2)}%`, getPriceImpactWarning(priceImpact)])
if (impact) {
rows.push([t`Price impact`, impact.toString(), impact.warning])
}
if (lpFeeAmount) {
const parsedLpFee = formatCurrencyAmount(lpFeeAmount, 6, i18n.locale)
@@ -70,36 +73,36 @@ export default function Details({ trade, allowedSlippage }: DetailsProps) {
}
if (trade.tradeType === TradeType.EXACT_OUTPUT) {
const localizedMaxSent = formatCurrencyAmount(trade.maximumAmountIn(allowedSlippage), 6, i18n.locale)
const localizedMaxSent = formatCurrencyAmount(trade.maximumAmountIn(slippage.allowed), 6, i18n.locale)
rows.push([t`Maximum sent`, `${localizedMaxSent} ${inputCurrency.symbol}`])
}
if (trade.tradeType === TradeType.EXACT_INPUT) {
const localizedMaxSent = formatCurrencyAmount(trade.minimumAmountOut(allowedSlippage), 6, i18n.locale)
const localizedMaxSent = formatCurrencyAmount(trade.minimumAmountOut(slippage.allowed), 6, i18n.locale)
rows.push([t`Minimum received`, `${localizedMaxSent} ${outputCurrency.symbol}`])
}
rows.push([t`Slippage tolerance`, `${allowedSlippage.toFixed(2)}%`, getSlippageWarning(allowedSlippage)])
rows.push([t`Slippage tolerance`, `${slippage.allowed.toFixed(2)}%`, slippage.warning])
return rows
}, [
feeOptions,
priceImpact,
lpFeeAmount,
trade,
allowedSlippage,
outputAmount,
i18n.locale,
integrator,
outputCurrency,
impact,
inputCurrency,
integrator,
lpFeeAmount,
outputAmount,
outputCurrency,
slippage,
trade,
])
return (
<>
<Column gap={0.5}>
{details.map(([label, detail, color]) => (
<Detail key={label} label={label} value={detail} color={color} />
))}
</>
</Column>
)
}

View File

@@ -1,11 +1,10 @@
import { useLingui } from '@lingui/react'
import { Currency, CurrencyAmount, Percent, Token } from '@uniswap/sdk-core'
import useUSDCPriceImpact, { toHumanReadablePriceImpact } from 'lib/hooks/useUSDCPriceImpact'
import { Currency, CurrencyAmount } from '@uniswap/sdk-core'
import { PriceImpact } from 'lib/hooks/useUSDCPriceImpact'
import { ArrowRight } from 'lib/icons'
import { ThemedText } from 'lib/theme'
import { useMemo } from 'react'
import { PropsWithChildren } from 'react'
import { formatCurrencyAmount } from 'utils/formatCurrencyAmount'
import { getPriceImpactWarning } from 'utils/prices'
import Column from '../../Column'
import Row from '../../Row'
@@ -13,13 +12,11 @@ import TokenImg from '../../TokenImg'
interface TokenValueProps {
input: CurrencyAmount<Currency>
usdc?: CurrencyAmount<Token>
priceImpact?: Percent
usdc?: CurrencyAmount<Currency>
}
function TokenValue({ input, usdc, priceImpact }: TokenValueProps) {
function TokenValue({ input, usdc, children }: PropsWithChildren<TokenValueProps>) {
const { i18n } = useLingui()
const priceImpactWarning = useMemo(() => getPriceImpactWarning(priceImpact), [priceImpact])
return (
<Column justify="flex-start">
<Row gap={0.375} justify="flex-start">
@@ -29,16 +26,12 @@ function TokenValue({ input, usdc, priceImpact }: TokenValueProps) {
</ThemedText.Body2>
</Row>
{usdc && (
<Row justify="flex-start">
<ThemedText.Caption color="secondary" userSelect>
${formatCurrencyAmount(usdc, 2, i18n.locale)}
{priceImpact && (
<ThemedText.Caption color={priceImpactWarning}>
({toHumanReadablePriceImpact(priceImpact)})
</ThemedText.Caption>
)}
</ThemedText.Caption>
</Row>
<ThemedText.Caption color="secondary" userSelect>
<Row justify="flex-start" gap={0.25}>
${formatCurrencyAmount(usdc, 6, 'en', 2)}
{children}
</Row>
</ThemedText.Caption>
)}
</Column>
)
@@ -47,17 +40,19 @@ function TokenValue({ input, usdc, priceImpact }: TokenValueProps) {
interface SummaryProps {
input: CurrencyAmount<Currency>
output: CurrencyAmount<Currency>
showUSDC?: true
inputUSDC?: CurrencyAmount<Currency>
outputUSDC?: CurrencyAmount<Currency>
impact?: PriceImpact
}
export default function Summary({ input, output, showUSDC }: SummaryProps) {
const { inputUSDC, outputUSDC, priceImpact } = useUSDCPriceImpact(input, output)
export default function Summary({ input, output, inputUSDC, outputUSDC, impact }: SummaryProps) {
return (
<Row gap={showUSDC ? 1 : 0.25}>
<TokenValue input={input} usdc={showUSDC && inputUSDC} />
<Row gap={impact ? 1 : 0.25}>
<TokenValue input={input} usdc={inputUSDC} />
<ArrowRight />
<TokenValue input={output} usdc={showUSDC && outputUSDC} priceImpact={priceImpact} />
<TokenValue input={output} usdc={outputUSDC}>
{impact && <ThemedText.Caption color={impact.warning}>({impact.toString()})</ThemedText.Caption>}
</TokenValue>
</Row>
)
}

View File

@@ -1,121 +1,127 @@
import { Trans } from '@lingui/macro'
import { useLingui } from '@lingui/react'
import { Trade } from '@uniswap/router-sdk'
import { Currency, Percent, TradeType } from '@uniswap/sdk-core'
import { IconButton } from 'lib/components/Button'
import { useSwapTradeType } from 'lib/hooks/swap'
import { getSlippageWarning } from 'lib/hooks/useAllowedSlippage'
import useScrollbar from 'lib/hooks/useScrollbar'
import { AlertTriangle, BarChart, Expando, Info } from 'lib/icons'
import { Currency, CurrencyAmount, TradeType } from '@uniswap/sdk-core'
import ActionButton, { Action } from 'lib/components/ActionButton'
import Column from 'lib/components/Column'
import { Header } from 'lib/components/Dialog'
import Expando from 'lib/components/Expando'
import Row from 'lib/components/Row'
import { Slippage } from 'lib/hooks/useSlippage'
import { PriceImpact } from 'lib/hooks/useUSDCPriceImpact'
import { AlertTriangle, BarChart, Info, Spinner } from 'lib/icons'
import styled, { ThemedText } from 'lib/theme'
import formatLocaleNumber from 'lib/utils/formatLocaleNumber'
import { useMemo, useState } from 'react'
import { formatCurrencyAmount, formatPrice } from 'utils/formatCurrencyAmount'
import { computeRealizedPriceImpact, getPriceImpactWarning } from 'utils/prices'
import { useCallback, useMemo, useState } from 'react'
import { formatCurrencyAmount } from 'utils/formatCurrencyAmount'
import { tradeMeaningfullyDiffers } from 'utils/tradeMeaningFullyDiffer'
import ActionButton, { Action } from '../../ActionButton'
import Column from '../../Column'
import { Header } from '../../Dialog'
import Row from '../../Row'
import Rule from '../../Rule'
import Price from '../Price'
import Details from './Details'
import Summary from './Summary'
export default Summary
const SummaryColumn = styled(Column)``
const ExpandoColumn = styled(Column)``
const DetailsColumn = styled(Column)``
const Estimate = styled(ThemedText.Caption)``
const Content = styled(Column)``
const Heading = styled(Column)``
const Footing = styled(Column)``
const Body = styled(Column)<{ open: boolean }>`
height: calc(100% - 2.5em);
${SummaryColumn} {
flex-grow: ${({ open }) => (open ? 0 : 1)};
${Content}, ${Heading} {
flex-grow: 1;
transition: flex-grow 0.25s;
}
${ExpandoColumn} {
flex-grow: ${({ open }) => (open ? 1 : 0)};
transition: flex-grow 0.25s;
${DetailsColumn} {
flex-basis: ${({ open }) => (open ? 7.5 : 0)}em;
overflow-y: hidden;
position: relative;
transition: flex-basis 0.25s;
${Column} {
height: 7.5em;
grid-template-rows: repeat(auto-fill, 1em);
padding: ${({ open }) => (open ? '0.5em 0' : 0)};
transition: padding 0.25s;
:after {
background: linear-gradient(#ffffff00, ${({ theme }) => theme.dialog});
bottom: 0;
content: '';
height: 0.75em;
pointer-events: none;
position: absolute;
width: calc(100% - 1em);
}
}
}
${Estimate} {
max-height: ${({ open }) => (open ? 0 : 56 / 12)}em; // 2 * line-height + padding
min-height: 0;
overflow-y: hidden;
padding: ${({ open }) => (open ? 0 : '1em 0')};
transition: ${({ open }) =>
open
? 'max-height 0.1s ease-out, padding 0.25s ease-out'
: 'flex-grow 0.25s ease-out, max-height 0.1s ease-in, padding 0.25s ease-out'};
}
${Footing} {
margin-bottom: ${({ open }) => (open ? '-0.75em' : undefined)};
max-height: ${({ open }) => (open ? 0 : '3em')};
opacity: ${({ open }) => (open ? 0 : 1)};
transition: max-height 0.25s, margin-bottom 0.25s, opacity 0.15s 0.1s;
visibility: ${({ open }) => (open ? 'hidden' : undefined)};
}
`
interface SummaryDialogProps {
trade: Trade<Currency, Currency, TradeType>
allowedSlippage: Percent
onConfirm: () => void
function Subhead({ impact, slippage }: { impact?: PriceImpact; slippage: Slippage }) {
return (
<Row gap={0.5}>
{impact?.warning || slippage.warning ? (
<AlertTriangle color={impact?.warning || slippage.warning} />
) : (
<Info color="secondary" />
)}
<ThemedText.Subhead2 color={impact?.warning || slippage.warning || 'secondary'}>
{impact?.warning ? (
<Trans>High price impact</Trans>
) : slippage.warning ? (
<Trans>High slippage</Trans>
) : (
<Trans>Swap details</Trans>
)}
</ThemedText.Subhead2>
</Row>
)
}
export function SummaryDialog({ trade, allowedSlippage, onConfirm }: SummaryDialogProps) {
const { inputAmount, outputAmount, executionPrice } = trade
const inputCurrency = inputAmount.currency
const outputCurrency = outputAmount.currency
const priceImpact = useMemo(() => computeRealizedPriceImpact(trade), [trade])
const tradeType = useSwapTradeType()
function Estimate({ trade, slippage }: { trade: Trade<Currency, Currency, TradeType>; slippage: Slippage }) {
const { i18n } = useLingui()
const text = useMemo(() => {
switch (trade.tradeType) {
case TradeType.EXACT_INPUT:
return (
<Trans>
Output is estimated. You will receive at least{' '}
{formatCurrencyAmount(trade.minimumAmountOut(slippage.allowed), 6, i18n.locale)}{' '}
{trade.outputAmount.currency.symbol} or the transaction will revert.
</Trans>
)
case TradeType.EXACT_OUTPUT:
return (
<Trans>
Output is estimated. You will send at most{' '}
{formatCurrencyAmount(trade.maximumAmountIn(slippage.allowed), 6, i18n.locale)}{' '}
{trade.inputAmount.currency.symbol} or the transaction will revert.
</Trans>
)
}
}, [i18n.locale, slippage.allowed, trade])
return <ThemedText.Caption color="secondary">{text}</ThemedText.Caption>
}
const [open, setOpen] = useState(false)
const [details, setDetails] = useState<HTMLDivElement | null>(null)
const scrollbar = useScrollbar(details)
const warning = useMemo(() => {
return getPriceImpactWarning(priceImpact) || getSlippageWarning(allowedSlippage)
}, [allowedSlippage, priceImpact])
function ConfirmButton({
trade,
highPriceImpact,
onConfirm,
}: {
trade: Trade<Currency, Currency, TradeType>
highPriceImpact: boolean
onConfirm: () => Promise<void>
}) {
const [ackPriceImpact, setAckPriceImpact] = useState(false)
const [confirmedTrade, setConfirmedTrade] = useState(trade)
const [ackTrade, setAckTrade] = useState(trade)
const doesTradeDiffer = useMemo(
() => Boolean(trade && confirmedTrade && tradeMeaningfullyDiffers(trade, confirmedTrade)),
[confirmedTrade, trade]
() => Boolean(trade && ackTrade && tradeMeaningfullyDiffers(trade, ackTrade)),
[ackTrade, trade]
)
const [isPending, setIsPending] = useState(false)
const onClick = useCallback(async () => {
setIsPending(true)
await onConfirm()
setIsPending(false)
}, [onConfirm])
const action = useMemo((): Action | undefined => {
if (doesTradeDiffer) {
if (isPending) {
return { message: <Trans>Confirm in your wallet</Trans>, icon: Spinner }
} else if (doesTradeDiffer) {
return {
message: <Trans>Price updated</Trans>,
icon: BarChart,
onClick: () => setConfirmedTrade(trade),
onClick: () => setAckTrade(trade),
children: <Trans>Accept</Trans>,
}
} else if (getPriceImpactWarning(priceImpact) === 'error' && !ackPriceImpact) {
} else if (highPriceImpact && !ackPriceImpact) {
return {
message: <Trans>High price impact</Trans>,
onClick: () => setAckPriceImpact(true),
@@ -123,62 +129,53 @@ export function SummaryDialog({ trade, allowedSlippage, onConfirm }: SummaryDial
}
}
return
}, [ackPriceImpact, doesTradeDiffer, priceImpact, trade])
}, [ackPriceImpact, doesTradeDiffer, highPriceImpact, isPending, trade])
if (!(inputAmount && outputAmount && inputCurrency && outputCurrency)) {
return null
}
return (
<ActionButton onClick={onClick} action={action}>
<Trans>Confirm swap</Trans>
</ActionButton>
)
}
interface SummaryDialogProps {
trade: Trade<Currency, Currency, TradeType>
slippage: Slippage
inputUSDC?: CurrencyAmount<Currency>
outputUSDC?: CurrencyAmount<Currency>
impact?: PriceImpact
onConfirm: () => Promise<void>
}
export function SummaryDialog({ trade, slippage, inputUSDC, outputUSDC, impact, onConfirm }: SummaryDialogProps) {
const { inputAmount, outputAmount } = trade
const [open, setOpen] = useState(false)
const onExpand = useCallback(() => setOpen((open) => !open), [])
return (
<>
<Header title={<Trans>Swap summary</Trans>} ruled />
<Body flex align="stretch" gap={0.75} padded open={open}>
<SummaryColumn gap={0.75} flex justify="center">
<Summary input={inputAmount} output={outputAmount} showUSDC />
<Row>
<ThemedText.Caption userSelect>
{formatLocaleNumber({ number: 1, sigFigs: 1, locale: i18n.locale })} {inputCurrency.symbol} ={' '}
{formatPrice(executionPrice, 6, i18n.locale)} {outputCurrency.symbol}
</ThemedText.Caption>
</Row>
</SummaryColumn>
<Rule />
<Row>
<Row gap={0.5}>
{warning ? <AlertTriangle color={warning} /> : <Info color="secondary" />}
<ThemedText.Subhead2 color="secondary">
<Trans>Swap details</Trans>
</ThemedText.Subhead2>
</Row>
<IconButton color="secondary" onClick={() => setOpen(!open)} icon={Expando} iconProps={{ open }} />
</Row>
<ExpandoColumn flex align="stretch">
<Rule />
<DetailsColumn>
<Column gap={0.5} ref={setDetails} css={scrollbar}>
<Details trade={trade} allowedSlippage={allowedSlippage} />
</Column>
</DetailsColumn>
<Estimate color="secondary">
<Trans>Output is estimated.</Trans>{' '}
{tradeType === TradeType.EXACT_INPUT && (
<Trans>
You will receive at least{' '}
{formatCurrencyAmount(trade.minimumAmountOut(allowedSlippage), 6, i18n.locale)} {outputCurrency.symbol}{' '}
or the transaction will revert.
</Trans>
)}
{tradeType === TradeType.EXACT_OUTPUT && (
<Trans>
You will send at most {formatCurrencyAmount(trade.maximumAmountIn(allowedSlippage), 6, i18n.locale)}{' '}
{inputCurrency.symbol} or the transaction will revert.
</Trans>
)}
</Estimate>
<ActionButton onClick={onConfirm} action={action}>
<Trans>Confirm swap</Trans>
</ActionButton>
</ExpandoColumn>
<Body flex align="stretch" padded gap={0.75} open={open}>
<Heading gap={0.75} flex justify="center">
<Summary
input={inputAmount}
output={outputAmount}
inputUSDC={inputUSDC}
outputUSDC={outputUSDC}
impact={impact}
/>
<Price trade={trade} />
</Heading>
<Column gap={open ? 0 : 0.75} style={{ transition: 'gap 0.25s' }}>
<Expando title={<Subhead impact={impact} slippage={slippage} />} open={open} onExpand={onExpand} height={7}>
<Details trade={trade} slippage={slippage} impact={impact} />
</Expando>
<Footing>
<Estimate trade={trade} slippage={slippage} />
</Footing>
<ConfirmButton trade={trade} highPriceImpact={impact?.warning === 'error'} onConfirm={onConfirm} />
</Column>
</Body>
</>
)

View File

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

View File

@@ -0,0 +1,195 @@
import { Trans } from '@lingui/macro'
import { useAtomValue, useUpdateAtom } from 'jotai/utils'
import { useSwapInfo } from 'lib/hooks/swap'
import { useSwapApprovalOptimizedTrade } from 'lib/hooks/swap/useSwapApproval'
import { useSwapCallback } from 'lib/hooks/swap/useSwapCallback'
import useWrapCallback, { WrapType } from 'lib/hooks/swap/useWrapCallback'
import { useAddTransaction } from 'lib/hooks/transactions'
import useActiveWeb3React from 'lib/hooks/useActiveWeb3React'
import { useSetOldestValidBlock } from 'lib/hooks/useIsValidBlock'
import useTransactionDeadline from 'lib/hooks/useTransactionDeadline'
import { Spinner } from 'lib/icons'
import { displayTxHashAtom, feeOptionsAtom, Field } from 'lib/state/swap'
import { TransactionType } from 'lib/state/transactions'
import { useTheme } from 'lib/theme'
import { isAnimating } from 'lib/utils/animations'
import { memo, useCallback, useEffect, useMemo, useState } from 'react'
import invariant from 'tiny-invariant'
import ActionButton, { ActionButtonProps } from '../../ActionButton'
import Dialog from '../../Dialog'
import { SummaryDialog } from '../Summary'
import useApprovalData, { useIsPendingApproval } from './useApprovalData'
interface SwapButtonProps {
disabled?: boolean
}
export default memo(function SwapButton({ disabled }: SwapButtonProps) {
const { account, chainId } = useActiveWeb3React()
const {
[Field.INPUT]: {
currency: inputCurrency,
amount: inputCurrencyAmount,
balance: inputCurrencyBalance,
usdc: inputUSDC,
},
[Field.OUTPUT]: { usdc: outputUSDC },
trade,
slippage,
impact,
} = useSwapInfo()
const feeOptions = useAtomValue(feeOptionsAtom)
// TODO(zzmp): Return an optimized trade directly from useSwapInfo.
const optimizedTrade =
// Use trade.trade if there is no swap optimized trade. This occurs if approvals are still pending.
useSwapApprovalOptimizedTrade(trade.trade, slippage.allowed, useIsPendingApproval) || trade.trade
const deadline = useTransactionDeadline()
const { type: wrapType, callback: wrapCallback } = useWrapCallback()
const { approvalAction, signatureData } = useApprovalData(optimizedTrade, slippage, inputCurrencyAmount)
const { callback: swapCallback } = useSwapCallback({
trade: optimizedTrade,
allowedSlippage: slippage.allowed,
recipientAddressOrName: account ?? null,
signatureData,
deadline,
feeOptions,
})
const [open, setOpen] = useState(false)
// Close the review modal if there is no available trade.
useEffect(() => setOpen((open) => (trade.trade ? open : false)), [trade.trade])
// Close the review modal on chain change.
useEffect(() => setOpen(false), [chainId])
const addTransaction = useAddTransaction()
const setDisplayTxHash = useUpdateAtom(displayTxHashAtom)
const setOldestValidBlock = useSetOldestValidBlock()
const [isPending, setIsPending] = useState(false)
const onWrap = useCallback(async () => {
setIsPending(true)
try {
const transaction = await wrapCallback?.()
if (!transaction) return
addTransaction({
response: transaction,
type: TransactionType.WRAP,
unwrapped: wrapType === WrapType.UNWRAP,
currencyAmountRaw: transaction.value?.toString() ?? '0',
chainId,
})
setDisplayTxHash(transaction.hash)
} catch (e) {
// TODO(zzmp): Surface errors from wrap.
console.log(e)
}
// Only reset pending after any queued animations to avoid layout thrashing, because a
// successful wrap will open the status dialog and immediately cover the button.
const postWrap = () => {
setIsPending(false)
document.removeEventListener('animationend', postWrap)
}
if (isAnimating(document)) {
document.addEventListener('animationend', postWrap)
} else {
postWrap()
}
}, [addTransaction, chainId, setDisplayTxHash, wrapCallback, wrapType])
// Reset the pending state if user updates the swap.
useEffect(() => setIsPending(false), [inputCurrencyAmount, trade])
const onSwap = useCallback(async () => {
try {
const transaction = await swapCallback?.()
if (!transaction) return
invariant(trade.trade)
addTransaction({
response: transaction,
type: TransactionType.SWAP,
tradeType: trade.trade.tradeType,
inputCurrencyAmount: trade.trade.inputAmount,
outputCurrencyAmount: trade.trade.outputAmount,
})
setDisplayTxHash(transaction.hash)
// Set the block containing the response to the oldest valid block to ensure that the
// completed trade's impact is reflected in future fetched trades.
transaction.wait(1).then((receipt) => {
setOldestValidBlock(receipt.blockNumber)
})
// Only reset open after any queued animations to avoid layout thrashing, because a
// successful swap will open the status dialog and immediately cover the summary dialog.
const postSwap = () => {
setOpen(false)
document.removeEventListener('animationend', postSwap)
}
if (isAnimating(document)) {
document.addEventListener('animationend', postSwap)
} else {
postSwap()
}
} catch (e) {
// TODO(zzmp): Surface errors from swap.
console.log(e)
}
}, [addTransaction, setDisplayTxHash, setOldestValidBlock, swapCallback, trade.trade])
const disableSwap = useMemo(
() =>
disabled ||
!chainId ||
(wrapType === WrapType.NONE && !optimizedTrade) ||
!(inputCurrencyAmount && inputCurrencyBalance) ||
inputCurrencyBalance.lessThan(inputCurrencyAmount),
[disabled, wrapType, optimizedTrade, chainId, inputCurrencyAmount, inputCurrencyBalance]
)
const actionProps = useMemo((): Partial<ActionButtonProps> | undefined => {
if (disableSwap) {
return { disabled: true }
} else if (wrapType === WrapType.NONE) {
return approvalAction ? { action: approvalAction } : { onClick: () => setOpen(true) }
} else {
return isPending
? { action: { message: <Trans>Confirm in your wallet</Trans>, icon: Spinner } }
: { onClick: onWrap }
}
}, [approvalAction, disableSwap, isPending, onWrap, wrapType])
const Label = useCallback(() => {
switch (wrapType) {
case WrapType.UNWRAP:
return <Trans>Unwrap {inputCurrency?.symbol}</Trans>
case WrapType.WRAP:
return <Trans>Wrap {inputCurrency?.symbol}</Trans>
case WrapType.NONE:
default:
return <Trans>Review swap</Trans>
}
}, [inputCurrency?.symbol, wrapType])
const onClose = useCallback(() => setOpen(false), [])
const { tokenColorExtraction } = useTheme()
return (
<>
<ActionButton color={tokenColorExtraction ? 'interactive' : 'accent'} {...actionProps}>
<Label />
</ActionButton>
{open && trade.trade && (
<Dialog color="dialog" onClose={onClose}>
<SummaryDialog
trade={trade.trade}
slippage={slippage}
inputUSDC={inputUSDC}
outputUSDC={outputUSDC}
impact={impact}
onConfirm={onSwap}
/>
</Dialog>
)}
</>
)
})

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

@@ -81,5 +81,5 @@ export default function SwapPropValidator(props: ValidatorProps) {
}
}, [defaultInputTokenAddress, defaultInputAmount, defaultOutputTokenAddress, defaultOutputAmount])
return <>{props.children}</>
return null
}

View File

@@ -1,19 +1,16 @@
import { Trans } from '@lingui/macro'
import { Currency, TradeType } from '@uniswap/sdk-core'
import { Currency, CurrencyAmount, TradeType } from '@uniswap/sdk-core'
import Column from 'lib/components/Column'
import Rule from 'lib/components/Rule'
import Tooltip from 'lib/components/Tooltip'
import { loadingCss } from 'lib/css/loading'
import { WrapType } from 'lib/hooks/swap/useWrapCallback'
import useUSDCPriceImpact, { toHumanReadablePriceImpact } from 'lib/hooks/useUSDCPriceImpact'
import { PriceImpact } from 'lib/hooks/useUSDCPriceImpact'
import { AlertTriangle, Icon, Info, InlineSpinner } from 'lib/icons'
import styled, { ThemedText } from 'lib/theme'
import { ReactNode, useCallback, useMemo, useState } from 'react'
import { ReactNode, useCallback } from 'react'
import { InterfaceTrade } from 'state/routing/types'
import { getPriceImpactWarning } from 'utils/prices'
import { TextButton } from '../../Button'
import Row from '../../Row'
import Price from '../Price'
import RoutingDiagram from '../RoutingDiagram'
const Loading = styled.span`
@@ -40,7 +37,7 @@ export function ConnectWallet() {
}
export function UnsupportedNetwork() {
return <Caption caption={<Trans>Unsupported network - switch to another to trade.</Trans>} />
return <Caption caption={<Trans>Unsupported network - switch to another to trade</Trans>} />
}
export function InsufficientBalance({ currency }: { currency: Currency }) {
@@ -51,6 +48,10 @@ export function InsufficientLiquidity() {
return <Caption caption={<Trans>Insufficient liquidity in the pool for your trade</Trans>} />
}
export function Error() {
return <Caption caption={<Trans>Error fetching trade</Trans>} />
}
export function Empty() {
return <Caption icon={Info} caption={<Trans>Enter an amount</Trans>} />
}
@@ -68,55 +69,37 @@ export function LoadingTrade() {
)
}
export function WrapCurrency({ loading, wrapType }: { loading: boolean; wrapType: WrapType.UNWRAP | WrapType.WRAP }) {
const WrapText = useCallback(() => {
if (wrapType === WrapType.WRAP) {
return loading ? <Trans>Wrapping native currency.</Trans> : <Trans>Wrap native currency.</Trans>
}
return loading ? <Trans>Unwrapping native currency.</Trans> : <Trans>Unwrap native currency.</Trans>
}, [loading, wrapType])
export function WrapCurrency({ inputCurrency, outputCurrency }: { inputCurrency: Currency; outputCurrency: Currency }) {
const Text = useCallback(
() => (
<Trans>
Convert {inputCurrency.symbol} to {outputCurrency.symbol} with no slippage
</Trans>
),
[inputCurrency.symbol, outputCurrency.symbol]
)
return <Caption icon={Info} caption={<WrapText />} />
return <Caption icon={Info} caption={<Text />} />
}
export function Trade({ trade }: { trade: InterfaceTrade<Currency, Currency, TradeType> }) {
const [flip, setFlip] = useState(true)
const { inputAmount: input, outputAmount: output, executionPrice } = trade
const { inputUSDC, outputUSDC, priceImpact } = useUSDCPriceImpact(input, output)
const isPriceImpactHigh = priceImpact && getPriceImpactWarning(priceImpact)
const ratio = useMemo(() => {
const [a, b] = flip ? [output, input] : [input, output]
const priceString = (!flip ? executionPrice : executionPrice?.invert())?.toSignificant(6)
const ratio = `1 ${a.currency.symbol} = ${priceString} ${b.currency.symbol}`
const usdc = !flip
? inputUSDC
? ` ($${inputUSDC.toSignificant(6)})`
: null
: outputUSDC
? ` ($${outputUSDC.toSignificant(6)})`
: null
return (
<ThemedText.Caption userSelect>
<Row gap={0.25}>
{ratio}
{usdc && <ThemedText.Caption color="secondary">{usdc}</ThemedText.Caption>}
</Row>
</ThemedText.Caption>
)
}, [executionPrice, inputUSDC, outputUSDC, flip, input, output])
export function Trade({
trade,
outputUSDC,
impact,
}: {
trade: InterfaceTrade<Currency, Currency, TradeType>
outputUSDC?: CurrencyAmount<Currency>
impact?: PriceImpact
}) {
return (
<>
<Tooltip placement="bottom" icon={isPriceImpactHigh ? AlertTriangle : Info}>
<Tooltip placement="bottom" icon={impact?.warning ? AlertTriangle : Info}>
<Column gap={0.75}>
{isPriceImpactHigh && (
{impact?.warning && (
<>
<ThemedText.Caption>
The output amount is estimated at {toHumanReadablePriceImpact(priceImpact)} less than the input amount
due to high price impact
The output amount is estimated at {impact.toString()} less than the input amount due to high price
impact
</ThemedText.Caption>
<Rule />
</>
@@ -124,9 +107,7 @@ export function Trade({ trade }: { trade: InterfaceTrade<Currency, Currency, Tra
<RoutingDiagram trade={trade} />
</Column>
</Tooltip>
<TextButton color="primary" onClick={() => setFlip(!flip)}>
{ratio}
</TextButton>
<Price trade={trade} outputUSDC={outputUSDC} />
</>
)
}

View File

@@ -5,7 +5,7 @@ import useActiveWeb3React from 'lib/hooks/useActiveWeb3React'
import { largeIconCss } from 'lib/icons'
import { Field } from 'lib/state/swap'
import styled, { ThemedText } from 'lib/theme'
import { useMemo } from 'react'
import { memo, useMemo } from 'react'
import { TradeState } from 'state/routing/types'
import Row from '../../Row'
@@ -17,16 +17,16 @@ const ToolbarRow = styled(Row)`
${largeIconCss}
`
export default function Toolbar({ disabled }: { disabled?: boolean }) {
export default memo(function Toolbar({ disabled }: { disabled?: boolean }) {
const { chainId } = useActiveWeb3React()
const {
[Field.INPUT]: { currency: inputCurrency, balance: inputBalance, amount: inputAmount },
[Field.OUTPUT]: { currency: outputCurrency, usdc: outputUSDC },
trade: { trade, state },
currencies: { [Field.INPUT]: inputCurrency, [Field.OUTPUT]: outputCurrency },
currencyBalances: { [Field.INPUT]: balance },
impact,
} = useSwapInfo()
const isRouteLoading = state === TradeState.SYNCING || state === TradeState.LOADING
const isAmountPopulated = useIsAmountPopulated()
const { type: wrapType, loading: wrapLoading } = useWrapCallback()
const { type: wrapType } = useWrapCallback()
const caption = useMemo(() => {
if (disabled) {
return <Caption.ConnectWallet />
@@ -37,34 +37,39 @@ export default function Toolbar({ disabled }: { disabled?: boolean }) {
}
if (inputCurrency && outputCurrency && isAmountPopulated) {
if (wrapType !== WrapType.NOT_APPLICABLE) {
return <Caption.WrapCurrency wrapType={wrapType} loading={wrapLoading} />
}
if (isRouteLoading) {
if (state === TradeState.SYNCING || state === TradeState.LOADING) {
return <Caption.LoadingTrade />
}
if (!trade?.swaps) {
if (inputBalance && inputAmount?.greaterThan(inputBalance)) {
return <Caption.InsufficientBalance currency={inputCurrency} />
}
if (wrapType !== WrapType.NONE) {
return <Caption.WrapCurrency inputCurrency={inputCurrency} outputCurrency={outputCurrency} />
}
if (state === TradeState.NO_ROUTE_FOUND || (trade && !trade.swaps)) {
return <Caption.InsufficientLiquidity />
}
if (balance && trade?.inputAmount.greaterThan(balance)) {
return <Caption.InsufficientBalance currency={trade.inputAmount.currency} />
if (trade?.inputAmount && trade.outputAmount) {
return <Caption.Trade trade={trade} outputUSDC={outputUSDC} impact={impact} />
}
if (trade.inputAmount && trade.outputAmount) {
return <Caption.Trade trade={trade} />
if (state === TradeState.INVALID) {
return <Caption.Error />
}
}
return <Caption.Empty />
}, [
balance,
chainId,
disabled,
impact,
inputAmount,
inputBalance,
inputCurrency,
isAmountPopulated,
isRouteLoading,
outputCurrency,
outputUSDC,
state,
trade,
wrapLoading,
wrapType,
])
@@ -78,4 +83,4 @@ export default function Toolbar({ disabled }: { disabled?: boolean }) {
</ThemedText.Caption>
</>
)
}
})

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

@@ -47,11 +47,15 @@ export interface SwapProps extends TokenDefaults, FeeOptions {
onConnectWallet?: () => void
}
export default function Swap(props: SwapProps) {
function Updaters(props: SwapProps & { disabled: boolean }) {
useSyncTokenList(props.tokenList)
useSyncTokenDefaults(props)
useSyncConvenienceFee(props)
return props.disabled ? null : <SwapInfoUpdater />
}
export default function Swap(props: SwapProps) {
const { active, account } = useActiveWeb3React()
const [wrapper, setWrapper] = useState<HTMLDivElement | null>(null)
@@ -71,8 +75,9 @@ export default function Swap(props: SwapProps) {
const isInteractive = Boolean(active && onSupportedNetwork)
return (
<SwapPropValidator {...props}>
{isSwapSupported && <SwapInfoUpdater />}
<>
<SwapPropValidator {...props} />
<Updaters {...props} disabled={!isSwapSupported} />
<Header title={<Trans>Swap</Trans>}>
{active && <Wallet disabled={!account} onClick={props.onConnectWallet} />}
<Settings disabled={!isInteractive} />
@@ -92,6 +97,6 @@ export default function Swap(props: SwapProps) {
<StatusDialog tx={displayTx} onClose={() => setDisplayTxHash()} />
</Dialog>
)}
</SwapPropValidator>
</>
)
}

View File

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

View File

@@ -22,9 +22,14 @@ const StyledTokenButton = styled(Button)<{ empty?: boolean }>`
const TokenButtonRow = styled(Row)<{ collapsed: boolean }>`
height: 1.2em;
max-width: ${({ collapsed }) => (collapsed ? '1.2' : '8.2')}em;
overflow-x: hidden;
// max-width must have an absolute value in order to transition.
max-width: ${({ collapsed }) => (collapsed ? '1.2em' : '12em')};
overflow: hidden;
transition: max-width 0.25s linear;
img {
min-width: 1.2em;
}
`
interface TokenButtonProps {

View File

@@ -5,7 +5,7 @@ import { useCurrencyBalances } from 'lib/hooks/useCurrencyBalance'
import useNativeCurrency from 'lib/hooks/useNativeCurrency'
import useTokenList, { useIsTokenListLoaded, useQueryCurrencies } from 'lib/hooks/useTokenList'
import styled, { ThemedText } from 'lib/theme'
import { ElementRef, useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { ElementRef, memo, useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { currencyId } from 'utils/currencyId'
import Column from '../Column'
@@ -68,7 +68,7 @@ export function TokenSelectDialog({ value, onSelect }: TokenSelectDialogProps) {
const baseTokens: Currency[] = [] // TODO(zzmp): Add base tokens to token list functionality
const input = useRef<HTMLInputElement>(null)
useEffect(() => input.current?.focus(), [input])
useEffect(() => input.current?.focus({ preventScroll: true }), [input])
const [options, setOptions] = useState<ElementRef<typeof TokenOptions> | null>(null)
@@ -97,7 +97,21 @@ export function TokenSelectDialog({ value, onSelect }: TokenSelectDialogProps) {
)}
<Rule padded />
</Column>
{isLoaded ? <TokenOptions tokens={tokens} onSelect={onSelect} ref={setOptions} /> : <TokenOptionsSkeleton />}
{isLoaded ? (
tokens.length ? (
<TokenOptions tokens={tokens} onSelect={onSelect} ref={setOptions} />
) : (
<Column padded>
<Row justify="center">
<ThemedText.Body1 color="secondary">
<Trans>No results found.</Trans>
</ThemedText.Body1>
</Row>
</Column>
)
) : (
<TokenOptionsSkeleton />
)}
</>
)
}
@@ -109,10 +123,11 @@ interface TokenSelectProps {
onSelect: (value: Currency) => void
}
export default function TokenSelect({ value, collapsed, disabled, onSelect }: TokenSelectProps) {
export default memo(function TokenSelect({ value, collapsed, disabled, onSelect }: TokenSelectProps) {
usePrefetchBalances()
const [open, setOpen] = useState(false)
const onOpen = useCallback(() => setOpen(true), [])
const selectAndClose = useCallback(
(value: Currency) => {
onSelect(value)
@@ -122,7 +137,7 @@ export default function TokenSelect({ value, collapsed, disabled, onSelect }: To
)
return (
<>
<TokenButton value={value} collapsed={collapsed} disabled={disabled} onClick={() => setOpen(true)} />
<TokenButton value={value} collapsed={collapsed} disabled={disabled} onClick={onOpen} />
{open && (
<Dialog color="module" onClose={() => setOpen(false)}>
<TokenSelectDialog value={value} onSelect={selectAndClose} />
@@ -130,4 +145,4 @@ export default function TokenSelect({ value, collapsed, disabled, onSelect }: To
)}
</>
)
}
})

View File

@@ -1,40 +0,0 @@
import { initializeConnector } from '@web3-react/core'
import { EIP1193 } from '@web3-react/eip1193'
import { Actions, Connector, Provider as Eip1193Provider } from '@web3-react/types'
import { Url } from '@web3-react/url'
import { SetStateAction } from 'jotai'
import { RESET, useUpdateAtom } from 'jotai/utils'
import { injectedAtom, urlAtom, Web3ReactState } from 'lib/state/web3'
import { ReactNode, useEffect } from 'react'
interface Web3ProviderProps {
jsonRpcEndpoint?: string
provider?: Eip1193Provider
children: ReactNode
}
function useConnector<T extends { new (actions: Actions, initializer: I): Connector }, I>(
Connector: T,
initializer: I | undefined,
setContext: (update: typeof RESET | SetStateAction<Web3ReactState>) => void
) {
return useEffect(() => {
if (initializer) {
const state = initializeConnector((actions) => new Connector(actions, initializer))
state[0].activate()
setContext(state)
} else {
setContext(RESET)
}
}, [Connector, initializer, setContext])
}
export default function Web3Provider({ jsonRpcEndpoint, provider, children }: Web3ProviderProps) {
const setUrl = useUpdateAtom(urlAtom)
useConnector(Url, jsonRpcEndpoint, setUrl)
const setInjected = useUpdateAtom(injectedAtom)
useConnector(EIP1193, provider, setInjected)
return <>{children}</>
}

View File

@@ -1,21 +1,20 @@
import { Provider as EthersProvider } from '@ethersproject/abstract-provider'
import { JsonRpcProvider } from '@ethersproject/providers'
import { Provider as Eip1193Provider } from '@web3-react/types'
import { DEFAULT_LOCALE, SupportedLocale } from 'constants/locales'
import { Provider as AtomProvider } from 'jotai'
import { TransactionsUpdater } from 'lib/hooks/transactions'
import { ActiveWeb3Provider } from 'lib/hooks/useActiveWeb3React'
import { BlockUpdater } from 'lib/hooks/useBlockNumber'
import useEip1193Provider from 'lib/hooks/useEip1193Provider'
import { UNMOUNTING } from 'lib/hooks/useUnmount'
import { Provider as I18nProvider } from 'lib/i18n'
import { MulticallUpdater, store as multicallStore } from 'lib/state/multicall'
import styled, { keyframes, Theme, ThemeProvider } from 'lib/theme'
import { UNMOUNTING } from 'lib/utils/animations'
import { PropsWithChildren, StrictMode, useState } from 'react'
import { Provider as ReduxProvider } from 'react-redux'
import { Modal, Provider as DialogProvider } from './Dialog'
import ErrorBoundary, { ErrorHandler } from './Error/ErrorBoundary'
import WidgetPropValidator from './Error/WidgetsPropsValidator'
import Web3Provider from './Web3Provider'
const WidgetWrapper = styled.div<{ width?: number | string }>`
-moz-osx-font-smoothing: grayscale;
@@ -40,26 +39,27 @@ const WidgetWrapper = styled.div<{ width?: number | string }>`
* {
box-sizing: border-box;
font-family: ${({ theme }) => theme.fontFamily};
font-family: ${({ theme }) => (typeof theme.fontFamily === 'string' ? theme.fontFamily : theme.fontFamily.font)};
@supports (font-variation-settings: normal) {
font-family: ${({ theme }) => theme.fontFamilyVariable};
font-family: ${({ theme }) => (typeof theme.fontFamily === 'string' ? undefined : theme.fontFamily.variable)};
}
}
`
const slideDown = keyframes`
to {
const slideIn = keyframes`
from {
transform: translateY(calc(100% - 0.25em));
}
`
const slideUp = keyframes`
from {
const slideOut = keyframes`
to {
transform: translateY(calc(100% - 0.25em));
}
`
const DialogWrapper = styled.div`
border-radius: ${({ theme }) => theme.borderRadius * 0.75}em;
height: calc(100% - 0.5em);
left: 0;
margin: 0.25em;
@@ -73,11 +73,11 @@ const DialogWrapper = styled.div`
}
${Modal} {
animation: ${slideUp} 0.25s ease-in-out;
}
animation: ${slideIn} 0.25s ease-in;
${Modal}.${UNMOUNTING} {
animation: ${slideDown} 0.25s ease-in-out;
&.${UNMOUNTING} {
animation: ${slideOut} 0.25s ease-out;
}
}
`
@@ -94,8 +94,8 @@ function Updaters() {
export type WidgetProps = {
theme?: Theme
locale?: SupportedLocale
provider?: Eip1193Provider | EthersProvider
jsonRpcEndpoint?: string
provider?: Eip1193Provider | JsonRpcProvider
jsonRpcEndpoint?: string | JsonRpcProvider
width?: string | number
dialog?: HTMLElement | null
className?: string
@@ -114,31 +114,29 @@ export default function Widget(props: PropsWithChildren<WidgetProps>) {
className,
onError,
} = props
const eip1193 = useEip1193Provider(provider)
const [dialog, setDialog] = useState<HTMLDivElement | null>(null)
return (
<StrictMode>
<I18nProvider locale={locale}>
<ThemeProvider theme={theme}>
<WidgetWrapper width={width} className={className}>
<ThemeProvider theme={theme}>
<WidgetWrapper width={width} className={className}>
<I18nProvider locale={locale}>
<DialogWrapper ref={setDialog} />
<DialogProvider value={userDialog || dialog}>
<ErrorBoundary onError={onError}>
<WidgetPropValidator {...props}>
<ReduxProvider store={multicallStore}>
<AtomProvider>
<Web3Provider provider={eip1193} jsonRpcEndpoint={jsonRpcEndpoint}>
<Updaters />
{children}
</Web3Provider>
</AtomProvider>
</ReduxProvider>
</WidgetPropValidator>
<WidgetPropValidator {...props} />
<ReduxProvider store={multicallStore}>
<AtomProvider>
<ActiveWeb3Provider provider={provider} jsonRpcEndpoint={jsonRpcEndpoint}>
<Updaters />
{children}
</ActiveWeb3Provider>
</AtomProvider>
</ReduxProvider>
</ErrorBoundary>
</DialogProvider>
</WidgetWrapper>
</ThemeProvider>
</I18nProvider>
</I18nProvider>
</WidgetWrapper>
</ThemeProvider>
</StrictMode>
)
}

View File

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

View File

@@ -1,4 +1,6 @@
import { BigintIsh, CurrencyAmount, Token, TradeType } from '@uniswap/sdk-core'
// This file is lazy-loaded, so the import of smart-order-router is intentional.
// eslint-disable-next-line no-restricted-imports
import { AlphaRouter, AlphaRouterConfig, AlphaRouterParams, ChainId } from '@uniswap/smart-order-router'
import JSBI from 'jsbi'
import { GetQuoteResult } from 'state/routing/types'

View File

@@ -1,15 +1,18 @@
import 'setimmediate'
import { Protocol } from '@uniswap/router-sdk'
import { Currency, CurrencyAmount, TradeType } from '@uniswap/sdk-core'
import { ChainId } from '@uniswap/smart-order-router'
import { SupportedChainId } from 'constants/chains'
import useDebounce from 'hooks/useDebounce'
import { useStablecoinAmountFromFiatValue } from 'hooks/useUSDCPrice'
import { useEffect, useMemo, useState } from 'react'
import { useCallback, useMemo } from 'react'
import { GetQuoteResult, InterfaceTrade, TradeState } from 'state/routing/types'
import { computeRoutes, transformRoutesToTrade } from 'state/routing/utils'
import useWrapCallback, { WrapType } from '../swap/useWrapCallback'
import useActiveWeb3React from '../useActiveWeb3React'
import { getClientSideQuote } from './clientSideSmartOrderRouter'
import { useGetIsValidBlock } from '../useIsValidBlock'
import usePoll from '../usePoll'
import { useRoutingAPIArguments } from './useRoutingAPIArguments'
/**
@@ -17,20 +20,17 @@ import { useRoutingAPIArguments } from './useRoutingAPIArguments'
* Defaults are defined in https://github.com/Uniswap/smart-order-router/blob/309e6f6603984d3b5aef0733b0cfaf129c29f602/src/routers/alpha-router/config.ts#L83.
*/
const DistributionPercents: { [key: number]: number } = {
[ChainId.MAINNET]: 10,
[ChainId.OPTIMISM]: 10,
[ChainId.OPTIMISTIC_KOVAN]: 10,
[ChainId.ARBITRUM_ONE]: 25,
[ChainId.ARBITRUM_RINKEBY]: 25,
[SupportedChainId.MAINNET]: 10,
[SupportedChainId.OPTIMISM]: 10,
[SupportedChainId.OPTIMISTIC_KOVAN]: 10,
[SupportedChainId.ARBITRUM_ONE]: 25,
[SupportedChainId.ARBITRUM_RINKEBY]: 25,
}
const DEFAULT_DISTRIBUTION_PERCENT = 10
function getConfig(chainId: ChainId | undefined) {
function getConfig(chainId: SupportedChainId | undefined) {
return {
// Limit to only V2 and V3.
protocols: [Protocol.V2, Protocol.V3],
distributionPercent: (chainId && DistributionPercents[chainId]) ?? DEFAULT_DISTRIBUTION_PERCENT,
}
}
@@ -43,22 +43,20 @@ export default function useClientSideSmartOrderRouterTrade<TTradeType extends Tr
state: TradeState
trade: InterfaceTrade<Currency, Currency, TTradeType> | undefined
} {
const amount = useMemo(() => amountSpecified?.asFraction, [amountSpecified])
const [currencyIn, currencyOut] =
tradeType === TradeType.EXACT_INPUT
? [amountSpecified?.currency, otherCurrency]
: [otherCurrency, amountSpecified?.currency]
// Debounce is used to prevent excessive requests to SOR, as it is data intensive.
// This helps provide a "syncing" state the UI can reference for loading animations.
const inputs = useMemo(() => [tradeType, amountSpecified, otherCurrency], [tradeType, amountSpecified, otherCurrency])
const debouncedInputs = useDebounce(inputs, 200)
const isDebouncing = inputs !== debouncedInputs
const chainId = amountSpecified?.currency.chainId
const { library } = useActiveWeb3React()
const [currencyIn, currencyOut]: [Currency | undefined, Currency | undefined] = useMemo(
() =>
tradeType === TradeType.EXACT_INPUT
? [amountSpecified?.currency, otherCurrency]
: [otherCurrency, amountSpecified?.currency],
[amountSpecified, otherCurrency, tradeType]
// Fast user actions (ie updating the input) should be debounced, but currency changes should not.
const [debouncedAmount, debouncedCurrencyIn, debouncedCurrencyOut] = useDebounce(
useMemo(() => [amount, currencyIn, currencyOut], [amount, currencyIn, currencyOut]),
200
)
const isDebouncing =
amount !== debouncedAmount && currencyIn === debouncedCurrencyIn && currencyOut === debouncedCurrencyOut
const queryArgs = useRoutingAPIArguments({
tokenIn: currencyIn,
@@ -67,46 +65,38 @@ export default function useClientSideSmartOrderRouterTrade<TTradeType extends Tr
tradeType,
useClientSideRouter: true,
})
const chainId = amountSpecified?.currency.chainId
const { library } = useActiveWeb3React()
const params = useMemo(() => chainId && library && { chainId, provider: library }, [chainId, library])
const [loading, setLoading] = useState(false)
const [{ data: quoteResult, error }, setResult] = useState<{
data?: GetQuoteResult
error?: unknown
}>({ error: undefined })
const config = useMemo(() => getConfig(chainId), [chainId])
const { type: wrapType } = useWrapCallback()
// When arguments update, make a new call to SOR for updated quote
useEffect(() => {
if (wrapType !== WrapType.NOT_APPLICABLE) {
return
}
setLoading(true)
if (isDebouncing) return
const getQuoteResult = useCallback(async (): Promise<{ data?: GetQuoteResult; error?: unknown }> => {
if (wrapType !== WrapType.NONE) return { error: undefined }
if (!queryArgs || !params) return { error: undefined }
try {
// Lazy-load the smart order router to improve initial pageload times.
const quoteResult = await (
await import('./clientSideSmartOrderRouter')
).getClientSideQuote(queryArgs, params, config)
let stale = false
fetchQuote()
return () => {
stale = true
setLoading(false)
// There is significant post-fetch processing, so delay a tick to prevent dropped frames.
// This is only important in the context of integrations - if we control the whole site,
// then we can afford to drop a few frames.
return new Promise((resolve) => setImmediate(() => resolve(quoteResult)))
} catch {
return { error: true }
}
}, [config, params, queryArgs, wrapType])
async function fetchQuote() {
if (queryArgs && params) {
let result
try {
result = await getClientSideQuote(queryArgs, params, config)
} catch {
result = { error: true }
}
if (!stale) {
setResult(result)
setLoading(false)
}
}
}
}, [queryArgs, params, config, isDebouncing, wrapType])
const getIsValidBlock = useGetIsValidBlock()
const { data: quoteResult, error } = usePoll(getQuoteResult, JSON.stringify(queryArgs), {
debounce: isDebouncing,
staleCallback: useCallback(({ data }) => !getIsValidBlock(Number(data?.blockNumber) || 0), [getIsValidBlock]),
}) ?? {
error: undefined,
}
const isValid = getIsValidBlock(Number(quoteResult?.blockNumber) || 0)
const route = useMemo(
() => computeRoutes(currencyIn, currencyOut, tradeType, quoteResult),
@@ -129,11 +119,12 @@ export default function useClientSideSmartOrderRouterTrade<TTradeType extends Tr
return { state: TradeState.INVALID, trade: undefined }
}
// Returns the last trade state while syncing/loading to avoid jank from clearing the last trade while loading.
if (isDebouncing) {
return { state: TradeState.SYNCING, trade }
} else if (loading) {
return { state: TradeState.LOADING, trade }
if (!trade && !error) {
if (isDebouncing) {
return { state: TradeState.SYNCING, trade: undefined }
} else if (!isValid) {
return { state: TradeState.LOADING, trade: undefined }
}
}
let otherAmount = undefined
@@ -148,7 +139,7 @@ export default function useClientSideSmartOrderRouterTrade<TTradeType extends Tr
}
}
if (error || !otherAmount || !route || route.length === 0 || !queryArgs) {
if (error || !otherAmount || !route || route.length === 0) {
return { state: TradeState.NO_ROUTE_FOUND, trade: undefined }
}
@@ -156,5 +147,5 @@ export default function useClientSideSmartOrderRouterTrade<TTradeType extends Tr
return { state: TradeState.VALID, trade }
}
return { state: TradeState.INVALID, trade: undefined }
}, [currencyIn, currencyOut, isDebouncing, loading, quoteResult, error, route, queryArgs, trade, tradeType])
}, [currencyIn, currencyOut, trade, error, isValid, quoteResult, route, isDebouncing, tradeType])
}

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
@@ -83,7 +73,7 @@ export function useIsAmountPopulated() {
export function useSwapAmount(field: Field): [string | undefined, (amount: string) => void] {
const amount = useAtomValue(amountAtom)
const isFieldIndependent = useIsSwapFieldIndependent(field)
const value = useMemo(() => (isFieldIndependent ? amount : undefined), [amount, isFieldIndependent])
const value = isFieldIndependent ? amount : undefined
const updateSwap = useUpdateAtom(swapAtom)
const updateAmount = useCallback(
(amount: string) =>
@@ -101,8 +91,9 @@ export function useSwapCurrencyAmount(field: Field): CurrencyAmount<Currency> |
const isAmountPopulated = useIsAmountPopulated()
const [swapAmount] = useSwapAmount(field)
const [swapCurrency] = useSwapCurrency(field)
const currencyAmount = useMemo(() => tryParseCurrencyAmount(swapAmount, swapCurrency), [swapAmount, swapCurrency])
if (isFieldIndependent && isAmountPopulated) {
return tryParseCurrencyAmount(swapAmount, swapCurrency)
return currencyAmount
}
return
}

View File

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

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,151 +1,111 @@
import { Trans } from '@lingui/macro'
import { Currency, CurrencyAmount, Percent, TradeType } from '@uniswap/sdk-core'
import { FeeOptions } from '@uniswap/v3-sdk'
import { atom } from 'jotai'
import { useAtomValue, useUpdateAtom } from 'jotai/utils'
import { useCurrencyBalances } from 'lib/hooks/useCurrencyBalance'
import { feeOptionsAtom, Field, swapAtom } from 'lib/state/swap'
import { Field, swapAtom } from 'lib/state/swap'
import tryParseCurrencyAmount from 'lib/utils/tryParseCurrencyAmount'
import { ReactNode, useEffect, useMemo } from 'react'
import { useEffect, useMemo } from 'react'
import { InterfaceTrade, TradeState } from 'state/routing/types'
import { isAddress } from '../../../utils'
import useActiveWeb3React from '../useActiveWeb3React'
import useAllowedSlippage from '../useAllowedSlippage'
import useSlippage, { Slippage } from '../useSlippage'
import useUSDCPriceImpact, { PriceImpact } from '../useUSDCPriceImpact'
import { useBestTrade } from './useBestTrade'
import useWrapCallback, { WrapType } from './useWrapCallback'
interface SwapField {
currency?: Currency
amount?: CurrencyAmount<Currency>
balance?: CurrencyAmount<Currency>
usdc?: CurrencyAmount<Currency>
}
interface SwapInfo {
currencies: { [field in Field]?: Currency }
currencyBalances: { [field in Field]?: CurrencyAmount<Currency> }
tradeCurrencyAmounts: { [field in Field]?: CurrencyAmount<Currency> }
[Field.INPUT]: SwapField
[Field.OUTPUT]: SwapField
trade: {
trade?: InterfaceTrade<Currency, Currency, TradeType>
state: TradeState
}
allowedSlippage: Percent
feeOptions: FeeOptions | undefined
}
const BAD_RECIPIENT_ADDRESSES: { [address: string]: true } = {
'0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f': true, // v2 factory
'0xf164fC0Ec4E93095b804a4795bBe1e041497b92a': true, // v2 router 01
'0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D': true, // v2 router 02
slippage: Slippage
impact?: PriceImpact
}
// from the current swap inputs, compute the best trade and return it.
function useComputeSwapInfo(): SwapInfo {
const { account } = useActiveWeb3React()
const {
independentField,
amount,
[Field.INPUT]: inputCurrency,
[Field.OUTPUT]: outputCurrency,
} = useAtomValue(swapAtom)
const feeOptions = useAtomValue(feeOptionsAtom)
const to = account
const relevantTokenBalances = useCurrencyBalances(
account,
useMemo(() => [inputCurrency ?? undefined, outputCurrency ?? undefined], [inputCurrency, outputCurrency])
)
const { type: wrapType } = useWrapCallback()
const isWrapping = wrapType === WrapType.WRAP || wrapType === WrapType.UNWRAP
const { independentField, amount, [Field.INPUT]: currencyIn, [Field.OUTPUT]: currencyOut } = useAtomValue(swapAtom)
const isExactIn = independentField === Field.INPUT
const parsedAmount = useMemo(
() => tryParseCurrencyAmount(amount, (isExactIn ? inputCurrency : outputCurrency) ?? undefined),
[inputCurrency, isExactIn, outputCurrency, amount]
)
//@TODO(ianlapham): this would eventually be replaced with routing api logic.
const parsedAmount = useMemo(
() => tryParseCurrencyAmount(amount, (isExactIn ? currencyIn : currencyOut) ?? undefined),
[amount, isExactIn, currencyIn, currencyOut]
)
const trade = useBestTrade(
isExactIn ? TradeType.EXACT_INPUT : TradeType.EXACT_OUTPUT,
parsedAmount,
(isExactIn ? outputCurrency : inputCurrency) ?? undefined
(isExactIn ? currencyOut : currencyIn) ?? undefined
)
const currencies = useMemo(
() => ({
[Field.INPUT]: inputCurrency ?? undefined,
[Field.OUTPUT]: outputCurrency ?? undefined,
}),
[inputCurrency, outputCurrency]
const amountIn = useMemo(
() => (isWrapping || isExactIn ? parsedAmount : trade.trade?.inputAmount),
[isExactIn, isWrapping, parsedAmount, trade.trade?.inputAmount]
)
const amountOut = useMemo(
() => (isWrapping || !isExactIn ? parsedAmount : trade.trade?.outputAmount),
[isExactIn, isWrapping, parsedAmount, trade.trade?.outputAmount]
)
const [balanceIn, balanceOut] = useCurrencyBalances(
account,
useMemo(() => [currencyIn, currencyOut], [currencyIn, currencyOut])
)
const currencyBalances = useMemo(
() => ({
[Field.INPUT]: relevantTokenBalances[0],
[Field.OUTPUT]: relevantTokenBalances[1],
}),
[relevantTokenBalances]
)
const tradeCurrencyAmounts = useMemo(
() => ({
[Field.INPUT]: trade.trade?.inputAmount,
[Field.OUTPUT]: trade.trade?.outputAmount,
}),
[trade.trade?.inputAmount, trade.trade?.outputAmount]
)
const allowedSlippage = useAllowedSlippage(trade.trade)
const inputError = useMemo(() => {
let inputError: ReactNode | undefined
if (!account) {
inputError = <Trans>Connect Wallet</Trans>
}
if (!currencies[Field.INPUT] || !currencies[Field.OUTPUT]) {
inputError = inputError ?? <Trans>Select a token</Trans>
}
if (!parsedAmount) {
inputError = inputError ?? <Trans>Enter an amount</Trans>
}
const formattedTo = isAddress(to)
if (!to || !formattedTo) {
inputError = inputError ?? <Trans>Enter a recipient</Trans>
} else {
if (BAD_RECIPIENT_ADDRESSES[formattedTo]) {
inputError = inputError ?? <Trans>Invalid recipient</Trans>
}
}
// compare input balance to max input based on version
const [balanceIn, amountIn] = [currencyBalances[Field.INPUT], trade.trade?.maximumAmountIn(allowedSlippage)]
if (balanceIn && amountIn && balanceIn.lessThan(amountIn)) {
inputError = <Trans>Insufficient {amountIn.currency.symbol} balance</Trans>
}
return inputError
}, [account, allowedSlippage, currencies, currencyBalances, parsedAmount, to, trade.trade])
// Compute slippage and impact off of the trade so that it refreshes with the trade.
// (Using amountIn/amountOut would show (incorrect) intermediate values.)
const slippage = useSlippage(trade.trade)
const { inputUSDC, outputUSDC, impact } = useUSDCPriceImpact(trade.trade?.inputAmount, trade.trade?.outputAmount)
return useMemo(
() => ({
currencies,
currencyBalances,
inputError,
[Field.INPUT]: {
currency: currencyIn,
amount: amountIn,
balance: balanceIn,
usdc: inputUSDC,
},
[Field.OUTPUT]: {
currency: currencyOut,
amount: amountOut,
balance: balanceOut,
usdc: outputUSDC,
},
trade,
tradeCurrencyAmounts,
allowedSlippage,
feeOptions,
slippage,
impact,
}),
[currencies, currencyBalances, inputError, trade, tradeCurrencyAmounts, allowedSlippage, feeOptions]
[
amountIn,
amountOut,
balanceIn,
balanceOut,
currencyIn,
currencyOut,
impact,
inputUSDC,
outputUSDC,
slippage,
trade,
]
)
}
const swapInfoAtom = atom<SwapInfo>({
currencies: {},
currencyBalances: {},
[Field.INPUT]: {},
[Field.OUTPUT]: {},
trade: { state: TradeState.INVALID },
tradeCurrencyAmounts: {},
allowedSlippage: new Percent(0),
feeOptions: undefined,
slippage: { auto: true, allowed: new Percent(0) },
})
export function SwapInfoUpdater() {

View File

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

View File

@@ -1,19 +0,0 @@
import { Web3ReactHooks } from '@web3-react/core'
import { useAtomValue } from 'jotai/utils'
import { injectedAtom, urlAtom, Web3ReactState } from 'lib/state/web3'
export function useActiveWeb3ReactState(): Web3ReactState {
const injected = useAtomValue(injectedAtom)
const url = useAtomValue(urlAtom)
return injected[1].useIsActive() ? injected : url
}
export function useActiveWeb3ReactHooks(): Web3ReactHooks {
const [, hooks] = useActiveWeb3ReactState()
return hooks
}
export default function useActiveWeb3React() {
const { useProvider, useWeb3React } = useActiveWeb3ReactHooks()
return useWeb3React(useProvider())
}

View File

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

View File

@@ -1,27 +0,0 @@
import { Currency, Percent, TradeType } from '@uniswap/sdk-core'
import useAutoSlippageTolerance from 'hooks/useAutoSlippageTolerance'
import { useAtomValue } from 'jotai/utils'
import { autoSlippageAtom, maxSlippageAtom } from 'lib/state/settings'
import { InterfaceTrade } from 'state/routing/types'
export function toPercent(maxSlippage: number | undefined): Percent | undefined {
if (!maxSlippage) return undefined
const numerator = Math.floor(maxSlippage * 100)
return new Percent(numerator, 10_000)
}
/** Returns the user-inputted max slippage. */
export default function useAllowedSlippage(trade: InterfaceTrade<Currency, Currency, TradeType> | undefined): Percent {
const autoSlippage = useAutoSlippageTolerance(trade)
const maxSlippage = toPercent(useAtomValue(maxSlippageAtom))
return useAtomValue(autoSlippageAtom) ? autoSlippage : maxSlippage ?? autoSlippage
}
export const MAX_VALID_SLIPPAGE = new Percent(1, 2)
export const MIN_HIGH_SLIPPAGE = new Percent(1, 100)
export function getSlippageWarning(slippage?: Percent): 'warning' | 'error' | undefined {
if (slippage?.greaterThan(MAX_VALID_SLIPPAGE)) return 'error'
if (slippage?.greaterThan(MIN_HIGH_SLIPPAGE)) return 'warning'
return
}

View File

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

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

88
src/lib/hooks/usePoll.ts Normal file
View File

@@ -0,0 +1,88 @@
import ms from 'ms.macro'
import { useEffect, useMemo, useState } from 'react'
const DEFAULT_POLLING_INTERVAL = ms`15s`
const DEFAULT_KEEP_UNUSED_DATA_FOR = ms`10s`
interface PollingOptions<T> {
// If true, any cached result will be returned, but no new fetch will be initiated.
debounce?: boolean
// If stale, any cached result will be returned, and a new fetch will be initiated.
staleCallback?: (value: T) => boolean
pollingInterval?: number
keepUnusedDataFor?: number
}
interface CacheEntry<T> {
ttl: number | null // null denotes a pending fetch
result?: T
}
export default function usePoll<T>(
fetch: () => Promise<T>,
key = '',
{
debounce = false,
staleCallback,
pollingInterval = DEFAULT_POLLING_INTERVAL,
keepUnusedDataFor = DEFAULT_KEEP_UNUSED_DATA_FOR,
}: PollingOptions<T>
): T | undefined {
const cache = useMemo(() => new Map<string, CacheEntry<T>>(), [])
const [, setData] = useState<{ key: string; result?: T }>({ key })
useEffect(() => {
if (debounce) return
let timeout: number
const entry = cache.get(key)
const isStale = staleCallback && entry?.result !== undefined ? staleCallback(entry.result) : false
if (entry) {
// If there is not a pending fetch (and there should be), queue one.
if (entry.ttl) {
if (isStale) {
poll() // stale results should be refetched immediately
} else if (entry.ttl && entry.ttl + keepUnusedDataFor > Date.now()) {
timeout = setTimeout(poll, Math.max(0, entry.ttl - Date.now()))
}
}
} else {
// If there is no cached entry, trigger a poll immediately.
poll()
}
setData({ key, result: entry?.result })
return () => {
clearTimeout(timeout)
}
async function poll(ttl = Date.now() + pollingInterval) {
timeout = setTimeout(poll, pollingInterval) // queue the next poll
cache.set(key, { ttl: null, ...cache.get(key) }) // mark the entry as a pending fetch
// Always set the result in the cache, but only set it as data if the key is still being queried.
const result = await fetch()
cache.set(key, { ttl, result })
setData((data) => (data.key === key ? { key, result } : data))
}
}, [cache, debounce, fetch, keepUnusedDataFor, key, pollingInterval, staleCallback])
useEffect(() => {
// Cleanup stale entries when a new key is used.
void key
const now = Date.now()
cache.forEach(({ ttl }, key) => {
if (ttl && ttl + keepUnusedDataFor <= now) {
cache.delete(key)
}
})
}, [cache, keepUnusedDataFor, key])
// Use data.result to force a re-render, but actually retrieve the data from the cache.
// This gives the _first_ render access to a new result, avoiding lag introduced by React.
return cache.get(key)?.result
}

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

@@ -0,0 +1,41 @@
import { Currency, Percent, TradeType } from '@uniswap/sdk-core'
import useAutoSlippageTolerance from 'hooks/useAutoSlippageTolerance'
import { useAtomValue } from 'jotai/utils'
import { autoSlippageAtom, maxSlippageAtom } from 'lib/state/settings'
import { useMemo } from 'react'
import { InterfaceTrade } from 'state/routing/types'
export function toPercent(maxSlippage: number | undefined): Percent | undefined {
if (!maxSlippage) return undefined
const numerator = Math.floor(maxSlippage * 100)
return new Percent(numerator, 10_000)
}
export interface Slippage {
auto: boolean
allowed: Percent
warning?: 'warning' | 'error'
}
/** Returns the allowed slippage, and whether it is auto-slippage. */
export default function useSlippage(trade: InterfaceTrade<Currency, Currency, TradeType> | undefined): Slippage {
const shouldUseAutoSlippage = useAtomValue(autoSlippageAtom)
const autoSlippage = useAutoSlippageTolerance(shouldUseAutoSlippage ? trade : undefined)
const maxSlippageValue = useAtomValue(maxSlippageAtom)
const maxSlippage = useMemo(() => toPercent(maxSlippageValue), [maxSlippageValue])
return useMemo(() => {
const auto = shouldUseAutoSlippage || !maxSlippage
const allowed = shouldUseAutoSlippage ? autoSlippage : maxSlippage ?? autoSlippage
const warning = auto ? undefined : getSlippageWarning(allowed)
return { auto, allowed, warning }
}, [autoSlippage, maxSlippage, shouldUseAutoSlippage])
}
export const MAX_VALID_SLIPPAGE = new Percent(1, 2)
export const MIN_HIGH_SLIPPAGE = new Percent(1, 100)
export function getSlippageWarning(slippage?: Percent): 'warning' | 'error' | undefined {
if (slippage?.greaterThan(MAX_VALID_SLIPPAGE)) return 'error'
if (slippage?.greaterThan(MIN_HIGH_SLIPPAGE)) return 'warning'
return
}

View File

@@ -1,7 +1,7 @@
import { NativeCurrency, Token } from '@uniswap/sdk-core'
import { TokenInfo, TokenList } from '@uniswap/token-lists'
import { atom } from 'jotai'
import { useAtomValue, useUpdateAtom } from 'jotai/utils'
import { atom, useAtom } from 'jotai'
import { useAtomValue } from 'jotai/utils'
import useActiveWeb3React from 'lib/hooks/useActiveWeb3React'
import resolveENSContentHash from 'lib/utils/resolveENSContentHash'
import { useCallback, useEffect, useMemo, useState } from 'react'
@@ -14,31 +14,35 @@ import { validateTokens } from './validateTokenList'
export const DEFAULT_TOKEN_LIST = 'https://gateway.ipfs.io/ipns/tokens.uniswap.org'
const chainTokenMapAtom = atom<ChainTokenMap | undefined>(undefined)
const chainTokenMapAtom = atom<ChainTokenMap | null>(null)
export function useIsTokenListLoaded() {
return Boolean(useAtomValue(chainTokenMapAtom))
}
export function useSyncTokenList(list: string | TokenInfo[] = DEFAULT_TOKEN_LIST): void {
const { chainId, library } = useActiveWeb3React()
const setChainTokenMap = useUpdateAtom(chainTokenMapAtom)
// Error boundaries will not catch (non-rendering) async errors, but it should still be shown
const [error, setError] = useState<Error>()
if (error) throw error
const [chainTokenMap, setChainTokenMap] = useAtom(chainTokenMapAtom)
useEffect(() => setChainTokenMap(null), [list, setChainTokenMap])
const { chainId, library } = useActiveWeb3React()
const resolver = useCallback(
(ensName: string) => {
if (library && chainId === 1) {
// TODO(zzmp): Use network resolver when wallet is not on chainId === 1.
return resolveENSContentHash(ensName, library)
}
throw new Error('Could not construct mainnet ENS resolver')
},
[chainId, library]
)
useEffect(() => {
// If the list was already loaded, don't reload it.
if (chainTokenMap) return
let stale = false
activateList(list)
return () => {
@@ -53,19 +57,20 @@ export function useSyncTokenList(list: string | TokenInfo[] = DEFAULT_TOKEN_LIST
} else {
tokens = await validateTokens(list)
}
const tokenMap = tokensToChainTokenMap(tokens) // also caches the fetched tokens, so it is invoked even if stale
// tokensToChainTokenMap also caches the fetched tokens, so it must be invoked even if stale.
const map = tokensToChainTokenMap(tokens)
if (!stale) {
setChainTokenMap(tokenMap)
setChainTokenMap(map)
setError(undefined)
}
} catch (e: unknown) {
if (!stale) {
setChainTokenMap(undefined)
// Do not update the token map, in case the map was already resolved without error on mainnet.
setError(e as Error)
}
}
}
}, [list, resolver, setChainTokenMap])
}, [chainTokenMap, list, resolver, setChainTokenMap])
}
export default function useTokenList(): WrappedTokenInfo[] {

View File

@@ -2,24 +2,42 @@ import { Currency, CurrencyAmount, Percent, Token } from '@uniswap/sdk-core'
import { useUSDCValue } from 'hooks/useUSDCPrice'
import { useMemo } from 'react'
import { computeFiatValuePriceImpact } from 'utils/computeFiatValuePriceImpact'
import { getPriceImpactWarning } from 'utils/prices'
export interface PriceImpact {
percent: Percent
warning?: 'warning' | 'error'
toString(): string
}
/**
* Computes input/output USDC equivalents and the price impact.
* Returns the price impact as a human readable string.
*/
export default function useUSDCPriceImpact(
inputAmount: CurrencyAmount<Currency> | undefined,
outputAmount: CurrencyAmount<Currency> | undefined
): {
inputUSDC?: CurrencyAmount<Token>
outputUSDC?: CurrencyAmount<Token>
priceImpact?: Percent
impact?: PriceImpact
} {
const inputUSDC = useUSDCValue(inputAmount) ?? undefined
const outputUSDC = useUSDCValue(outputAmount) ?? undefined
return useMemo(() => {
const priceImpact = computeFiatValuePriceImpact(inputUSDC, outputUSDC)
return { inputUSDC, outputUSDC, priceImpact }
const impact = priceImpact
? {
percent: priceImpact,
warning: getPriceImpactWarning(priceImpact),
toString: () => toHumanReadablePriceImpact(priceImpact),
}
: undefined
return { inputUSDC, outputUSDC, impact }
}, [inputUSDC, outputUSDC])
}
export function toHumanReadablePriceImpact(priceImpact: Percent): string {
function toHumanReadablePriceImpact(priceImpact: Percent): string {
const sign = priceImpact.lessThan(0) ? '+' : ''
const number = parseFloat(priceImpact.multiply(-1)?.toSignificant(3))
return `${sign}${number}%`

View File

@@ -1,33 +0,0 @@
import { RefObject, useEffect } from 'react'
export const UNMOUNTING = 'unmounting'
/**
* Delays a node's unmounting so that an animation may be applied.
* An animation *must* be applied, or the node will not unmount.
*/
export default function useUnmount(node: RefObject<HTMLElement>) {
useEffect(() => {
const current = node.current
const parent = current?.parentElement
const removeChild = parent?.removeChild
if (parent && removeChild) {
parent.removeChild = function <T extends Node>(child: T) {
if ((child as Node) === current) {
current.classList.add(UNMOUNTING)
current.onanimationend = () => {
removeChild.call(parent, child)
}
return child
} else {
return removeChild.call(parent, child) as T
}
}
}
return () => {
if (parent && removeChild) {
parent.removeChild = removeChild
}
}
}, [node])
}

View File

@@ -1,6 +1,6 @@
import { i18n } from '@lingui/core'
import { I18nProvider } from '@lingui/react'
import { DEFAULT_CATALOG, DEFAULT_LOCALE, SupportedLocale } from 'constants/locales'
import { SupportedLocale } from 'constants/locales'
import {
af,
ar,
@@ -78,12 +78,13 @@ const plurals: LocalePlural = {
export async function dynamicActivate(locale: SupportedLocale) {
i18n.loadLocaleData(locale, { plurals: () => plurals[locale] })
// There are no default messages in production; instead, bundle the default to save a network request:
// see https://github.com/lingui/js-lingui/issues/388#issuecomment-497779030
const catalog =
locale === DEFAULT_LOCALE ? DEFAULT_CATALOG : await import(`${process.env.REACT_APP_LOCALES}/${locale}.js`)
// Bundlers will either export it as default or as a named export named default.
i18n.load(locale, catalog.messages || catalog.default.messages)
try {
// There are no default messages in production,
// see https://github.com/lingui/js-lingui/issues/388#issuecomment-497779030
const catalog = await import(`${process.env.REACT_APP_LOCALES}/${locale}.js`)
// Bundlers will either export it as default or as a named export named default.
i18n.load(locale, catalog.messages || catalog.default.messages)
} catch {}
i18n.activate(locale)
}

View File

@@ -109,17 +109,18 @@ export const Check = styled(icon(CheckIcon))`
`
export const Expando = styled(icon(ExpandoIcon))<{ open: boolean }>`
path {
.left,
.right {
transition: transform 0.25s ease-in-out;
will-change: transform;
}
&:first-child {
transform: ${({ open }) => open && 'translateX(-25%)'};
}
.left {
transform: ${({ open }) => (open ? undefined : 'translateX(-25%)')};
}
&:last-child {
transform: ${({ open }) => open && 'translateX(25%)'};
}
.right {
transform: ${({ open }) => (open ? undefined : 'translateX(25%)')};
}
`

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