Compare commits

..

194 Commits

Author SHA1 Message Date
Zach Pomerantz
f50bcbdb2d fix: initial transitions (#3719)
* fix: rm action fade

* fix: disallow stale swaps

* fix: fade in buttons

* fix: fade in input text

* fix: standardize border handling

* fix: transition token button width

* fix: cleanup transitions

* fix: use transition for button

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

* remove console.logs

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

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

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

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

This reverts commit a96f7178e5.

* recover more gracefully from failed fee fetch

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

* fix: prevent unnecessary trade renders

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

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

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

* chore: clean up token defaults

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

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

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

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

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

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

* test: update cosmos to trigger edge cases

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

* chore: simplify atom usage

* fix: support single-token chain

* fix: avoid extra rpcs

* chore: rename isDisabled

* fix: simplify useUSDCPrice

* fix: simplify useComputeSwapInfo

* chore: include type

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

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

* feat: use a JsonRpcConnector

* chore: rm @ethersproject/experimental

* fix: assert Web3Provider in app

* fix: type providers more loosely

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

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

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

* pr feedback from z

* pr feedback from w and z

* separate README.md and INTERFACE_README.md

* Update WIDGETS_README.md

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

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

* fix: mv anti-janking up a level

* feat: add error caption for completeness

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

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

* feat: prompt wrap in wallet

* feat: prompt confirm in wallet

* fix: animations

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

* chore: mv approval data to its own hook

* chore: mv approval actions to approvals hook

* chore: simplify SwapButton logic

* fix: pass through approval amount

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

* fix: polling and validation logic

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

* fix: useUnmount portability

* chore: clean up dialog ordering

* fix: dialog border-radius

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

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

* fix: use a minimum fresh block

* fix: re-poll on stale data

* chore: rename to staleCallback

* check for undefined

* chore: rename fresh->valid

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

* fix: webkit-compat gradient
2022-03-21 10:26:35 -04:00
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
Ian Lapham
e569dc2152 fix: update default tokens on unsupported networks (#3470)
* update default tokens on unsupported networks

* update supported network with hook

* update defaults

* fix on default input token
2022-03-08 20:35:23 -05:00
Crowdin Bot
1aa042c5ef chore(i18n): synchronize translations from crowdin [skip ci] 2022-03-08 21:07:02 +00:00
Zach Pomerantz
1450315b98 chore: bump to v0.0.21-beta (#3466) 2022-03-08 09:54:59 -08:00
Zach Pomerantz
aefbb3d812 feat: price impact warning (#3437)
* chore: mv usdc price impact to hook

* fix: popover padding

* feat: warn on high price impact from toolbar

* fix: display price impact on warning too

* chore: rename useUSDCValue params

* fix: conform uses of price impact color
2022-03-08 09:53:40 -08:00
Moody Salem
c3f12398cd fix: reverts the defaulting behavior of 1 eth to usdc (#3465) 2022-03-08 11:31:52 -05:00
Zach Pomerantz
2272f2a01a chore: specify a module for CRAv4 (#3462) 2022-03-08 08:19:56 -08:00
Zach Pomerantz
fb71078ea2 fix: fully specify locales (#3461) 2022-03-08 08:19:31 -08:00
Zach Pomerantz
1c7c93191e fix: default input to native currency (#3456)
* fix: default to native

* chore: fix import
2022-03-08 08:19:23 -08:00
Zach Pomerantz
0713f730b3 fix: widget sizing (#3455) 2022-03-08 08:19:12 -08:00
Crowdin Bot
5f7a18b411 chore(i18n): synchronize translations from crowdin [skip ci] 2022-03-08 16:07:25 +00:00
Curly Brackets
020c8d181a fix(ui): display wrapped currency symbol with current chain (#3441) 2022-03-08 09:06:31 -06:00
Zach Pomerantz
ab3f024031 fix: display loaded input amount (#3451) 2022-03-08 08:57:51 -06:00
Ian Lapham
d989c61de5 fix: update type on wrapped native currency (#3454)
* update wrapped native currency type

* fix build error on base check

* update type on mapping to permit undefined values

* undo unneeded changes

* update filter check
2022-03-07 21:04:21 -05:00
Zach Pomerantz
5dd8059734 fix: routing tooltip styles (#3459) 2022-03-07 15:08:01 -08:00
Zach Pomerantz
b50e5511ea chore: update loading colors (#3438)
* chore: update loading colors

* fix: loading opacity of 0.6
2022-03-07 09:40:15 -08:00
Crowdin Bot
1efe5e9cd5 chore(i18n): synchronize translations from crowdin [skip ci] 2022-03-06 15:06:35 +00:00
Zach Pomerantz
2944dc4d0b chore: bump to v0.0.21-beta (#3439) 2022-03-04 14:30:25 -08:00
Jordan Frankfurt
29ae755f2a add new token image to widget (#3433) 2022-03-04 11:14:31 -06:00
Zach Pomerantz
27b831b301 chore: convert inline spinner to svg (#3432)
* chore: convert inline spinner to svg

* feat: inline spinner outline
2022-03-04 08:59:09 -08:00
Ian Lapham
6d9d38819e feat: update loading icon (#3428)
* update loading icon

* color update, update file for spinner

* update loader styling size

* update file naming for spinner
2022-03-04 11:07:02 -05:00
Zach Pomerantz
2de29129ed feat: export types (#3411)
* feat: export used types

* fix: clean defaulting names

* chore: rename to useSyncTokenDefaults
2022-03-03 14:21:03 -08:00
Crowdin Bot
52af0e506b chore(i18n): synchronize translations from crowdin [skip ci] 2022-03-03 22:07:03 +00:00
Noah Zinsmeister
4d69c946bf fix: Improve Vote page (#3429)
* introduce useQuorum

introduce LATEST_GOVERNOR_INDEX

* use CurrencyAmounts and fix % logic

* gate useQuorum to mainnet, just to be safe

* comment
2022-03-03 16:27:59 -05:00
Zach Pomerantz
542bf0bf66 feat: user select (#3410)
* feat: make data user-selectable

* fix: consider the whole node for focus

* fix: back out lineheight typing

* fix: straggling occurences

* chore: comment on root user-select
2022-03-03 11:09:12 -08:00
Zach Pomerantz
a4fbfae4ba fix: set appropriate types to nowrap (#3392) 2022-03-03 10:39:44 -08:00
Crowdin Bot
b2288258f2 chore(i18n): synchronize translations from crowdin [skip ci] 2022-03-03 18:11:42 +00:00
Ian Lapham
8703013b2d feat: use permit when valid on approvals (#3354)
* use permit when valid on approvals

* fix broken check for permit sig

* update conditionals

* update text

* remove unneeded else

* move permit and approve logic to combined hook

* update comment

* split txn and approval state, code clean

* organize disable conditions

* small changes

* update conditional check
2022-03-03 12:28:36 -05:00
Crowdin Bot
4f6173675d chore(i18n): synchronize translations from crowdin [skip ci] 2022-03-03 09:11:25 +00:00
Crowdin Bot
2469eb58b9 chore(i18n): synchronize translations from crowdin [skip ci] 2022-03-03 07:14:57 +00:00
Crowdin Bot
e0a8ac2408 chore(i18n): synchronize translations from crowdin [skip ci] 2022-03-02 23:26:37 +00:00
Ian Lapham
0a736b5e62 feat: create donation banner (#3403)
* create donation banner

* update styling and copy

* only show banner on swap page

* dont use hard coded z index

* style updates
2022-03-02 17:48:12 -05:00
Jordan Frankfurt
b44eb8877c chore(widgets): export theme stuff from widget (#3409) 2022-03-02 14:20:55 -08:00
Zach Pomerantz
92e61fa34b fix: token balance reloading (#3408)
* fix: layout token balance on token select

* fix: do not clear block on window re-focus
2022-03-02 13:39:18 -08:00
Zach Pomerantz
ef62fd33b2 fix: error dialog transition (#3405) 2022-03-02 13:37:57 -08:00
Jordan Frankfurt
96a42f66d4 fix(widgets): disable interactions and don't show summary screen on unsupported networks (#3376)
* disable interactions and don't show summary screen on unsupported networks

* clear input values on unsupported network

* pr feedback

* pr feedback

* simpler active trade clearing

* rename some things
2022-03-02 13:45:31 -06:00
Crowdin Bot
c446f20d2f chore(i18n): synchronize translations from crowdin [skip ci] 2022-03-02 19:11:16 +00:00
Jordan Frankfurt
5a1ef8fb7d feat(widgets): support wrapping native assets (#3301)
* feat(widgets): support wrapping native assets

* integrate wrap with swapInfo, start a useWrapCallback hook

* add loading state

* add pending state to (un)wrap transactions

* final cleanup

* janky merge conflict fix--disregard! this will change

* fixed

* 💢

* pr feedback

* z's pr feedback

* pr feedback

* zzmp pr feedback

* zzmp pr feedback
2022-03-02 12:36:35 -06:00
Zach Pomerantz
2863971640 fix: routing tooltip sizing (#3391) 2022-03-02 08:54:15 -08:00
Crowdin Bot
dcaf10ec29 chore(i18n): synchronize translations from crowdin [skip ci] 2022-03-02 07:31:50 +00:00
Zach Pomerantz
bca5113569 fix: connect wallet button (#3324)
* fix: make dialog provider an isolate

* fix: use button for connect wallet

* chore: document esoteric changes
2022-03-01 14:32:54 -08:00
David Walsh
6779c1a024 Fix #3117 - Open networks menu upon hover (#3378) 2022-03-01 14:45:22 -05:00
Ori Pomerantz
f79ef12494 fix(icons): Updated one of the Optimism images (#3357) 2022-03-01 14:37:18 -05:00
dependabot[bot]
7bcda46934 chore(deps): bump url-parse from 1.5.3 to 1.5.10 (#3371)
Bumps [url-parse](https://github.com/unshiftio/url-parse) from 1.5.3 to 1.5.10.
- [Release notes](https://github.com/unshiftio/url-parse/releases)
- [Commits](https://github.com/unshiftio/url-parse/compare/1.5.3...1.5.10)

---
updated-dependencies:
- dependency-name: url-parse
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-03-01 14:35:54 -05:00
dependabot[bot]
f4ba24cfd5 chore(deps): bump nanoid from 3.1.28 to 3.2.0 (#3173)
Bumps [nanoid](https://github.com/ai/nanoid) from 3.1.28 to 3.2.0.
- [Release notes](https://github.com/ai/nanoid/releases)
- [Changelog](https://github.com/ai/nanoid/blob/main/CHANGELOG.md)
- [Commits](https://github.com/ai/nanoid/compare/3.1.28...3.2.0)

---
updated-dependencies:
- dependency-name: nanoid
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-03-01 14:35:01 -05:00
Zach Pomerantz
59c6ab16dd fix: exports (#3379)
* fix: cjs in cra

* chore: wrap cjs for mjs

* chore: export cjs and esm

* chore: export chunks

* fix: correctly export locales

* chore: note exports compat

* fix: use cjs for common deps

* chore: bump to v0.0.20-beta

* chore: rm outdated comment
2022-03-01 11:32:55 -08:00
Ian Lapham
db17dcbf2c list update (#3389) 2022-03-01 14:05:13 -05:00
Moody Salem
1835de7f5f fix: the vote page could not render proposals without signature data
fixes https://github.com/Uniswap/interface/issues/3380
2022-02-28 18:16:24 -05:00
Crowdin Bot
00f158209c chore(i18n): synchronize translations from crowdin [skip ci] 2022-02-28 19:11:43 +00:00
Zach Pomerantz
2108ceedd5 chore: reduce token list block period (#3353) 2022-02-28 10:47:36 -08:00
Zach Pomerantz
ad080470da fix: use explicit value in Plural tag (#3363)
one is non-functional (I don't know why), but _1 seems to work 🤷
2022-02-28 10:41:15 -08:00
Moody Salem
fc34912b53 feat(swap): default 1 native to usdc on the swap page (#3347)
* feat(swap): default 1 eth to usdc on the swap page

* fix unit tests

* fix tests

* fix the issue better

* use the token list logo

* fix integration tests for swap and add one for eth/usdc

* address comments
2022-02-28 13:30:37 -05:00
jaclyn
c25d2b894c fix: update walletlink-connector to 6.2.13 (#3315)
* fix: update walletlink-connector to 6.2.12

* fix: update walletlink-connector to 6.2.13

Co-authored-by: Jaclyn Chan <jaclyn.chan@coinbase.com>
2022-02-28 11:13:36 -05:00
Moody Salem
83c99b8c04 chore: remove unused exports and unused integration test build env variable 2022-02-28 11:03:05 -05:00
Zach Pomerantz
ccdf1e7575 fix: convert to commonjs (#3373)
* chore: use fully specified path for non-main imports

* chore: convert to commonjs

- Pin @web3-react to exact cjs versions

* chore: bump to v0.0.18-beta

* chore: fix fonts.css comment

* fix: @ethersproject/providers version skew

* fix: @web3-react state typings
2022-02-28 07:52:22 -08:00
Crowdin Bot
c9faafee5e chore(i18n): synchronize translations from crowdin [skip ci] 2022-02-25 08:08:05 +00:00
Zach Pomerantz
26a44fb51b chore: bump to v0.0.15-beta (#3358) 2022-02-24 11:36:00 -08:00
Zach Pomerantz
1e16ac8449 fix: wrap Eip1193Bridge to fix outstanding bugs (#3355)
* fix: wrap Eip1193Bridge to fix outstanding bugs

* revert: widget height change
2022-02-24 11:20:09 -08:00
212 changed files with 7066 additions and 4265 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"

View File

@@ -38,10 +38,10 @@ jobs:
run: yarn install --frozen-lockfile
- run: yarn cypress install
- run: yarn build
env:
CI: false
REACT_APP_NETWORK_URL: 'https://mainnet.infura.io/v3/4bf032f2d38a4ed6bb975b80d6340847'
REACT_APP_SERVICE_WORKER: false
- run: yarn test:e2e

1
.gitignore vendored
View File

@@ -19,6 +19,7 @@
# builds
/build
/cosmos-export
/dist
/dts

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

@@ -7,4 +7,4 @@
"overridePath": "cosmos.override.cjs"
},
"port": 5001
}
}

View File

@@ -2,16 +2,27 @@ describe('Swap', () => {
beforeEach(() => {
cy.visit('/swap')
})
it('starts with ETH selected by default', () => {
cy.get('#swap-currency-input .token-amount-input').should('have.value', '')
cy.get('#swap-currency-input .token-symbol-container').should('contain.text', 'ETH')
cy.get('#swap-currency-output .token-amount-input').should('not.have.value')
cy.get('#swap-currency-output .token-symbol-container').should('contain.text', 'Select a token')
})
it('can enter an amount into input', () => {
cy.get('#swap-currency-input .token-amount-input').type('0.001', { delay: 200 }).should('have.value', '0.001')
cy.get('#swap-currency-input .token-amount-input')
.clear()
.type('0.001', { delay: 200 })
.should('have.value', '0.001')
})
it('zero swap amount', () => {
cy.get('#swap-currency-input .token-amount-input').type('0.0', { delay: 200 }).should('have.value', '0.0')
cy.get('#swap-currency-input .token-amount-input').clear().type('0.0', { delay: 200 }).should('have.value', '0.0')
})
it('invalid swap amount', () => {
cy.get('#swap-currency-input .token-amount-input').type('\\', { delay: 200 }).should('have.value', '')
cy.get('#swap-currency-input .token-amount-input').clear().type('\\', { delay: 200 }).should('have.value', '')
})
it('can enter an amount into output', () => {

View File

@@ -1,38 +1,45 @@
{
"name": "@uniswap/widgets",
"version": "0.0.14-beta",
"version": "1.0.6",
"description": "Uniswap Interface",
"homepage": ".",
"type": "module",
"files": [
"lib",
"dist"
],
"type": "module",
"types": "dist/index.d.ts",
"main": "dist/cjs/index.cjs",
"module": "dist/index.js",
"exports": {
".": {
"types": "./dist/widgets.d.ts",
"import": "./dist/widgets.esm.js",
"require": "./dist/widgets.js"
"types": "./dist/index.d.ts",
"import": "./dist/index.js",
"require": "./dist/cjs/index.cjs"
},
"./locales/*": {
"import": "./dist/locales/*.js",
"require": "./dist/cjs/locales/*.cjs"
},
"./fonts.css": {
"import": "./dist/fonts.css",
"require": "./dist/fonts.css"
}
},
"types": "./dist/widgets.d.ts",
"module": "./dist/widgets.esm.js",
"main": "./dist/widgets.js",
"devDependencies": {
"@babel/plugin-transform-runtime": "^7.17.0",
"@babel/preset-env": "^7.16.11",
"@babel/preset-react": "^7.16.7",
"@babel/preset-typescript": "^7.16.7",
"@ethersproject/experimental": "^5.4.0",
"@gnosis.pm/safe-apps-web3-react": "^0.6.0",
"@graphql-codegen/cli": "1.21.5",
"@graphql-codegen/typescript": "1.22.3",
"@graphql-codegen/typescript-operations": "^1.18.2",
"@graphql-codegen/typescript-rtk-query": "^1.1.1",
"@lingui/cli": "^3.9.0",
"@lingui/core": "^3.9.0",
"@lingui/macro": "^3.9.0",
"@lingui/react": "^3.9.0",
"@metamask/jazzicon": "^2.0.0",
"@reach/dialog": "^0.10.3",
"@reach/portal": "^0.10.3",
@@ -83,8 +90,8 @@
"@uniswap/v2-periphery": "^1.1.0-beta.0",
"@uniswap/v3-core": "1.0.0",
"@uniswap/v3-periphery": "^1.1.1",
"@web3-react/metamask": "8.0.16-alpha.0",
"@web3-react/walletconnect": "8.0.16-alpha.0",
"@web3-react/metamask": "^8.0.19-beta.0",
"@web3-react/walletconnect": "^8.0.26-beta.0",
"array.prototype.flat": "^1.2.4",
"array.prototype.flatmap": "^1.2.4",
"babel-plugin-macros": "^3.1.0",
@@ -112,7 +119,7 @@
"react-confetti": "^6.0.0",
"react-cosmos": "^5.6.6",
"react-dom": "^17.0.1",
"react-ga": "^2.5.7",
"react-ga4": "^1.4.1",
"react-is": "^17.0.2",
"react-markdown": "^4.3.1",
"react-redux": "^7.2.2",
@@ -126,6 +133,7 @@
"rollup-plugin-copy": "^3.4.0",
"rollup-plugin-delete": "^2.0.0",
"rollup-plugin-dts": "^4.1.0",
"rollup-plugin-multi-input": "^1.3.1",
"rollup-plugin-node-externals": "^3.1.2",
"rollup-plugin-scss": "^3.0.0",
"rollup-plugin-typescript2": "^0.31.1",
@@ -145,7 +153,7 @@
"web3-react-portis-connector": "npm:@web3-react/portis-connector@^6.0.9",
"web3-react-types": "npm:@web3-react/types@^6.0.7",
"web3-react-walletconnect-connector": "npm:@web3-react/walletconnect-connector@^7.0.2-alpha.0",
"web3-react-walletlink-connector": "npm:@web3-react/walletlink-connector@^6.2.11",
"web3-react-walletlink-connector": "npm:@web3-react/walletlink-connector@^6.2.13",
"workbox-core": "^6.1.0",
"workbox-precaching": "^6.1.0",
"workbox-routing": "^6.1.0"
@@ -186,42 +194,25 @@
"license": "GPL-3.0-or-later",
"dependencies": {
"@babel/runtime": "^7.17.0",
"@ethersproject/abi": "^5.4.1",
"@ethersproject/abstract-provider": "^5.4.1",
"@ethersproject/abstract-signer": "^5.4.1",
"@ethersproject/address": "^5.4.0",
"@ethersproject/bignumber": "^5.4.2",
"@ethersproject/bytes": "^5.4.0",
"@ethersproject/constants": "^5.4.0",
"@ethersproject/contracts": "^5.4.1",
"@ethersproject/experimental": "^5.4.0",
"@ethersproject/hash": "^5.4.0",
"@ethersproject/providers": "5.4.0",
"@ethersproject/solidity": "^5.4.0",
"@ethersproject/strings": "^5.4.0",
"@ethersproject/units": "^5.4.0",
"@ethersproject/wallet": "^5.4.0",
"@fontsource/ibm-plex-mono": "^4.5.1",
"@fontsource/inter": "^4.5.1",
"@lingui/core": "^3.9.0",
"@lingui/macro": "^3.9.0",
"@lingui/react": "^3.9.0",
"@popperjs/core": "^2.4.4",
"@reduxjs/toolkit": "^1.6.1",
"@uniswap/redux-multicall": "^1.0.0",
"@uniswap/redux-multicall": "^1.1.1",
"@uniswap/router-sdk": "^1.0.3",
"@uniswap/sdk-core": "^3.0.1",
"@uniswap/smart-order-router": "^2.5.10",
"@uniswap/smart-order-router": "^2.5.26",
"@uniswap/token-lists": "^1.0.0-beta.27",
"@uniswap/v2-sdk": "^3.0.1",
"@uniswap/v3-sdk": "^3.8.2",
"@web3-react/core": "8.0.16-alpha.0",
"@web3-react/eip1193": "8.0.16-alpha.0",
"@web3-react/empty": "8.0.17-alpha.0",
"@web3-react/types": "8.0.16-alpha.0",
"@web3-react/url": "8.0.17-alpha.0",
"@web3-react/core": "^8.0.23-beta.0",
"@web3-react/eip1193": "^8.0.18-beta.0",
"@web3-react/empty": "^8.0.12-beta.0",
"@web3-react/types": "^8.0.12-beta.0",
"@web3-react/url": "^8.0.17-beta.0",
"ajv": "^6.12.3",
"cids": "^1.0.0",
"ethers": "^5.1.4",
"immer": "^9.0.6",
"jotai": "^1.3.7",
"jsbi": "^3.1.4",

View File

@@ -17,6 +17,8 @@ import { RollupWarning } from 'rollup'
import copy from 'rollup-plugin-copy'
import del from 'rollup-plugin-delete'
import dts from 'rollup-plugin-dts'
// @ts-ignore // missing types
import multi from 'rollup-plugin-multi-input'
import externals from 'rollup-plugin-node-externals'
import sass from 'rollup-plugin-scss'
import { CompilerOptions } from 'typescript'
@@ -24,6 +26,8 @@ import { CompilerOptions } from 'typescript'
const REPLACEMENTS = {
'process.env.REACT_APP_IS_WIDGET': true,
'process.env.REACT_APP_LOCALES': '"./locales"',
// esm requires fully-specified paths:
'react/jsx-runtime': 'react/jsx-runtime.js',
}
const EXTENSIONS = ['.js', '.jsx', '.ts', '.tsx']
@@ -33,6 +37,11 @@ function isAsset(source: string) {
return extname && [...ASSET_EXTENSIONS, '.css', '.scss'].includes(extname)
}
function isEthers(source: string) {
// @ethersproject/* modules are provided by ethers, with the exception of experimental.
return source.startsWith('@ethersproject/') && !source.endsWith('experimental')
}
const TS_CONFIG = './tsconfig.lib.json'
// eslint-disable-next-line @typescript-eslint/no-var-requires
const { baseUrl, paths }: CompilerOptions = require(TS_CONFIG).compilerOptions
@@ -45,54 +54,86 @@ const aliases = Object.entries({ ...paths }).flatMap(([find, replacements]) => {
const plugins = [
// Dependency resolution
externals({ exclude: ['constants'], deps: true, peerDeps: true }), // marks builtins, dependencies, and peerDependencies external
resolve({ extensions: EXTENSIONS }), // resolves third-party modules within node_modules/
alias({ entries: aliases }), // resolves paths aliased through the tsconfig (babel does not use tsconfig path resolution)
// Source code transformation
replace({ ...REPLACEMENTS, preventAssignment: true }),
json(), // imports json; doing so type-checking allows the json to be type-checked
json(), // imports json as ES6; doing so enables type-checking and module resolution
]
const check = {
input: 'src/lib/index.tsx',
output: { file: 'dist/widgets.tsc' },
external: isAsset,
plugins: [...plugins, typescript({ tsconfig: TS_CONFIG })],
output: { file: 'dist/widgets.tsc', inlineDynamicImports: true },
external: (source: string) => isAsset(source) || isEthers(source),
plugins: [
externals({ exclude: ['constants'], deps: true, peerDeps: true }), // marks builtins, dependencies, and peerDependencies external
...plugins,
typescript({ tsconfig: TS_CONFIG }),
],
onwarn: squelchTranspilationWarnings, // this pipeline is only for typechecking and generating definitions
}
const type = {
input: 'dist/dts/lib/index.d.ts',
output: { file: 'dist/widgets.d.ts' },
external: isAsset,
output: { file: 'dist/index.d.ts' },
external: (source: string) => isAsset(source) || isEthers(source),
plugins: [
externals({ exclude: ['constants'], deps: true, peerDeps: true }),
dts({ compilerOptions: { baseUrl: 'dist/dts' } }),
process.env.ROLLUP_WATCH ? undefined : del({ hook: 'buildEnd', targets: ['dist/widgets.tsc', 'dist/dts'] }),
],
}
/**
* This exports scheme works for nextjs and for CRA5.
*
* It will also work for CRA4 if you use direct imports:
* instead of `import { SwapWidget } from '@uniswap/widgets'`,
* `import { SwapWidget } from '@uniswap/widgets/dist/index.js'`.
* I do not know why CRA4 does not seem to use exports for resolution.
*
* Note that chunks are enabled. This is so the tokenlist spec can be loaded async,
* to improve first load time (due to ajv). Locales are also in separate chunks.
*
* Lastly, note that JSON and lingui are bundled into the library, as neither are fully
* supported/compatible with ES Modules. Both _could_ be bundled only with esm, but this
* yields a less complex pipeline.
*/
const transpile = {
input: 'src/lib/index.tsx',
output: [
{
file: 'dist/widgets.js',
format: 'cjs',
sourcemap: true,
dir: 'dist',
format: 'esm',
sourcemap: false,
},
{
file: 'dist/widgets.esm.js',
format: 'esm',
sourcemap: true,
dir: 'dist/cjs',
entryFileNames: '[name].cjs',
chunkFileNames: '[name]-[hash].cjs',
format: 'cjs',
sourcemap: false,
},
],
external: isEthers,
plugins: [
externals({
exclude: [
'constants',
/@lingui\/(core|react)/, // @lingui incorrectly exports esm, so it must be bundled in
/\.json$/, // esm does not support JSON loading, so it must be bundled in
],
deps: true,
peerDeps: true,
}),
...plugins,
// Source code transformation
url({ include: ASSET_EXTENSIONS.map((extname) => '**/*' + extname) }), // imports assets as data URIs
url({ include: ASSET_EXTENSIONS.map((extname) => '**/*' + extname), limit: Infinity }), // imports assets as data URIs
svgr({ exportType: 'named', svgo: false }), // imports svgs as React components
sass({ output: 'dist/fonts.css' }), // generates widgets.css
sass({ output: 'dist/fonts.css' }), // generates fonts.css
commonjs(), // transforms cjs dependencies into tree-shakeable ES modules
babel({
@@ -104,16 +145,30 @@ const transpile = {
'@babel/plugin-transform-runtime', // embeds the babel runtime for library distribution
],
}),
copy({
copyOnce: true,
targets: [{ src: 'src/locales/*.js', dest: 'dist/locales' }],
}),
],
onwarn: squelchTypeWarnings, // this pipeline is only for transpilation
}
const config = [check, type, transpile]
const locales = {
input: 'src/locales/*.js',
output: [
{
dir: 'dist',
format: 'esm',
sourcemap: false,
},
],
plugins: [
copy({
copyOnce: true,
targets: [{ src: 'src/locales/*.js', dest: 'dist/cjs/locales', rename: (name) => `${name}.cjs` }],
}),
commonjs(),
multi(),
],
}
const config = [check, type, transpile, locales]
export default config
function squelchTranspilationWarnings(warning: RollupWarning, warn: (warning: RollupWarning) => void) {

Binary file not shown.

After

Width:  |  Height:  |  Size: 121 KiB

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,3 +1,3 @@
<svg width="100%" height="35" viewBox="800 0 300 200" xmlns="http://www.w3.org/2000/svg">
<line x1="0" x2="2000" y1="100" y2="100" stroke="currentColor" stroke-width="20" stroke-linecap="round" stroke-dasharray="1, 45"/>
<svg width="100%" height="35" viewBox="850 0 300 200" xmlns="http://www.w3.org/2000/svg">
<line x1="0" x2="3000" y1="100" y2="100" stroke="currentColor" stroke-width="20" stroke-linecap="round" stroke-dasharray="1, 45"/>
</svg>

Before

Width:  |  Height:  |  Size: 233 B

After

Width:  |  Height:  |  Size: 233 B

View File

@@ -1,16 +1,5 @@
<svg width="170" height="168" viewBox="0 0 170 168" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0)">
<path opacity="0.6" d="M85.05 168C132.022 168 170.1 130.105 170.1 83.3593C170.1 36.6135 0 36.6135 0 83.3593C0 130.105 38.0782 168 85.05 168Z" fill="#FF505F"/>
<path opacity="0.6" d="M85.05 168C132.022 168 170.1 130.105 170.1 83.3593C170.1 36.6135 0 36.6135 0 83.3593C0 130.105 38.0782 168 85.05 168Z" fill="#FF0320"/>
<path d="M85.05 0C132.022 0 170.1 37.8949 170.1 84.6407C170.1 131.386 0 131.386 0 84.6407C0 37.8949 38.0782 0 85.05 0Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M144.665 64.0394L112.444 12.3742L89.0263 78.9477L144.665 64.0394Z" fill="#FF4E65"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M143.777 64.215L112.444 12.3742L165.349 58.4347L143.777 64.215Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M144.551 63.613L142.479 124.467L88.912 78.5213L144.551 63.613Z" fill="#D0001A"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M143.663 63.7886L142.479 124.467L165.235 58.0083L143.663 63.7886Z" fill="#FF697B"/>
</g>
<defs>
<clipPath id="clip0">
<rect width="170" height="168" fill="white"/>
</clipPath>
</defs>
<svg width="500" height="500" viewBox="0 0 500 500" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="250" cy="250" r="250" fill="#FF0420"/>
<path d="M177.133 316.446C162.247 316.446 150.051 312.943 140.544 305.938C131.162 298.808 126.471 288.676 126.471 275.541C126.471 272.789 126.784 269.411 127.409 265.408C129.036 256.402 131.35 245.581 134.352 232.947C142.858 198.547 164.812 181.347 200.213 181.347C209.845 181.347 218.476 182.973 226.107 186.225C233.738 189.352 239.742 194.106 244.12 200.486C248.498 206.74 250.688 214.246 250.688 223.002C250.688 225.629 250.375 228.944 249.749 232.947C247.873 244.08 245.621 254.901 242.994 265.408C238.616 282.546 231.048 295.368 220.29 303.874C209.532 312.255 195.147 316.446 177.133 316.446ZM179.76 289.426C186.766 289.426 192.707 287.362 197.586 283.234C202.59 279.106 206.155 272.789 208.281 264.283C211.158 252.524 213.348 242.266 214.849 233.51C215.349 230.883 215.599 228.194 215.599 225.441C215.599 214.058 209.657 208.366 197.774 208.366C190.768 208.366 184.764 210.43 179.76 214.558C174.882 218.687 171.379 225.004 169.253 233.51C167.001 241.891 164.749 252.149 162.498 264.283C161.997 266.784 161.747 269.411 161.747 272.163C161.747 283.672 167.752 289.426 179.76 289.426Z" fill="white"/>
<path d="M259.303 314.57C257.927 314.57 256.863 314.132 256.113 313.256C255.487 312.255 255.3 311.13 255.55 309.879L281.444 187.914C281.694 186.538 282.382 185.412 283.508 184.536C284.634 183.661 285.822 183.223 287.073 183.223H336.985C350.87 183.223 362.003 186.1 370.384 191.854C378.891 197.609 383.144 205.927 383.144 216.81C383.144 219.937 382.769 223.19 382.018 226.567C378.891 240.953 372.574 251.586 363.067 258.466C353.685 265.346 340.8 268.786 324.413 268.786H299.082L290.451 309.879C290.2 311.255 289.512 312.38 288.387 313.256C287.261 314.132 286.072 314.57 284.822 314.57H259.303ZM325.727 242.892C330.98 242.892 335.546 241.453 339.424 238.576C343.427 235.699 346.054 231.571 347.305 226.192C347.68 224.065 347.868 222.189 347.868 220.563C347.868 216.935 346.805 214.183 344.678 212.307C342.551 210.305 338.924 209.305 333.795 209.305H311.278L304.148 242.892H325.727Z" fill="white"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

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

View File

@@ -1,6 +1,6 @@
import { Trans } from '@lingui/macro'
import React, { ErrorInfo } from 'react'
import ReactGA from 'react-ga'
import ReactGA from 'react-ga4'
import styled from 'styled-components/macro'
import store, { AppState } from '../../state'
@@ -49,6 +49,13 @@ type ErrorBoundaryState = {
const IS_UNISWAP = window.location.hostname === 'app.uniswap.org'
async function updateServiceWorker(): Promise<ServiceWorkerRegistration> {
const ready = await navigator.serviceWorker.ready
// the return type of update is incorrectly typed as Promise<void>. See
// https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerRegistration/update
return ready.update() as unknown as Promise<ServiceWorkerRegistration>
}
export default class ErrorBoundary extends React.Component<unknown, ErrorBoundaryState> {
constructor(props: unknown) {
super(props)
@@ -56,15 +63,29 @@ export default class ErrorBoundary extends React.Component<unknown, ErrorBoundar
}
static getDerivedStateFromError(error: Error): ErrorBoundaryState {
updateServiceWorker()
.then(async (registration) => {
// We want to refresh only if we detect a new service worker is waiting to be activated.
// See details about it: https://developers.google.com/web/fundamentals/primers/service-workers/lifecycle
if (registration?.waiting) {
await registration.unregister()
// Makes Workbox call skipWaiting(). For more info on skipWaiting see: https://developer.chrome.com/docs/workbox/handling-service-worker-updates/
registration.waiting.postMessage({ type: 'SKIP_WAITING' })
// Once the service worker is unregistered, we can reload the page to let
// the browser download a fresh copy of our app (invalidating the cache)
window.location.reload()
}
})
.catch((error) => {
console.error('Failed to update service worker', error)
})
return { error }
}
componentDidCatch(error: Error, errorInfo: ErrorInfo) {
ReactGA.exception({
...error,
...errorInfo,
fatal: true,
})
ReactGA.event('exception', { description: error.toString() + errorInfo.toString(), fatal: true })
}
render() {

View File

@@ -11,7 +11,7 @@ import { PoolState, usePools } from 'hooks/usePools'
import usePrevious from 'hooks/usePrevious'
import { DynamicSection } from 'pages/AddLiquidity/styled'
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import ReactGA from 'react-ga'
import ReactGA from 'react-ga4'
import { Box } from 'rebass'
import styled, { keyframes } from 'styled-components/macro'
import { ThemedText } from 'theme'

View File

@@ -51,6 +51,16 @@ const FlyoutHeader = styled.div`
font-weight: 400;
`
const FlyoutMenu = styled.div`
position: absolute;
top: 54px;
width: 272px;
z-index: 99;
padding-top: 10px;
@media screen and (min-width: ${MEDIA_WIDTHS.upToSmall}px) {
top: 40px;
}
`
const FlyoutMenuContents = styled.div`
align-items: flex-start;
background-color: ${({ theme }) => theme.bg0};
box-shadow: 0px 0px 1px rgba(0, 0, 0, 0.01), 0px 4px 8px rgba(0, 0, 0, 0.04), 0px 16px 24px rgba(0, 0, 0, 0.04),
@@ -61,16 +71,9 @@ const FlyoutMenu = styled.div`
font-size: 16px;
overflow: auto;
padding: 16px;
position: absolute;
top: 64px;
width: 272px;
z-index: 99;
& > *:not(:last-child) {
margin-bottom: 12px;
}
@media screen and (min-width: ${MEDIA_WIDTHS.upToSmall}px) {
top: 50px;
}
`
const FlyoutRow = styled.div<{ active: boolean }>`
align-items: center;
@@ -252,8 +255,8 @@ export default function NetworkSelector() {
const handleChainSwitch = useCallback(
(targetChain: number, skipToggle?: boolean) => {
if (!library) return
switchToNetwork({ library, chainId: targetChain })
if (!library?.provider) return
switchToNetwork({ provider: library.provider, chainId: targetChain })
.then(() => {
if (!skipToggle) {
toggle()
@@ -305,21 +308,23 @@ export default function NetworkSelector() {
}
return (
<SelectorWrapper ref={node as any}>
<SelectorControls onClick={toggle} interactive>
<SelectorWrapper ref={node as any} onMouseEnter={toggle} onMouseLeave={toggle}>
<SelectorControls interactive>
<SelectorLogo interactive src={info.logoUrl} />
<SelectorLabel>{info.label}</SelectorLabel>
<StyledChevronDown />
</SelectorControls>
{open && (
<FlyoutMenu onMouseLeave={toggle}>
<FlyoutHeader>
<Trans>Select a network</Trans>
</FlyoutHeader>
<Row onSelectChain={handleChainSwitch} targetChain={SupportedChainId.MAINNET} />
<Row onSelectChain={handleChainSwitch} targetChain={SupportedChainId.POLYGON} />
<Row onSelectChain={handleChainSwitch} targetChain={SupportedChainId.OPTIMISM} />
<Row onSelectChain={handleChainSwitch} targetChain={SupportedChainId.ARBITRUM_ONE} />
<FlyoutMenu>
<FlyoutMenuContents>
<FlyoutHeader>
<Trans>Select a network</Trans>
</FlyoutHeader>
<Row onSelectChain={handleChainSwitch} targetChain={SupportedChainId.MAINNET} />
<Row onSelectChain={handleChainSwitch} targetChain={SupportedChainId.POLYGON} />
<Row onSelectChain={handleChainSwitch} targetChain={SupportedChainId.OPTIMISM} />
<Row onSelectChain={handleChainSwitch} targetChain={SupportedChainId.ARBITRUM_ONE} />
</FlyoutMenuContents>
</FlyoutMenu>
)}
</SelectorWrapper>

View File

@@ -9,7 +9,7 @@ import useTheme from 'hooks/useTheme'
import { saturate } from 'polished'
import React, { ReactNode, useCallback, useMemo } from 'react'
import { BarChart2, CloudOff, Inbox } from 'react-feather'
import ReactGA from 'react-ga'
import ReactGA from 'react-ga4'
import { batch } from 'react-redux'
import { Bound } from 'state/mint/v3/actions'
import styled from 'styled-components/macro'
@@ -158,11 +158,7 @@ export default function LiquidityChartRangeInput({
)
if (isError) {
ReactGA.exception({
...error,
category: 'Liquidity',
fatal: false,
})
ReactGA.event('exception', { description: error.toString(), fatal: false })
}
return (

View File

@@ -3,7 +3,7 @@ import { CurrencyAmount, Token } from '@uniswap/sdk-core'
import useActiveWeb3React from 'hooks/useActiveWeb3React'
import { useCallback, useEffect } from 'react'
import { Heart, X } from 'react-feather'
import ReactGA from 'react-ga'
import ReactGA from 'react-ga4'
import styled, { keyframes } from 'styled-components/macro'
import tokenLogo from '../../assets/images/token-logo.png'

View File

@@ -4,7 +4,7 @@ import { RowFixed } from 'components/Row'
import useCurrentBlockTimestamp from 'hooks/useCurrentBlockTimestamp'
import { useEffect } from 'react'
import { MessageCircle, X } from 'react-feather'
import ReactGA from 'react-ga'
import ReactGA from 'react-ga4'
import { useShowSurveyPopup } from 'state/user/hooks'
import styled from 'styled-components/macro'
import { ExternalLink, ThemedText, Z_INDEX } from 'theme'

View File

@@ -14,11 +14,12 @@ const MobilePopupWrapper = styled.div<{ height: string | number }>`
max-width: 100%;
height: ${({ height }) => height};
margin: ${({ height }) => (height ? '0 auto;' : 0)};
margin-bottom: ${({ height }) => (height ? '20px' : 0)}};
margin-bottom: ${({ height }) => (height ? '20px' : 0)};
display: none;
${({ theme }) => theme.mediaWidth.upToSmall`
display: block;
padding-top: 20px;
`};
`

View File

@@ -19,7 +19,7 @@ import { PositionDetails } from 'types/position'
import { formatTickPrice } from 'utils/formatTickPrice'
import { unwrappedToken } from 'utils/unwrappedToken'
import { DAI, USDC, USDT, WBTC, WRAPPED_NATIVE_CURRENCY } from '../../constants/tokens'
import { DAI, USDC_MAINNET, USDT, WBTC, WRAPPED_NATIVE_CURRENCY } from '../../constants/tokens'
const LinkRow = styled(Link)`
align-items: center;
@@ -145,7 +145,7 @@ export function getPriceOrderingFromPositionForUI(position?: Position): {
const token1 = position.amount1.currency
// if token0 is a dollar-stable asset, set it as the quote token
const stables = [DAI, USDC, USDT]
const stables = [DAI, USDC_MAINNET, USDT]
if (stables.some((stable) => stable.equals(token0))) {
return {
priceLower: position.token0PriceUpper.invert(),
@@ -157,7 +157,7 @@ export function getPriceOrderingFromPositionForUI(position?: Position): {
// if token1 is an ETH-/BTC-stable asset, set it as the base token
const bases = [...Object.values(WRAPPED_NATIVE_CURRENCY), WBTC]
if (bases.some((base) => base.equals(token1))) {
if (bases.some((base) => base && base.equals(token1))) {
return {
priceLower: position.token0PriceUpper.invert(),
priceUpper: position.token0PriceLower.invert(),

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

@@ -1,7 +1,7 @@
import { Protocol } from '@uniswap/router-sdk'
import { Currency, Percent } from '@uniswap/sdk-core'
import { FeeAmount } from '@uniswap/v3-sdk'
import { DAI, USDC, WBTC } from 'constants/tokens'
import { DAI, USDC_MAINNET, WBTC } from 'constants/tokens'
import { render } from 'test-utils'
import RoutingDiagram, { RoutingDiagramEntry } from './RoutingDiagram'
@@ -10,16 +10,16 @@ const percent = (strings: TemplateStringsArray) => new Percent(parseInt(strings[
const singleRoute: RoutingDiagramEntry = {
percent: percent`100`,
path: [[USDC, DAI, FeeAmount.LOW]],
path: [[USDC_MAINNET, DAI, FeeAmount.LOW]],
protocol: Protocol.V3,
}
const multiRoute: RoutingDiagramEntry[] = [
{ percent: percent`75`, path: [[USDC, DAI, FeeAmount.LOWEST]], protocol: Protocol.V2 },
{ percent: percent`75`, path: [[USDC_MAINNET, DAI, FeeAmount.LOWEST]], protocol: Protocol.V2 },
{
percent: percent`25`,
path: [
[USDC, WBTC, FeeAmount.MEDIUM],
[USDC_MAINNET, WBTC, FeeAmount.MEDIUM],
[WBTC, DAI, FeeAmount.HIGH],
],
protocol: Protocol.V3,
@@ -47,16 +47,16 @@ jest.mock('hooks/useTokenInfoFromActiveList', () => ({
}))
it('renders when no routes are provided', () => {
const { asFragment } = render(<RoutingDiagram currencyIn={DAI} currencyOut={USDC} routes={[]} />)
const { asFragment } = render(<RoutingDiagram currencyIn={DAI} currencyOut={USDC_MAINNET} routes={[]} />)
expect(asFragment()).toMatchSnapshot()
})
it('renders single route', () => {
const { asFragment } = render(<RoutingDiagram currencyIn={USDC} currencyOut={DAI} routes={[singleRoute]} />)
const { asFragment } = render(<RoutingDiagram currencyIn={USDC_MAINNET} currencyOut={DAI} routes={[singleRoute]} />)
expect(asFragment()).toMatchSnapshot()
})
it('renders multi route', () => {
const { asFragment } = render(<RoutingDiagram currencyIn={USDC} currencyOut={DAI} routes={multiRoute} />)
const { asFragment } = render(<RoutingDiagram currencyIn={USDC_MAINNET} currencyOut={DAI} routes={multiRoute} />)
expect(asFragment()).toMatchSnapshot()
})

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

@@ -48,7 +48,7 @@ export function ImportToken(props: ImportProps) {
<RowBetween>
{onBack ? <ArrowLeft style={{ cursor: 'pointer' }} onClick={onBack} /> : <div />}
<ThemedText.MediumHeader>
<Plural value={tokens.length} one="Import token" other="Import tokens" />
<Plural value={tokens.length} _1="Import token" other="Import tokens" />
</ThemedText.MediumHeader>
{onDismiss ? <CloseIcon onClick={onDismiss} /> : <div />}
</RowBetween>

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

@@ -12,6 +12,8 @@ export const COMMON_CONTRACT_NAMES: Record<number, { [address: string]: string }
[TIMELOCK_ADDRESS[SupportedChainId.MAINNET]]: 'Timelock',
[GOVERNANCE_ALPHA_V0_ADDRESSES[SupportedChainId.MAINNET]]: 'Governance (V0)',
[GOVERNANCE_ALPHA_V1_ADDRESSES[SupportedChainId.MAINNET]]: 'Governance',
'0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e': 'ENS Registry',
'0x4976fb03C32e5B8cfe2b6cCB31c09Ba78EBaBa41': 'ENS Public Resolver',
},
}
@@ -21,3 +23,5 @@ export const DEFAULT_AVERAGE_BLOCK_TIME_IN_SECS = 13
export const AVERAGE_BLOCK_TIME_IN_SECS: { [chainId: number]: number } = {
1: DEFAULT_AVERAGE_BLOCK_TIME_IN_SECS,
}
export const LATEST_GOVERNOR_INDEX = 2

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

@@ -18,8 +18,8 @@ import {
sETH2,
SWISE,
TRIBE,
USDC,
USDC_ARBITRUM,
USDC_MAINNET,
USDC_OPTIMISM,
USDC_POLYGON,
USDT,
@@ -44,13 +44,21 @@ type ChainCurrencyList = {
}
const WRAPPED_NATIVE_CURRENCIES_ONLY: ChainTokenList = Object.fromEntries(
Object.entries(WRAPPED_NATIVE_CURRENCY).map(([key, value]) => [key, [value]])
Object.entries(WRAPPED_NATIVE_CURRENCY)
.map(([key, value]) => [key, [value]])
.filter(Boolean)
)
// used to construct intermediary pairs for trading
export const BASES_TO_CHECK_TRADES_AGAINST: ChainTokenList = {
...WRAPPED_NATIVE_CURRENCIES_ONLY,
[SupportedChainId.MAINNET]: [...WRAPPED_NATIVE_CURRENCIES_ONLY[SupportedChainId.MAINNET], DAI, USDC, USDT, WBTC],
[SupportedChainId.MAINNET]: [
...WRAPPED_NATIVE_CURRENCIES_ONLY[SupportedChainId.MAINNET],
DAI,
USDC_MAINNET,
USDT,
WBTC,
],
[SupportedChainId.OPTIMISM]: [
...WRAPPED_NATIVE_CURRENCIES_ONLY[SupportedChainId.OPTIMISM],
DAI_OPTIMISM,
@@ -90,7 +98,7 @@ export const ADDITIONAL_BASES: { [chainId: number]: { [tokenAddress: string]: To
*/
export const CUSTOM_BASES: { [chainId: number]: { [tokenAddress: string]: Token[] } } = {
[SupportedChainId.MAINNET]: {
[AMPL.address]: [DAI, WRAPPED_NATIVE_CURRENCY[SupportedChainId.MAINNET]],
[AMPL.address]: [DAI, WRAPPED_NATIVE_CURRENCY[SupportedChainId.MAINNET] as Token],
},
}
@@ -101,32 +109,38 @@ export const COMMON_BASES: ChainCurrencyList = {
[SupportedChainId.MAINNET]: [
nativeOnChain(SupportedChainId.MAINNET),
DAI,
USDC,
USDC_MAINNET,
USDT,
WBTC,
WRAPPED_NATIVE_CURRENCY[SupportedChainId.MAINNET],
WRAPPED_NATIVE_CURRENCY[SupportedChainId.MAINNET] as Token,
],
[SupportedChainId.ROPSTEN]: [
nativeOnChain(SupportedChainId.ROPSTEN),
WRAPPED_NATIVE_CURRENCY[SupportedChainId.ROPSTEN],
WRAPPED_NATIVE_CURRENCY[SupportedChainId.ROPSTEN] as Token,
],
[SupportedChainId.RINKEBY]: [
nativeOnChain(SupportedChainId.RINKEBY),
WRAPPED_NATIVE_CURRENCY[SupportedChainId.RINKEBY],
WRAPPED_NATIVE_CURRENCY[SupportedChainId.RINKEBY] as Token,
],
[SupportedChainId.GOERLI]: [
nativeOnChain(SupportedChainId.GOERLI),
WRAPPED_NATIVE_CURRENCY[SupportedChainId.GOERLI] as Token,
],
[SupportedChainId.KOVAN]: [
nativeOnChain(SupportedChainId.KOVAN),
WRAPPED_NATIVE_CURRENCY[SupportedChainId.KOVAN] as Token,
],
[SupportedChainId.GOERLI]: [nativeOnChain(SupportedChainId.GOERLI), WRAPPED_NATIVE_CURRENCY[SupportedChainId.GOERLI]],
[SupportedChainId.KOVAN]: [nativeOnChain(SupportedChainId.KOVAN), WRAPPED_NATIVE_CURRENCY[SupportedChainId.KOVAN]],
[SupportedChainId.ARBITRUM_ONE]: [
nativeOnChain(SupportedChainId.ARBITRUM_ONE),
DAI_ARBITRUM_ONE,
USDC_ARBITRUM,
USDT_ARBITRUM_ONE,
WBTC_ARBITRUM_ONE,
WRAPPED_NATIVE_CURRENCY[SupportedChainId.ARBITRUM_ONE],
WRAPPED_NATIVE_CURRENCY[SupportedChainId.ARBITRUM_ONE] as Token,
],
[SupportedChainId.ARBITRUM_RINKEBY]: [
nativeOnChain(SupportedChainId.ARBITRUM_RINKEBY),
WRAPPED_NATIVE_CURRENCY[SupportedChainId.ARBITRUM_RINKEBY],
WRAPPED_NATIVE_CURRENCY[SupportedChainId.ARBITRUM_RINKEBY] as Token,
],
[SupportedChainId.OPTIMISM]: [
nativeOnChain(SupportedChainId.OPTIMISM),
@@ -146,7 +160,7 @@ export const COMMON_BASES: ChainCurrencyList = {
],
[SupportedChainId.POLYGON_MUMBAI]: [
nativeOnChain(SupportedChainId.POLYGON_MUMBAI),
WRAPPED_NATIVE_CURRENCY[SupportedChainId.POLYGON_MUMBAI],
WRAPPED_NATIVE_CURRENCY[SupportedChainId.POLYGON_MUMBAI] as Token,
WETH_POLYGON_MUMBAI,
],
}
@@ -154,7 +168,13 @@ export const COMMON_BASES: ChainCurrencyList = {
// used to construct the list of all pairs we consider by default in the frontend
export const BASES_TO_TRACK_LIQUIDITY_FOR: ChainTokenList = {
...WRAPPED_NATIVE_CURRENCIES_ONLY,
[SupportedChainId.MAINNET]: [...WRAPPED_NATIVE_CURRENCIES_ONLY[SupportedChainId.MAINNET], DAI, USDC, USDT, WBTC],
[SupportedChainId.MAINNET]: [
...WRAPPED_NATIVE_CURRENCIES_ONLY[SupportedChainId.MAINNET],
DAI,
USDC_MAINNET,
USDT,
WBTC,
],
}
export const PINNED_PAIRS: { readonly [chainId: number]: [Token, Token][] } = {
[SupportedChainId.MAINNET]: [
@@ -168,7 +188,7 @@ export const PINNED_PAIRS: { readonly [chainId: number]: [Token, Token][] } = {
'Compound USD Coin'
),
],
[USDC, USDT],
[USDC_MAINNET, USDT],
[DAI, USDT],
],
}

View File

@@ -1101,6 +1101,20 @@
"name": "BMEX",
"symbol": "BMEX",
"decimals": 18
},
{
"chainId": 1,
"address": "0x322A46E88fa3C78F9c9E3DBb0254b61664a06109",
"name": "Ukraine DAO",
"symbol": "Ukraine",
"decimals": 18
},
{
"chainId": 1,
"address": "0x8290D7a64F25e6b5002d98367E8367c1b532b534",
"name": "oneUNI",
"symbol": "oneUNI",
"decimals": 18
}
]
}

View File

@@ -1,8 +1,87 @@
import { Currency, Ether, NativeCurrency, Token, WETH9 } from '@uniswap/sdk-core'
import invariant from 'tiny-invariant'
import { UNI_ADDRESS } from './addresses'
import { SupportedChainId } from './chains'
export const USDC_MAINNET = new Token(
SupportedChainId.MAINNET,
'0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48',
6,
'USDC',
'USD//C'
)
export const USDC_ROPSTEN = new Token(
SupportedChainId.ROPSTEN,
'0x07865c6e87b9f70255377e024ace6630c1eaa37f',
6,
'USDC',
'USD//C'
)
export const USDC_RINKEBY = new Token(
SupportedChainId.RINKEBY,
'0x4DBCdF9B62e891a7cec5A2568C3F4FAF9E8Abe2b',
6,
'tUSDC',
'test USD//C'
)
export const USDC_GOERLI = new Token(
SupportedChainId.GOERLI,
'0x07865c6e87b9f70255377e024ace6630c1eaa37f',
6,
'USDC',
'USD//C'
)
export const USDC_KOVAN = new Token(
SupportedChainId.KOVAN,
'0x31eeb2d0f9b6fd8642914ab10f4dd473677d80df',
6,
'USDC',
'USD//C'
)
export const USDC_OPTIMISM = new Token(
SupportedChainId.OPTIMISM,
'0x7F5c764cBc14f9669B88837ca1490cCa17c31607',
6,
'USDC',
'USD//C'
)
export const USDC_OPTIMISTIC_KOVAN = new Token(
SupportedChainId.OPTIMISTIC_KOVAN,
'0x3b8e53b3ab8e01fb57d0c9e893bc4d655aa67d84',
6,
'USDC',
'USD//C'
)
export const USDC_ARBITRUM = new Token(
SupportedChainId.ARBITRUM_ONE,
'0xFF970A61A04b1cA14834A43f5dE4533eBDDB5CC8',
6,
'USDC',
'USD//C'
)
export const USDC_ARBITRUM_RINKEBY = new Token(
SupportedChainId.ARBITRUM_RINKEBY,
'0x09b98f8b2395d076514037ff7d39a091a536206c',
6,
'USDC',
'USD//C'
)
export const USDC_POLYGON = new Token(
SupportedChainId.POLYGON,
'0x2791bca1f2de4661ed88a30c99a7a9449aa84174',
6,
'USDC',
'USD//C'
)
export const USDC_POLYGON_MUMBAI = new Token(
SupportedChainId.POLYGON_MUMBAI,
'0xe11a86849d99f524cac3e7a0ec1241828e332c62',
6,
'USDC',
'USD//C'
)
export const AMPL = new Token(
SupportedChainId.MAINNET,
'0xD46bA6D942050d489DBd938a2C909A5d5039A161',
@@ -31,27 +110,19 @@ export const DAI_OPTIMISM = new Token(
'DAI',
'Dai stable coin'
)
export const USDC = new Token(
SupportedChainId.MAINNET,
'0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48',
6,
'USDC',
'USD//C'
)
export const USDC_ARBITRUM = new Token(
SupportedChainId.ARBITRUM_ONE,
'0xFF970A61A04b1cA14834A43f5dE4533eBDDB5CC8',
6,
'USDC',
'USD//C'
)
export const USDC_POLYGON = new Token(
SupportedChainId.POLYGON,
'0x2791bca1f2de4661ed88a30c99a7a9449aa84174',
6,
'USDC',
'USD//C'
)
export const USDC: { [chainId in SupportedChainId]: Token } = {
[SupportedChainId.MAINNET]: USDC_MAINNET,
[SupportedChainId.ARBITRUM_ONE]: USDC_ARBITRUM,
[SupportedChainId.OPTIMISM]: USDC_OPTIMISM,
[SupportedChainId.ARBITRUM_RINKEBY]: USDC_ARBITRUM_RINKEBY,
[SupportedChainId.OPTIMISTIC_KOVAN]: USDC_OPTIMISTIC_KOVAN,
[SupportedChainId.POLYGON]: USDC_POLYGON,
[SupportedChainId.POLYGON_MUMBAI]: USDC_POLYGON_MUMBAI,
[SupportedChainId.GOERLI]: USDC_GOERLI,
[SupportedChainId.RINKEBY]: USDC_RINKEBY,
[SupportedChainId.KOVAN]: USDC_KOVAN,
[SupportedChainId.ROPSTEN]: USDC_ROPSTEN,
}
export const DAI_POLYGON = new Token(
SupportedChainId.POLYGON,
'0x8f3Cf7ad23Cd3CaDbD9735AFf958023239c6A063',
@@ -73,13 +144,6 @@ export const WBTC_POLYGON = new Token(
'WBTC',
'Wrapped BTC'
)
export const USDC_OPTIMISM = new Token(
SupportedChainId.OPTIMISM,
'0x7F5c764cBc14f9669B88837ca1490cCa17c31607',
6,
'USDC',
'USD//C'
)
export const USDT = new Token(
SupportedChainId.MAINNET,
'0xdAC17F958D2ee523a2206206994597C13D831ec7',
@@ -208,8 +272,8 @@ export const UNI: { [chainId: number]: Token } = {
[SupportedChainId.KOVAN]: new Token(SupportedChainId.KOVAN, UNI_ADDRESS[42], 18, 'UNI', 'Uniswap'),
}
export const WRAPPED_NATIVE_CURRENCY: { [chainId: number]: Token } = {
...WETH9,
export const WRAPPED_NATIVE_CURRENCY: { [chainId: number]: Token | undefined } = {
...(WETH9 as Record<SupportedChainId, Token>),
[SupportedChainId.OPTIMISM]: new Token(
SupportedChainId.OPTIMISM,
'0x4200000000000000000000000000000000000006',
@@ -265,7 +329,9 @@ class MaticNativeCurrency extends NativeCurrency {
get wrapped(): Token {
if (!isMatic(this.chainId)) throw new Error('Not matic')
return WRAPPED_NATIVE_CURRENCY[this.chainId]
const wrapped = WRAPPED_NATIVE_CURRENCY[this.chainId]
invariant(wrapped instanceof Token)
return wrapped
}
public constructor(chainId: number) {
@@ -276,7 +342,8 @@ class MaticNativeCurrency extends NativeCurrency {
export class ExtendedEther extends Ether {
public get wrapped(): Token {
if (this.chainId in WRAPPED_NATIVE_CURRENCY) return WRAPPED_NATIVE_CURRENCY[this.chainId]
const wrapped = WRAPPED_NATIVE_CURRENCY[this.chainId]
if (wrapped) return wrapped
throw new Error('Unsupported chain ID')
}
@@ -296,3 +363,19 @@ export function nativeOnChain(chainId: number): NativeCurrency {
: ExtendedEther.onChain(chainId))
)
}
export const TOKEN_SHORTHANDS: { [shorthand: string]: { [chainId in SupportedChainId]?: string } } = {
USDC: {
[SupportedChainId.MAINNET]: USDC_MAINNET.address,
[SupportedChainId.ARBITRUM_ONE]: USDC_ARBITRUM.address,
[SupportedChainId.OPTIMISM]: USDC_OPTIMISM.address,
[SupportedChainId.ARBITRUM_RINKEBY]: USDC_ARBITRUM_RINKEBY.address,
[SupportedChainId.OPTIMISTIC_KOVAN]: USDC_OPTIMISTIC_KOVAN.address,
[SupportedChainId.POLYGON]: USDC_POLYGON.address,
[SupportedChainId.POLYGON_MUMBAI]: USDC_POLYGON_MUMBAI.address,
[SupportedChainId.GOERLI]: USDC_GOERLI.address,
[SupportedChainId.RINKEBY]: USDC_RINKEBY.address,
[SupportedChainId.KOVAN]: USDC_KOVAN.address,
[SupportedChainId.ROPSTEN]: USDC_ROPSTEN.address,
},
}

View File

@@ -2,7 +2,7 @@ import { Currency, Token } from '@uniswap/sdk-core'
import { CHAIN_INFO } from 'constants/chainInfo'
import { L2_CHAIN_IDS, SupportedChainId, SupportedL2ChainId } from 'constants/chains'
import useActiveWeb3React from 'hooks/useActiveWeb3React'
import { useCurrencyFromMap, useTokenFromMap } from 'lib/hooks/useCurrency'
import { useCurrencyFromMap, useTokenFromMapOrNetwork } from 'lib/hooks/useCurrency'
import { getTokenFilter } from 'lib/hooks/useTokenList/filtering'
import { useMemo } from 'react'
@@ -159,7 +159,7 @@ export function useIsUserAddedToken(currency: Currency | undefined | null): bool
// otherwise returns the token
export function useToken(tokenAddress?: string | null): Token | null | undefined {
const tokens = useAllTokens()
return useTokenFromMap(tokens, tokenAddress)
return useTokenFromMapOrNetwork(tokens, tokenAddress)
}
export function useCurrency(currencyId?: string | null): Currency | null | undefined {

View File

@@ -15,7 +15,7 @@ export default function useAddTokenToMetamask(currencyToAdd: Currency | undefine
const logoURL = useCurrencyLogoURIs(token)[0]
const addToken = useCallback(() => {
if (library && library.provider.isMetaMask && library.provider.request && token) {
if (library && library?.provider?.isMetaMask && library.provider.request && token) {
library.provider
.request({
method: 'wallet_watchAsset',

View File

@@ -13,6 +13,7 @@ import useUSDCPrice, { useUSDCValue } from './useUSDCPrice'
const V3_SWAP_DEFAULT_SLIPPAGE = new Percent(50, 10_000) // .50%
const ONE_TENTHS_PERCENT = new Percent(10, 10_000) // .10%
export const DEFAULT_AUTO_SLIPPAGE = ONE_TENTHS_PERCENT
/**
* Return a guess of the gas cost used in computing slippage tolerance for a given trade
@@ -41,10 +42,10 @@ export default function useAutoSlippageTolerance(
const gasEstimate = guesstimateGas(trade)
const nativeCurrency = useNativeCurrency()
const nativeCurrencyPrice = useUSDCPrice(nativeCurrency ?? undefined)
const nativeCurrencyPrice = useUSDCPrice((trade && nativeCurrency) ?? undefined)
return useMemo(() => {
if (!trade || onL2) return ONE_TENTHS_PERCENT
if (!trade || onL2) return DEFAULT_AUTO_SLIPPAGE
const nativeGasCost =
nativeGasPrice && typeof gasEstimate === 'number'

View File

@@ -1,6 +1,6 @@
import { renderHook } from '@testing-library/react-hooks'
import { CurrencyAmount, TradeType } from '@uniswap/sdk-core'
import { DAI, USDC } from 'constants/tokens'
import { DAI, USDC_MAINNET } from 'constants/tokens'
import { TradeState } from 'state/routing/types'
import { useRoutingAPITrade } from '../state/routing/useRoutingAPITrade'
@@ -10,7 +10,7 @@ import { useClientSideV3Trade } from './useClientSideV3Trade'
import useDebounce from './useDebounce'
import useIsWindowVisible from './useIsWindowVisible'
const USDCAmount = CurrencyAmount.fromRawAmount(USDC, '10000')
const USDCAmount = CurrencyAmount.fromRawAmount(USDC_MAINNET, '10000')
const DAIAmount = CurrencyAmount.fromRawAmount(DAI, '10000')
jest.mock('./useAutoRouterSupported')
@@ -126,10 +126,10 @@ describe('#useBestV3Trade ExactOut', () => {
expectRouterMock(TradeState.INVALID)
expectClientSideMock(TradeState.VALID)
const { result } = renderHook(() => useBestTrade(TradeType.EXACT_OUTPUT, DAIAmount, USDC))
const { result } = renderHook(() => useBestTrade(TradeType.EXACT_OUTPUT, DAIAmount, USDC_MAINNET))
expect(mockUseRoutingAPITrade).toHaveBeenCalledWith(TradeType.EXACT_OUTPUT, undefined, USDC)
expect(mockUseClientSideV3Trade).toHaveBeenCalledWith(TradeType.EXACT_OUTPUT, DAIAmount, USDC)
expect(mockUseRoutingAPITrade).toHaveBeenCalledWith(TradeType.EXACT_OUTPUT, undefined, USDC_MAINNET)
expect(mockUseClientSideV3Trade).toHaveBeenCalledWith(TradeType.EXACT_OUTPUT, DAIAmount, USDC_MAINNET)
expect(result.current).toEqual({ state: TradeState.VALID, trade: undefined })
})
@@ -138,17 +138,17 @@ describe('#useBestV3Trade ExactOut', () => {
expectRouterMock(TradeState.NO_ROUTE_FOUND)
expectClientSideMock(TradeState.VALID)
const { result } = renderHook(() => useBestTrade(TradeType.EXACT_OUTPUT, DAIAmount, USDC))
const { result } = renderHook(() => useBestTrade(TradeType.EXACT_OUTPUT, DAIAmount, USDC_MAINNET))
expect(mockUseRoutingAPITrade).toHaveBeenCalledWith(TradeType.EXACT_OUTPUT, undefined, USDC)
expect(mockUseClientSideV3Trade).toHaveBeenCalledWith(TradeType.EXACT_OUTPUT, DAIAmount, USDC)
expect(mockUseRoutingAPITrade).toHaveBeenCalledWith(TradeType.EXACT_OUTPUT, undefined, USDC_MAINNET)
expect(mockUseClientSideV3Trade).toHaveBeenCalledWith(TradeType.EXACT_OUTPUT, DAIAmount, USDC_MAINNET)
expect(result.current).toEqual({ state: TradeState.VALID, trade: undefined })
})
describe('when routing api is in non-error state', () => {
it('does not compute client side v3 trade if routing api is LOADING', () => {
expectRouterMock(TradeState.LOADING)
const { result } = renderHook(() => useBestTrade(TradeType.EXACT_OUTPUT, DAIAmount, USDC))
const { result } = renderHook(() => useBestTrade(TradeType.EXACT_OUTPUT, DAIAmount, USDC_MAINNET))
expect(mockUseClientSideV3Trade).toHaveBeenCalledWith(TradeType.EXACT_OUTPUT, undefined, undefined)
expect(result.current).toEqual({ state: TradeState.LOADING, trade: undefined })
@@ -157,7 +157,7 @@ describe('#useBestV3Trade ExactOut', () => {
it('does not compute client side v3 trade if routing api is VALID', () => {
expectRouterMock(TradeState.VALID)
const { result } = renderHook(() => useBestTrade(TradeType.EXACT_OUTPUT, DAIAmount, USDC))
const { result } = renderHook(() => useBestTrade(TradeType.EXACT_OUTPUT, DAIAmount, USDC_MAINNET))
expect(mockUseClientSideV3Trade).toHaveBeenCalledWith(TradeType.EXACT_OUTPUT, undefined, undefined)
expect(result.current).toEqual({ state: TradeState.VALID, trade: undefined })
@@ -166,7 +166,7 @@ describe('#useBestV3Trade ExactOut', () => {
it('does not compute client side v3 trade if routing api is SYNCING', () => {
expectRouterMock(TradeState.SYNCING)
const { result } = renderHook(() => useBestTrade(TradeType.EXACT_OUTPUT, DAIAmount, USDC))
const { result } = renderHook(() => useBestTrade(TradeType.EXACT_OUTPUT, DAIAmount, USDC_MAINNET))
expect(mockUseClientSideV3Trade).toHaveBeenCalledWith(TradeType.EXACT_OUTPUT, undefined, undefined)
expect(result.current).toEqual({ state: TradeState.SYNCING, trade: undefined })
@@ -178,7 +178,7 @@ describe('#useBestV3Trade ExactOut', () => {
expectRouterMock(TradeState.INVALID)
expectClientSideMock(TradeState.VALID)
renderHook(() => useBestTrade(TradeType.EXACT_OUTPUT, DAIAmount, USDC))
renderHook(() => useBestTrade(TradeType.EXACT_OUTPUT, DAIAmount, USDC_MAINNET))
expect(mockUseClientSideV3Trade).toHaveBeenCalledWith(TradeType.EXACT_OUTPUT, undefined, undefined)
})
@@ -187,9 +187,9 @@ describe('#useBestV3Trade ExactOut', () => {
expectRouterMock(TradeState.NO_ROUTE_FOUND)
expectClientSideMock(TradeState.VALID)
const { result } = renderHook(() => useBestTrade(TradeType.EXACT_OUTPUT, DAIAmount, USDC))
const { result } = renderHook(() => useBestTrade(TradeType.EXACT_OUTPUT, DAIAmount, USDC_MAINNET))
expect(mockUseClientSideV3Trade).toHaveBeenCalledWith(TradeType.EXACT_OUTPUT, DAIAmount, USDC)
expect(mockUseClientSideV3Trade).toHaveBeenCalledWith(TradeType.EXACT_OUTPUT, DAIAmount, USDC_MAINNET)
expect(result.current).toEqual({ state: TradeState.VALID, trade: undefined })
})
})

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,9 +1,9 @@
import { Token } from '@uniswap/sdk-core'
import { SupportedChainId } from 'constants/chains'
import uriToHttp from 'lib/utils/uriToHttp'
import Vibrant from 'node-vibrant/lib/bundle'
import Vibrant from 'node-vibrant/lib/bundle.js'
import { shade } from 'polished'
import { useLayoutEffect, useState } from 'react'
import { useEffect, useState } from 'react'
import { WrappedTokenInfo } from 'state/lists/wrappedTokenInfo'
import { hex } from 'wcag-contrast'
@@ -64,7 +64,7 @@ async function getColorFromUriPath(uri: string): Promise<string | null> {
export function useColor(token?: Token) {
const [color, setColor] = useState('#2172E5')
useLayoutEffect(() => {
useEffect(() => {
let stale = false
if (token) {
@@ -87,7 +87,7 @@ export function useColor(token?: Token) {
export function useListColor(listImageUri?: string) {
const [color, setColor] = useState('#2172E5')
useLayoutEffect(() => {
useEffect(() => {
let stale = false
if (listImageUri) {

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

@@ -10,7 +10,7 @@ import { useSingleCallResult } from 'lib/hooks/multicall'
import { useMemo, useState } from 'react'
import { SWAP_ROUTER_ADDRESSES, V3_ROUTER_ADDRESS } from '../constants/addresses'
import { DAI, UNI, USDC } from '../constants/tokens'
import { DAI, UNI, USDC_MAINNET } from '../constants/tokens'
import { useEIP2612Contract } from './useContract'
import useIsArgentWallet from './useIsArgentWallet'
@@ -36,7 +36,7 @@ const PERMITTABLE_TOKENS: {
}
} = {
1: {
[USDC.address]: { type: PermitType.AMOUNT, name: 'USD Coin', version: '2' },
[USDC_MAINNET.address]: { type: PermitType.AMOUNT, name: 'USD Coin', version: '2' },
[DAI.address]: { type: PermitType.ALLOWED, name: 'Dai Stablecoin', version: '1' },
[UNI[1].address]: { type: PermitType.AMOUNT, name: 'Uniswap' },
},

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

@@ -12,13 +12,14 @@ function isWindowVisible() {
* Returns whether the window is currently visible to the user.
*/
export default function useIsWindowVisible(): boolean {
const [focused, setFocused] = useState<boolean>(isWindowVisible())
const [focused, setFocused] = useState<boolean>(false)
const listener = useCallback(() => {
setFocused(isWindowVisible())
}, [setFocused])
useEffect(() => {
if (!isVisibilityStateSupported()) return undefined
setFocused((focused) => isWindowVisible())
document.addEventListener('visibilitychange', listener)
return () => {

View File

@@ -3,7 +3,7 @@ import { LocationDescriptor } from 'history'
import useParsedQueryString from 'hooks/useParsedQueryString'
import { stringify } from 'qs'
import { useMemo } from 'react'
import ReactGA from 'react-ga'
import ReactGA from 'react-ga4'
import { useLocation } from 'react-router-dom'
import { useActiveLocale } from './useActiveLocale'

View File

@@ -1,7 +1,7 @@
import { skipToken } from '@reduxjs/toolkit/query/react'
import { Currency } from '@uniswap/sdk-core'
import { ChainId } from '@uniswap/smart-order-router'
import { FeeAmount, nearestUsableTick, Pool, TICK_SPACINGS, tickToPrice } from '@uniswap/v3-sdk'
import { SupportedChainId } from 'constants/chains'
import { ZERO_ADDRESS } from 'constants/misc'
import JSBI from 'jsbi'
import { useSingleContractMultipleData } from 'lib/hooks/multicall'
@@ -14,7 +14,7 @@ import { useTickLens } from './useContract'
import { PoolState, usePool } from './usePools'
const PRICE_FIXED_DIGITS = 8
const CHAIN_IDS_MISSING_SUBGRAPH_DATA = [ChainId.ARBITRUM_ONE, ChainId.ARBITRUM_RINKEBY]
const CHAIN_IDS_MISSING_SUBGRAPH_DATA = [SupportedChainId.ARBITRUM_ONE, SupportedChainId.ARBITRUM_RINKEBY]
export interface TickData {
tick: number

View File

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

@@ -1,17 +1,17 @@
import { Currency, CurrencyAmount, Price, Token, TradeType } from '@uniswap/sdk-core'
import useActiveWeb3React from 'hooks/useActiveWeb3React'
import tryParseCurrencyAmount from 'lib/utils/tryParseCurrencyAmount'
import { useMemo } from 'react'
import { useMemo, useRef } from 'react'
import { SupportedChainId } from '../constants/chains'
import { DAI_OPTIMISM, USDC, USDC_ARBITRUM, USDC_POLYGON } from '../constants/tokens'
import { DAI_OPTIMISM, USDC_ARBITRUM, USDC_MAINNET, USDC_POLYGON } from '../constants/tokens'
import { useBestV2Trade } from './useBestV2Trade'
import { useClientSideV3Trade } from './useClientSideV3Trade'
// Stablecoin amounts used when calculating spot price for a given currency.
// The amount is large enough to filter low liquidity pairs.
export const STABLECOIN_AMOUNT_OUT: { [chainId: number]: CurrencyAmount<Token> } = {
[SupportedChainId.MAINNET]: CurrencyAmount.fromRawAmount(USDC, 100_000e6),
[SupportedChainId.MAINNET]: CurrencyAmount.fromRawAmount(USDC_MAINNET, 100_000e6),
[SupportedChainId.ARBITRUM_ONE]: CurrencyAmount.fromRawAmount(USDC_ARBITRUM, 10_000e6),
[SupportedChainId.OPTIMISM]: CurrencyAmount.fromRawAmount(DAI_OPTIMISM, 10_000e18),
[SupportedChainId.POLYGON]: CurrencyAmount.fromRawAmount(USDC_POLYGON, 10_000e6),
@@ -32,8 +32,7 @@ export default function useUSDCPrice(currency?: Currency): Price<Currency, Token
maxHops: 2,
})
const v3USDCTrade = useClientSideV3Trade(TradeType.EXACT_OUTPUT, amountOut, currency)
return useMemo(() => {
const price = useMemo(() => {
if (!currency || !stablecoin) {
return undefined
}
@@ -54,6 +53,12 @@ export default function useUSDCPrice(currency?: Currency): Price<Currency, Token
return undefined
}, [currency, stablecoin, v2USDCTrade, v3USDCTrade.trade])
const lastPrice = useRef(price)
if (!price || !lastPrice.current || !price.equalTo(lastPrice.current)) {
lastPrice.current = price
}
return lastPrice.current
}
export function useUSDCValue(currencyAmount: CurrencyAmount<Currency> | undefined | null) {
@@ -78,17 +83,18 @@ export function useStablecoinAmountFromFiatValue(fiatValue: string | null | unde
const { chainId } = useActiveWeb3React()
const stablecoin = chainId ? STABLECOIN_AMOUNT_OUT[chainId]?.currency : undefined
if (fiatValue === null || fiatValue === undefined || !chainId || !stablecoin) {
return undefined
}
return useMemo(() => {
if (fiatValue === null || fiatValue === undefined || !chainId || !stablecoin) {
return undefined
}
// trim for decimal precision when parsing
const parsedForDecimals = parseFloat(fiatValue).toFixed(stablecoin.decimals).toString()
try {
// parse USD string into CurrencyAmount based on stablecoin decimals
return tryParseCurrencyAmount(parsedForDecimals, stablecoin)
} catch (error) {
return undefined
}
// trim for decimal precision when parsing
const parsedForDecimals = parseFloat(fiatValue).toFixed(stablecoin.decimals).toString()
try {
// parse USD string into CurrencyAmount based on stablecoin decimals
return tryParseCurrencyAmount(parsedForDecimals, stablecoin)
} catch (error) {
return undefined
}
}, [chainId, fiatValue, stablecoin])
}

View File

@@ -1,14 +1,12 @@
import { Interface } from '@ethersproject/abi'
import { Currency, CurrencyAmount } from '@uniswap/sdk-core'
import IUniswapV2PairJson from '@uniswap/v2-core/build/IUniswapV2Pair.json'
import { abi as IUniswapV2PairABI } from '@uniswap/v2-core/build/IUniswapV2Pair.json'
import { computePairAddress, Pair } from '@uniswap/v2-sdk'
import { useMultipleContractSingleData } from 'lib/hooks/multicall'
import { useMemo } from 'react'
import { V2_FACTORY_ADDRESSES } from '../constants/addresses'
const { abi: IUniswapV2PairABI } = IUniswapV2PairJson
const PAIR_INTERFACE = new Interface(IUniswapV2PairABI)
export enum PairState {

View File

@@ -23,13 +23,11 @@ export function useV3PositionFees(
const tokenIdHexString = tokenId?.toHexString()
const latestBlockNumber = useBlockNumber()
// TODO find a way to get this into multicall
// we can't use multicall for this because we need to simulate the call from a specific address
// latestBlockNumber is included to ensure data stays up-to-date every block
const [amounts, setAmounts] = useState<[BigNumber, BigNumber]>()
const [amounts, setAmounts] = useState<[BigNumber, BigNumber] | undefined>()
useEffect(() => {
let stale = false
if (positionManager && tokenIdHexString && owner && typeof latestBlockNumber === 'number') {
if (positionManager && tokenIdHexString && owner) {
positionManager.callStatic
.collect(
{
@@ -41,19 +39,15 @@ export function useV3PositionFees(
{ from: owner } // need to simulate the call as the owner
)
.then((results) => {
if (!stale) setAmounts([results.amount0, results.amount1])
setAmounts([results.amount0, results.amount1])
})
}
return () => {
stale = true
}
}, [positionManager, tokenIdHexString, owner, latestBlockNumber])
if (pool && amounts) {
return [
CurrencyAmount.fromRawAmount(!asWETH ? unwrappedToken(pool.token0) : pool.token0, amounts[0].toString()),
CurrencyAmount.fromRawAmount(!asWETH ? unwrappedToken(pool.token1) : pool.token1, amounts[1].toString()),
CurrencyAmount.fromRawAmount(asWETH ? pool.token0 : unwrappedToken(pool.token0), amounts[0].toString()),
CurrencyAmount.fromRawAmount(asWETH ? pool.token1 : unwrappedToken(pool.token1), amounts[1].toString()),
]
} else {
return [undefined, undefined]

View File

@@ -3,7 +3,7 @@ import 'inter-ui'
import 'polyfills'
import 'components/analytics'
import { BlockUpdater } from 'lib/hooks/useBlockNumber'
import { BlockNumberProvider } from 'lib/hooks/useBlockNumber'
import { MulticallUpdater } from 'lib/state/multicall'
import { StrictMode } from 'react'
import ReactDOM from 'react-dom'
@@ -40,7 +40,6 @@ function Updaters() {
<UserUpdater />
<ApplicationUpdater />
<TransactionUpdater />
<BlockUpdater />
<MulticallUpdater />
<LogsUpdater />
</>
@@ -55,11 +54,13 @@ ReactDOM.render(
<Web3ReactProvider getLibrary={getLibrary}>
<Web3ProviderNetwork getLibrary={getLibrary}>
<Blocklist>
<Updaters />
<ThemeProvider>
<ThemedGlobalStyle />
<App />
</ThemeProvider>
<BlockNumberProvider>
<Updaters />
<ThemeProvider>
<ThemedGlobalStyle />
<App />
</ThemeProvider>
</BlockNumberProvider>
</Blocklist>
</Web3ProviderNetwork>
</Web3ReactProvider>
@@ -73,4 +74,3 @@ ReactDOM.render(
if (process.env.REACT_APP_SERVICE_WORKER !== 'false') {
serviceWorkerRegistration.register()
}
export { INFURA_NETWORK_URLS } from 'constants/infura'

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

@@ -0,0 +1,35 @@
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<mask id="mask">
<circle cx="12" cy="12" r="10" fill="black" stroke="black" stroke-width="2" />
<rect width="12" height="12" fill="white" stroke-width="0" />
<circle cx="2" cy="12" r="1" fill="white" stroke-width="0" />
<circle cx="12" cy="2" r="1" fill="white" stroke-width="0" />
</mask>
<circle
cx="12"
cy="12"
r="6"
stroke="none"
/>
<circle
id="track"
cx="12"
cy="12"
r="10"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
fill="none"
/>
<circle
cx="12"
cy="12"
r="10"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
fill="none"
mask="url(#mask)"
/>
</svg>

After

Width:  |  Height:  |  Size: 931 B

View File

@@ -1,12 +1,11 @@
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<mask id="mask">
<rect width="24" height="24" fill="white" stroke-width="0" />
<circle cx="12" cy="12" r="10" stroke="white" stroke-width="2" />
<rect width="12" height="12" fill="black" stroke-width="0" />
<circle cx="2" cy="12" r="1" fill="white" stroke-width="0" />
<circle cx="12" cy="2" r="1" fill="white" stroke-width="0" />
</mask>
<circle
id="circle"
cx="12"
cy="12"
r="10"

Before

Width:  |  Height:  |  Size: 608 B

After

Width:  |  Height:  |  Size: 592 B

View File

@@ -6,9 +6,9 @@ import Button from './Button'
import Row from './Row'
const StyledButton = styled(Button)`
border-radius: ${({ theme }) => theme.borderRadius}em;
border-radius: ${({ theme }) => theme.borderRadius * 0.75}em;
flex-grow: 1;
transition: background-color 0.25s ease-out, flex-grow 0.25s ease-out, padding 0.25s ease-out;
transition: background-color 0.25s ease-out, border-radius 0.25s ease-out, flex-grow 0.25s ease-out;
:disabled {
margin: -1px;
@@ -35,11 +35,13 @@ const actionCss = css`
${ActionRow} {
animation: ${grow} 0.25s ease-in;
flex-grow: 1;
justify-content: flex-start;
white-space: nowrap;
}
${StyledButton} {
border-radius: ${({ theme }) => theme.borderRadius * 0.75}em;
border-radius: ${({ theme }) => theme.borderRadius}em;
flex-grow: 0;
padding: 1em;
}
@@ -57,27 +59,28 @@ export const Overlay = styled(Row)<{ hasAction: boolean }>`
export interface Action {
message: ReactNode
icon?: Icon
onClick: () => void
children: ReactNode
onClick?: () => void
children?: ReactNode
}
export interface ActionButtonProps {
export interface BaseProps {
color?: Color
disabled?: boolean
action?: Action
onClick: () => void
children: ReactNode
}
export type ActionButtonProps = BaseProps & Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, keyof BaseProps>
export default function ActionButton({ color = 'accent', disabled, action, onClick, children }: ActionButtonProps) {
const textColor = useMemo(() => (color === 'accent' && !disabled ? 'onAccent' : 'currentColor'), [color, disabled])
return (
<Overlay hasAction={Boolean(action)} flex align="stretch">
<StyledButton color={color} disabled={disabled} onClick={action ? action.onClick : onClick}>
<ThemedText.TransitionButton buttonSize={action ? 'medium' : 'large'} color={textColor}>
{action ? action.children : children}
</ThemedText.TransitionButton>
</StyledButton>
{(action ? action.onClick : true) && (
<StyledButton color={color} disabled={disabled} onClick={action?.onClick || onClick}>
<ThemedText.TransitionButton buttonSize={action ? 'medium' : 'large'} color={textColor}>
{action?.children || children}
</ThemedText.TransitionButton>
</StyledButton>
)}
{action && (
<ActionRow gap={0.5}>
<LargeIcon color="currentColor" icon={action.icon || AlertTriangle} />

View File

@@ -1,11 +0,0 @@
import styled, { Color } from 'lib/theme'
import Row from './Row'
const Badge = styled(Row)<{ borderRadius?: number; padding?: string; color?: Color }>`
background-color: ${({ theme, color = 'outline' }) => theme[color]};
border-radius: ${({ borderRadius }) => `${borderRadius ?? 0.5}em`};
padding: ${({ padding }) => padding ?? '0.25em 0.375em'};
`
export default Badge

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,5 +1,5 @@
import { Icon } from 'lib/icons'
import styled, { Color } from 'lib/theme'
import styled, { Color, css } from 'lib/theme'
import { ComponentProps, forwardRef } from 'react'
export const BaseButton = styled.button`
@@ -10,21 +10,31 @@ export const BaseButton = styled.button`
cursor: pointer;
font-size: inherit;
font-weight: inherit;
height: inherit;
line-height: inherit;
margin: 0;
padding: 0;
:enabled {
transition: filter 0.125s linear;
}
:disabled {
cursor: initial;
filter: saturate(0) opacity(0.4);
}
`
const transitionCss = css`
transition: background-color 0.125s linear, border-color 0.125s linear, filter 0.125s linear;
`
export default styled(BaseButton)<{ color?: Color }>`
export default styled(BaseButton)<{ color?: Color; transition?: boolean }>`
border: 1px solid transparent;
color: ${({ color = 'interactive', theme }) => color === 'interactive' && theme.onInteractive};
:enabled {
background-color: ${({ color = 'interactive', theme }) => theme[color]};
${({ transition = true }) => transition && transitionCss};
}
:enabled:hover {
@@ -32,9 +42,8 @@ export default styled(BaseButton)<{ color?: Color }>`
}
:disabled {
border: 1px solid ${({ theme }) => theme.outline};
border-color: ${({ theme }) => theme.outline};
color: ${({ theme }) => theme.secondary};
cursor: initial;
}
`

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'
@@ -40,7 +40,10 @@ export function Provider({ value, children }: ProviderProps) {
}
}, [active])
return (
<div ref={ref}>
<div
ref={ref}
style={{ isolation: 'isolate' }} // creates a new stacking context, preventing the dialog from intercepting non-dialog clicks
>
<Context.Provider value={context}>{children}</Context.Provider>
</div>
)
@@ -94,8 +97,10 @@ export default function Dialog({ color, children, onClose = () => void 0 }: Dial
context.setActive(true)
return () => context.setActive(false)
}, [context])
const dialog = useRef<HTMLDivElement>(null)
useUnmount(dialog)
const modal = useRef<HTMLDivElement>(null)
useEffect(() => delayUnmountForAnimation(modal), [])
useEffect(() => {
const close = (e: KeyboardEvent) => e.key === 'Escape' && onClose?.()
document.addEventListener('keydown', close, true)
@@ -105,9 +110,11 @@ export default function Dialog({ color, children, onClose = () => void 0 }: Dial
context.element &&
createPortal(
<ThemeProvider>
<Modal color={color} ref={dialog}>
<OnCloseContext.Provider value={onClose}>{children}</OnCloseContext.Provider>
</Modal>
<OnCloseContext.Provider value={onClose}>
<Modal color={color} ref={modal}>
{children}
</Modal>
</OnCloseContext.Provider>
</ThemeProvider>,
context.element
)

View File

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

View File

@@ -1,30 +0,0 @@
import { SUPPORTED_LOCALES } from 'constants/locales'
import { WidgetProps } from 'lib/components/Widget'
import { IntegrationError } from 'lib/errors'
import { PropsWithChildren, useEffect } from 'react'
export default function WidgetsPropsValidator(props: PropsWithChildren<WidgetProps>) {
const { jsonRpcEndpoint, provider } = props
useEffect(() => {
if (!provider && !jsonRpcEndpoint) {
throw new IntegrationError('This widget requires a provider or jsonRpcEndpoint.')
}
}, [provider, jsonRpcEndpoint])
const { width } = props
useEffect(() => {
if (width && width < 300) {
throw new IntegrationError(`Set widget width to at least 300px. (You set it to ${width}.)`)
}
}, [width])
const { locale } = props
useEffect(() => {
if (locale && locale !== 'pseudo' && !SUPPORTED_LOCALES.includes(locale)) {
console.warn('Unsupported locale: ', locale)
}
}, [locale])
return <>{props.children}</>
}

View File

@@ -26,10 +26,12 @@ export default function EtherscanLink({ data, type, color = 'currentColor', chil
() => data && getExplorerLink(chainId || SupportedChainId.MAINNET, data, type),
[chainId, data, type]
)
return (
<StyledExternalLink href={url} color={color} target="_blank">
<Row gap={0.25}>
{children} <Link />
{children}
{url && <Link />}
</Row>
</StyledExternalLink>
)

View File

@@ -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,7 @@
import JSBI from 'jsbi'
import { loadingOpacity } from 'lib/css/loading'
import styled, { css } from 'lib/theme'
import { forwardRef, HTMLProps, useCallback, useEffect, useState } from 'react'
import { transparentize } from 'polished'
import { ChangeEvent, forwardRef, HTMLProps, useCallback } from 'react'
const Input = styled.input`
-webkit-appearance: textfield;
@@ -35,6 +36,16 @@ const Input = styled.input`
::placeholder {
color: ${({ theme }) => theme.secondary};
}
:enabled {
transition: color 0.125s linear;
}
:disabled {
// Overrides WebKit's override of input:disabled color.
-webkit-text-fill-color: ${({ theme }) => transparentize(1 - loadingOpacity, theme.primary)};
color: ${({ theme }) => transparentize(1 - loadingOpacity, theme.primary)};
}
`
export default Input
@@ -77,42 +88,23 @@ interface EnforcedNumericInputProps extends NumericInputProps {
enforcer: (nextUserInput: string) => string | null
}
function isNumericallyEqual(a: string, b: string) {
const [aInteger, aDecimal] = a.split('.')
const [bInteger, bDecimal] = b.split('.')
return (
JSBI.equal(JSBI.BigInt(aInteger ?? 0), JSBI.BigInt(bInteger ?? 0)) &&
JSBI.equal(JSBI.BigInt(aDecimal ?? 0), JSBI.BigInt(bDecimal ?? 0))
)
}
const NumericInput = forwardRef<HTMLInputElement, EnforcedNumericInputProps>(function NumericInput(
{ value, onChange, enforcer, pattern, ...props }: EnforcedNumericInputProps,
ref
) {
const [state, setState] = useState(value ?? '')
useEffect(() => {
if (!isNumericallyEqual(state, value)) {
setState(value ?? '')
}
}, [value, state, setState])
const validateChange = useCallback(
(event) => {
const nextInput = enforcer(event.target.value.replace(/,/g, '.'))
if (nextInput !== null) {
setState(nextInput ?? '')
if (!isNumericallyEqual(nextInput, value)) {
onChange(nextInput)
}
(event: ChangeEvent<HTMLInputElement>) => {
const nextInput = enforcer(event.target.value.replace(/,/g, '.'))?.replace(/^0+$/, '0')
if (nextInput !== undefined) {
onChange(nextInput)
}
},
[value, onChange, enforcer]
[enforcer, onChange]
)
return (
<Input
value={state}
value={value}
onChange={validateChange}
// universal input options
inputMode="decimal"

View File

@@ -14,14 +14,16 @@ const PopoverContainer = styled.div<{ show: boolean }>`
border: 1px solid ${({ theme }) => theme.outline};
border-radius: 0.5em;
opacity: ${(props) => (props.show ? 1 : 0)};
padding: 8px;
padding: 10px 12px;
transition: visibility 0.25s linear, opacity 0.25s linear;
visibility: ${(props) => (props.show ? 'visible' : 'hidden')};
z-index: ${Layer.TOOLTIP};
`
const Reference = styled.div`
align-self: flex-start;
display: inline-block;
height: 1em;
`
const Arrow = styled.div`

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,6 @@
import { useLingui } from '@lingui/react'
import { useUSDCValue } from 'hooks/useUSDCPrice'
import { loadingOpacityCss } from 'lib/css/loading'
import { Currency, CurrencyAmount } from '@uniswap/sdk-core'
import { loadingTransitionCss } from 'lib/css/loading'
import {
useIsSwapFieldIndependent,
useSwapAmount,
@@ -21,8 +21,8 @@ import Row from '../Row'
import TokenImg from '../TokenImg'
import TokenInput from './TokenInput'
export const LoadingRow = styled(Row)<{ $loading: boolean }>`
${loadingOpacityCss};
export const USDC = styled(Row)`
${loadingTransitionCss};
`
export const Balance = styled(ThemedText.Body2)<{ focused: boolean }>`
@@ -45,23 +45,42 @@ export interface InputProps {
focused: boolean
}
interface UseFormattedFieldAmountArguments {
disabled: boolean
currencyAmount?: CurrencyAmount<Currency>
fieldAmount?: string
}
export function useFormattedFieldAmount({ disabled, currencyAmount, fieldAmount }: UseFormattedFieldAmountArguments) {
return useMemo(() => {
if (disabled) {
return ''
}
if (fieldAmount !== undefined) {
return fieldAmount
}
if (currencyAmount) {
return currencyAmount.toSignificant(6)
}
return ''
}, [disabled, currencyAmount, fieldAmount])
}
export default function Input({ disabled, focused }: InputProps) {
const { i18n } = useLingui()
const {
[Field.INPUT]: { balance, amount: tradeCurrencyAmount, usdc },
trade: { state: tradeState },
currencyBalances: { [Field.INPUT]: balance },
currencyAmounts: { [Field.INPUT]: swapInputCurrencyAmount },
} = useSwapInfo()
const inputUSDC = useUSDCValue(swapInputCurrencyAmount)
const [swapInputAmount, updateSwapInputAmount] = useSwapAmount(Field.INPUT)
const [swapInputCurrency, updateSwapInputCurrency] = useSwapCurrency(Field.INPUT)
const [inputAmount, updateInputAmount] = useSwapAmount(Field.INPUT)
const [inputCurrency, updateInputCurrency] = useSwapCurrency(Field.INPUT)
const inputCurrencyAmount = useSwapCurrencyAmount(Field.INPUT)
// extract eagerly in case of reversal
usePrefetchCurrencyColor(swapInputCurrency)
usePrefetchCurrencyColor(inputCurrency)
const isRouteLoading = tradeState === TradeState.SYNCING || tradeState === TradeState.LOADING
const isRouteLoading = disabled || tradeState === TradeState.SYNCING || tradeState === TradeState.LOADING
const isDependentField = !useIsSwapFieldIndependent(Field.INPUT)
const isLoading = isRouteLoading && isDependentField
@@ -77,27 +96,33 @@ export default function Input({ disabled, focused }: InputProps) {
const balanceColor = useMemo(() => {
const insufficientBalance =
balance &&
(inputCurrencyAmount ? inputCurrencyAmount.greaterThan(balance) : swapInputCurrencyAmount?.greaterThan(balance))
(inputCurrencyAmount ? inputCurrencyAmount.greaterThan(balance) : tradeCurrencyAmount?.greaterThan(balance))
return insufficientBalance ? 'error' : undefined
}, [balance, inputCurrencyAmount, swapInputCurrencyAmount])
}, [balance, inputCurrencyAmount, tradeCurrencyAmount])
const amount = useFormattedFieldAmount({
disabled,
currencyAmount: tradeCurrencyAmount,
fieldAmount: inputAmount,
})
return (
<InputColumn gap={0.5} approved={mockApproved}>
<TokenInput
currency={swapInputCurrency}
amount={(swapInputAmount !== undefined ? swapInputAmount : swapInputCurrencyAmount?.toSignificant(6)) ?? ''}
currency={inputCurrency}
amount={amount}
max={max}
disabled={disabled}
onChangeInput={updateSwapInputAmount}
onChangeCurrency={updateSwapInputCurrency}
onChangeInput={updateInputAmount}
onChangeCurrency={updateInputCurrency}
loading={isLoading}
>
<ThemedText.Body2 color="secondary">
<ThemedText.Body2 color="secondary" userSelect>
<Row>
<LoadingRow $loading={isLoading}>{inputUSDC ? `$${inputUSDC.toFixed(2)}` : '-'}</LoadingRow>
<USDC isLoading={isRouteLoading}>{usdc ? `$${formatCurrencyAmount(usdc, 6, 'en', 2)}` : '-'}</USDC>
{balance && (
<Balance color={balanceColor} focused={focused}>
Balance: <span style={{ userSelect: 'text' }}>{formatCurrencyAmount(balance, 4, i18n.locale)}</span>
Balance: <span>{formatCurrencyAmount(balance, 4, i18n.locale)}</span>
</Balance>
)}
</Row>

View File

@@ -1,6 +1,5 @@
import { Trans } from '@lingui/macro'
import { useLingui } from '@lingui/react'
import { useUSDCValue } from 'hooks/useUSDCPrice'
import { atom } from 'jotai'
import { useAtomValue } from 'jotai/utils'
import BrandedFooter from 'lib/components/BrandedFooter'
@@ -8,15 +7,13 @@ import { useIsSwapFieldIndependent, useSwapAmount, useSwapCurrency, useSwapInfo
import useCurrencyColor from 'lib/hooks/useCurrencyColor'
import { Field } from 'lib/state/swap'
import styled, { DynamicThemeProvider, ThemedText } from 'lib/theme'
import { PropsWithChildren, useMemo } from 'react'
import { PropsWithChildren } from 'react'
import { TradeState } from 'state/routing/types'
import { computeFiatValuePriceImpact } from 'utils/computeFiatValuePriceImpact'
import { formatCurrencyAmount } from 'utils/formatCurrencyAmount'
import { getPriceImpactWarning } from 'utils/prices'
import Column from '../Column'
import Row from '../Row'
import { Balance, InputProps, LoadingRow } from './Input'
import { Balance, InputProps, USDC, useFormattedFieldAmount } from './Input'
import TokenInput from './TokenInput'
export const colorAtom = atom<string | undefined>(undefined)
@@ -41,15 +38,15 @@ export default function Output({ disabled, focused, children }: PropsWithChildre
const { i18n } = useLingui()
const {
[Field.OUTPUT]: { balance, amount: outputCurrencyAmount, usdc: outputUSDC },
trade: { state: tradeState },
currencyBalances: { [Field.OUTPUT]: balance },
currencyAmounts: { [Field.INPUT]: inputCurrencyAmount, [Field.OUTPUT]: outputCurrencyAmount },
impact,
} = useSwapInfo()
const [swapOutputAmount, updateSwapOutputAmount] = useSwapAmount(Field.OUTPUT)
const [swapOutputCurrency, updateSwapOutputCurrency] = useSwapCurrency(Field.OUTPUT)
const isRouteLoading = tradeState === TradeState.SYNCING || tradeState === TradeState.LOADING
const isRouteLoading = disabled || tradeState === TradeState.SYNCING || tradeState === TradeState.LOADING
const isDependentField = !useIsSwapFieldIndependent(Field.OUTPUT)
const isLoading = isRouteLoading && isDependentField
@@ -60,23 +57,11 @@ export default function Output({ disabled, focused, children }: PropsWithChildre
// different state true/null/false allow smoother color transition
const hasColor = swapOutputCurrency ? Boolean(color) || null : false
const inputUSDC = useUSDCValue(inputCurrencyAmount)
const outputUSDC = useUSDCValue(outputCurrencyAmount)
const priceImpact = useMemo(() => {
const fiatValuePriceImpact = computeFiatValuePriceImpact(inputUSDC, outputUSDC)
if (!fiatValuePriceImpact) return null
const color = getPriceImpactWarning(fiatValuePriceImpact)
const sign = fiatValuePriceImpact.lessThan(0) ? '+' : ''
const displayedPriceImpact = parseFloat(fiatValuePriceImpact.multiply(-1)?.toSignificant(3))
return (
<ThemedText.Body2 color={color}>
({sign}
{displayedPriceImpact}%)
</ThemedText.Body2>
)
}, [inputUSDC, outputUSDC])
const amount = useFormattedFieldAmount({
disabled,
currencyAmount: outputCurrencyAmount,
fieldAmount: swapOutputAmount,
})
return (
<DynamicThemeProvider color={color}>
@@ -88,20 +73,21 @@ export default function Output({ disabled, focused, children }: PropsWithChildre
</Row>
<TokenInput
currency={swapOutputCurrency}
amount={(swapOutputAmount !== undefined ? swapOutputAmount : outputCurrencyAmount?.toSignificant(6)) ?? ''}
amount={amount}
disabled={disabled}
onChangeInput={updateSwapOutputAmount}
onChangeCurrency={updateSwapOutputCurrency}
loading={isLoading}
>
<ThemedText.Body2 color="secondary">
<ThemedText.Body2 color="secondary" userSelect>
<Row>
<LoadingRow gap={0.5} $loading={isLoading}>
{outputUSDC ? `$${outputUSDC.toFixed(2)}` : '-'} {priceImpact}
</LoadingRow>
<USDC gap={0.5} isLoading={isRouteLoading}>
{outputUSDC ? `$${formatCurrencyAmount(outputUSDC, 6, 'en', 2)}` : '-'}{' '}
{impact && <ThemedText.Body2 color={impact.warning}>({impact.toString()})</ThemedText.Body2>}
</USDC>
{balance && (
<Balance focused={focused}>
Balance: <span style={{ userSelect: 'text' }}>{formatCurrencyAmount(balance, 4, i18n.locale)}</span>
Balance: <span>{formatCurrencyAmount(balance, 4, i18n.locale)}</span>
</Balance>
)}
</Row>

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

@@ -33,6 +33,7 @@ const Overlay = styled.div`
const StyledReverseButton = styled(Button)<{ turns: number }>`
border-radius: ${({ theme }) => theme.borderRadius * 0.75}em;
color: ${({ theme }) => theme.primary};
height: 2.5em;
position: relative;
width: 2.5em;

View File

@@ -1,8 +1,7 @@
import { Plural, Trans } from '@lingui/macro'
import { Currency, TradeType } from '@uniswap/sdk-core'
import { FeeAmount } from '@uniswap/v3-sdk'
import { ReactComponent as DotLineImage } from 'assets/svg/dot_line.svg'
import Badge from 'lib/components/Badge'
import { ReactComponent as DotLine } from 'assets/svg/dot_line.svg'
import Column from 'lib/components/Column'
import Row from 'lib/components/Row'
import Rule from 'lib/components/Rule'
@@ -14,47 +13,6 @@ import { InterfaceTrade } from 'state/routing/types'
import { getTokenPath, RoutingDiagramEntry } from './utils'
const Wrapper = styled(Column)`
padding: 0.25em;
`
const RouteRow = styled(Row)`
grid-template-columns: 1em 1.15em 1fr 1em;
min-width: 430px;
`
const RouteDetailsContainer = styled(Row)`
padding: 0.1em 0.5em;
position: relative;
`
const DotsContainer = styled.div`
align-items: center;
display: flex;
opacity: 0.5;
position: absolute;
width: calc(100% - 1em);
z-index: ${Layer.UNDERLAYER};
`
const DotsContainerShort = styled(DotsContainer)`
overflow: hidden;
position: relative;
width: 71px;
`
const Dots = styled(DotLineImage)`
path {
stroke: ${({ theme }) => theme.secondary};
}
`
const DetailsRow = styled(Row)`
display: grid;
grid-template-columns: 4.8125em 1fr;
width: 100%;
`
const StyledAutoRouterLabel = styled(ThemedText.ButtonSmall)`
@supports (-webkit-background-clip: text) and (-webkit-text-fill-color: transparent) {
background-image: linear-gradient(90deg, #2172e5 0%, #54e521 163.16%);
@@ -63,6 +21,66 @@ const StyledAutoRouterLabel = styled(ThemedText.ButtonSmall)`
}
`
function Header({ routes }: { routes: RoutingDiagramEntry[] }) {
return (
<Row justify="space-between" gap={1}>
<ThemedText.Subhead2>
<Row gap={0.25}>
<AutoRouter />
<StyledAutoRouterLabel color="primary" lineHeight={'16px'}>
<Trans>Auto Router</Trans>
</StyledAutoRouterLabel>
</Row>
</ThemedText.Subhead2>
<ThemedText.Body2>
<Plural value={routes.length} _1="Best route via 1 hop" other="Best route via # hops" />
</ThemedText.Body2>
</Row>
)
}
const Dots = styled(DotLine)`
color: ${({ theme }) => theme.outline};
position: absolute;
z-index: ${Layer.UNDERLAYER};
`
const RouteRow = styled(Row)`
flex-wrap: nowrap;
`
const RouteNode = styled(Row)`
background-color: ${({ theme }) => theme.interactive};
border-radius: ${({ theme }) => `${(theme.borderRadius ?? 1) * 0.5}em`};
margin-left: 1.625em;
padding: 0.25em 0.375em;
width: max-content;
`
const RouteBadge = styled.div`
background-color: ${({ theme }) => theme.module};
border-radius: ${({ theme }) => `${(theme.borderRadius ?? 1) * 0.25}em`};
padding: 0.125em;
`
function RouteDetail({ route }: { route: RoutingDiagramEntry }) {
const protocol = route.protocol.toUpperCase()
return (
<RouteNode>
<Row gap={0.375}>
<ThemedText.Caption>{route.percent.toSignificant(2)}%</ThemedText.Caption>
<RouteBadge>
<ThemedText.Badge color="secondary">{protocol}</ThemedText.Badge>
</RouteBadge>
</Row>
</RouteNode>
)
}
const RoutePool = styled(RouteNode)`
margin: 0 0.75em;
`
function Pool({
originCurrency,
targetCurrency,
@@ -73,15 +91,36 @@ function Pool({
feeAmount: FeeAmount
}) {
return (
<Badge padding="0 4px" color="dialog">
<Badge gap={0.375}>
<Row>
<RoutePool>
<ThemedText.Caption>
<Row gap={0.25}>
<TokenImg token={originCurrency} />
<TokenImg token={targetCurrency} />
<TokenImg token={targetCurrency} style={{ marginLeft: '-0.65em' }} />
{feeAmount / 10_000}%
</Row>
<ThemedText.Subhead2>{feeAmount / 10_000}%</ThemedText.Subhead2>
</Badge>
</Badge>
</ThemedText.Caption>
</RoutePool>
)
}
function Route({ route }: { route: RoutingDiagramEntry }) {
const [originCurrency] = route.path[0]
const [, targetCurrency] = route.path[route.path.length - 1]
return (
<Row align="center" style={{ gridTemplateColumns: '1em 1fr 1em' }}>
<TokenImg token={originCurrency} />
<RouteRow flex style={{ position: 'relative' }}>
<Dots />
<RouteDetail route={route} />
<RouteRow justify="space-evenly" flex>
{route.path.map(([originCurrency, targetCurrency, feeAmount], index) => (
<Pool key={index} originCurrency={originCurrency} targetCurrency={targetCurrency} feeAmount={feeAmount} />
))}
</RouteRow>
</RouteRow>
<TokenImg token={targetCurrency} />
</Row>
)
}
@@ -89,55 +128,12 @@ export default function RoutingDiagram({ trade }: { trade: InterfaceTrade<Curren
const routes: RoutingDiagramEntry[] = useMemo(() => getTokenPath(trade), [trade])
return (
<Wrapper gap={0.75}>
<Row justify="space-between">
<Row gap={0.25}>
<AutoRouter />
<StyledAutoRouterLabel color="primary" lineHeight={'16px'}>
<Trans>Auto Router</Trans>
</StyledAutoRouterLabel>
</Row>
<ThemedText.ButtonSmall>
<Plural value={routes.length} _1="Best route via 1 hop" other="Best route via # hops" />
</ThemedText.ButtonSmall>
</Row>
<Column gap={0.75}>
<Header routes={routes} />
<Rule />
{routes.map((route, index) => (
<RouteRow key={index} align="center">
<TokenImg token={trade.inputAmount.currency} />
<DotsContainerShort>
<Dots />
</DotsContainerShort>
<RouteDetailsContainer justify="flex-start" flex>
<DotsContainer>
<Dots />
</DotsContainer>
<DetailsRow>
<Badge padding="0 4px" color="dialog">
<Badge gap={0.375}>
<ThemedText.ButtonSmall color="secondary">{route.percent.toSignificant(2)}%</ThemedText.ButtonSmall>
<Badge padding="0.125em" borderRadius={0.25} color="module">
<ThemedText.Badge color="secondary" fontSize={'0.5rem'}>
{route.protocol.toUpperCase()}
</ThemedText.Badge>
</Badge>
</Badge>
</Badge>
<Row justify="space-evenly" flex style={{ width: '100%' }}>
{route.path.map(([originCurrency, targetCurrency, feeAmount], index) => (
<Pool
key={index}
originCurrency={originCurrency}
targetCurrency={targetCurrency}
feeAmount={feeAmount}
/>
))}
</Row>
</DetailsRow>
</RouteDetailsContainer>
<TokenImg token={trade.outputAmount.currency} />
</RouteRow>
<Route key={index} route={route} />
))}
</Wrapper>
</Column>
)
}

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,9 +1,11 @@
import { Trans } from '@lingui/macro'
import ErrorDialog, { StatusHeader } from 'lib/components/Error/ErrorDialog'
import EtherscanLink from 'lib/components/EtherscanLink'
import Rule from 'lib/components/Rule'
import SwapSummary from 'lib/components/Swap/Summary'
import useInterval from 'lib/hooks/useInterval'
import { CheckCircle, Clock, Spinner } from 'lib/icons'
import { SwapTransactionInfo, Transaction } from 'lib/state/transactions'
import { SwapTransactionInfo, Transaction, TransactionType, WrapTransactionInfo } from 'lib/state/transactions'
import styled, { ThemedText } from 'lib/theme'
import ms from 'ms.macro'
import { useCallback, useMemo, useState } from 'react'
@@ -12,7 +14,6 @@ import { ExplorerDataType } from 'utils/getExplorerLink'
import ActionButton from '../../ActionButton'
import Column from '../../Column'
import Row from '../../Row'
import Summary from '../Summary'
const errorMessage = (
<Trans>
@@ -26,7 +27,9 @@ const TransactionRow = styled(Row)`
flex-direction: row-reverse;
`
function ElapsedTime({ tx }: { tx: Transaction<SwapTransactionInfo> }) {
type PendingTransaction = Transaction<SwapTransactionInfo | WrapTransactionInfo>
function ElapsedTime({ tx }: { tx: PendingTransaction }) {
const [elapsedMs, setElapsedMs] = useState(0)
useInterval(() => setElapsedMs(Date.now() - tx.addedTime), tx.receipt ? null : ms`1s`)
@@ -54,7 +57,7 @@ function ElapsedTime({ tx }: { tx: Transaction<SwapTransactionInfo> }) {
}
interface TransactionStatusProps {
tx: Transaction<SwapTransactionInfo>
tx: PendingTransaction
onClose: () => void
}
@@ -63,15 +66,26 @@ function TransactionStatus({ tx, onClose }: TransactionStatusProps) {
return tx.receipt?.status ? CheckCircle : Spinner
}, [tx.receipt?.status])
const heading = useMemo(() => {
return tx.receipt?.status ? <Trans>Transaction submitted</Trans> : <Trans>Transaction pending</Trans>
}, [tx.receipt?.status])
if (tx.info.type === TransactionType.SWAP) {
return tx.receipt?.status ? <Trans>Swap confirmed</Trans> : <Trans>Swap pending</Trans>
} else if (tx.info.type === TransactionType.WRAP) {
if (tx.info.unwrapped) {
return tx.receipt?.status ? <Trans>Unwrap confirmed</Trans> : <Trans>Unwrap pending</Trans>
}
return tx.receipt?.status ? <Trans>Wrap confirmed</Trans> : <Trans>Wrap pending</Trans>
}
return tx.receipt?.status ? <Trans>Transaction confirmed</Trans> : <Trans>Transaction pending</Trans>
}, [tx.info, tx.receipt?.status])
return (
<Column flex padded gap={0.75} align="stretch" style={{ height: '100%' }}>
<StatusHeader icon={Icon} iconColor={tx.receipt?.status ? 'success' : undefined}>
<ThemedText.Subhead1>{heading}</ThemedText.Subhead1>
<Summary input={tx.info.inputCurrencyAmount} output={tx.info.outputCurrencyAmount} />
{tx.info.type === TransactionType.SWAP ? (
<SwapSummary input={tx.info.inputCurrencyAmount} output={tx.info.outputCurrencyAmount} />
) : null}
</StatusHeader>
<Rule />
<TransactionRow flex>
<ThemedText.ButtonSmall>
<EtherscanLink type={ExplorerDataType.TRANSACTION} data={tx.info.response.hash}>

View File

@@ -3,7 +3,7 @@ import { SupportedChainId } from 'constants/chains'
import { nativeOnChain } from 'constants/tokens'
import { useUpdateAtom } from 'jotai/utils'
import { useSwapInfo } from 'lib/hooks/swap'
import { SwapInfoUpdater } from 'lib/hooks/swap/useSwapInfo'
import { SwapInfoProvider } from 'lib/hooks/swap/useSwapInfo'
import { Field, swapAtom } from 'lib/state/swap'
import { useEffect } from 'react'
import { WrappedTokenInfo } from 'state/lists/wrappedTokenInfo'
@@ -22,8 +22,11 @@ const UNI = (function () {
function Fixture() {
const setState = useUpdateAtom(swapAtom)
const {
allowedSlippage,
[Field.INPUT]: { usdc: inputUSDC },
[Field.OUTPUT]: { usdc: outputUSDC },
trade: { trade },
slippage,
impact,
} = useSwapInfo()
useEffect(() => {
@@ -37,14 +40,22 @@ function Fixture() {
return trade ? (
<Modal color="dialog">
<SummaryDialog onConfirm={() => void 0} trade={trade} allowedSlippage={allowedSlippage} />
<SummaryDialog
onConfirm={async () => void 0}
trade={trade}
slippage={slippage}
inputUSDC={inputUSDC}
outputUSDC={outputUSDC}
impact={impact}
/>
</Modal>
) : null
}
export default (
<>
<SwapInfoUpdater />
<Fixture />
<SwapInfoProvider>
<Fixture />
</SwapInfoProvider>
</>
)

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]};
@@ -26,7 +27,7 @@ interface DetailProps {
function Detail({ label, value, color }: DetailProps) {
return (
<ThemedText.Caption>
<ThemedText.Caption userSelect>
<Row gap={2}>
<span>{label}</span>
<Value color={color}>{value}</Value>
@@ -37,16 +38,16 @@ function Detail({ label, value, color }: DetailProps) {
interface DetailsProps {
trade: Trade<Currency, Currency, TradeType>
allowedSlippage: Percent
slippage: Slippage
impact?: PriceImpact
}
export default function Details({ trade, allowedSlippage }: DetailsProps) {
export default function Details({ trade, slippage, impact }: DetailsProps) {
const { inputAmount, outputAmount } = trade
const inputCurrency = inputAmount.currency
const outputCurrency = outputAmount.currency
const integrator = window.location.hostname
const feeOptions = useAtomValue(feeOptionsAtom)
const priceImpact = useMemo(() => computeRealizedPriceImpact(trade), [trade])
const lpFeeAmount = useMemo(() => computeRealizedLPFeeAmount(trade), [trade])
const { i18n } = useLingui()
@@ -62,7 +63,9 @@ export default function Details({ trade, allowedSlippage }: DetailsProps) {
}
}
rows.push([t`Price impact`, `${priceImpact.toFixed(2)}%`, getPriceImpactWarning(priceImpact)])
if (impact) {
rows.push([t`Price impact`, impact.toString(), impact.warning])
}
if (lpFeeAmount) {
const parsedLpFee = formatCurrencyAmount(lpFeeAmount, 6, i18n.locale)
@@ -70,36 +73,36 @@ export default function Details({ trade, allowedSlippage }: DetailsProps) {
}
if (trade.tradeType === TradeType.EXACT_OUTPUT) {
const localizedMaxSent = formatCurrencyAmount(trade.maximumAmountIn(allowedSlippage), 6, i18n.locale)
const localizedMaxSent = formatCurrencyAmount(trade.maximumAmountIn(slippage.allowed), 6, i18n.locale)
rows.push([t`Maximum sent`, `${localizedMaxSent} ${inputCurrency.symbol}`])
}
if (trade.tradeType === TradeType.EXACT_INPUT) {
const localizedMaxSent = formatCurrencyAmount(trade.minimumAmountOut(allowedSlippage), 6, i18n.locale)
const localizedMaxSent = formatCurrencyAmount(trade.minimumAmountOut(slippage.allowed), 6, i18n.locale)
rows.push([t`Minimum received`, `${localizedMaxSent} ${outputCurrency.symbol}`])
}
rows.push([t`Slippage tolerance`, `${allowedSlippage.toFixed(2)}%`, getSlippageWarning(allowedSlippage)])
rows.push([t`Slippage tolerance`, `${slippage.allowed.toFixed(2)}%`, slippage.warning])
return rows
}, [
feeOptions,
priceImpact,
lpFeeAmount,
trade,
allowedSlippage,
outputAmount,
i18n.locale,
integrator,
outputCurrency,
impact,
inputCurrency,
integrator,
lpFeeAmount,
outputAmount,
outputCurrency,
slippage,
trade,
])
return (
<>
<Column gap={0.5}>
{details.map(([label, detail, color]) => (
<Detail key={label} label={label} value={detail} color={color} />
))}
</>
</Column>
)
}

View File

@@ -1,54 +1,37 @@
import { useLingui } from '@lingui/react'
import { Currency, CurrencyAmount } from '@uniswap/sdk-core'
import { useUSDCValue } from 'hooks/useUSDCPrice'
import { PriceImpact } from 'lib/hooks/useUSDCPriceImpact'
import { ArrowRight } from 'lib/icons'
import styled from 'lib/theme'
import { ThemedText } from 'lib/theme'
import { useMemo } from 'react'
import { computeFiatValuePriceImpact } from 'utils/computeFiatValuePriceImpact'
import { PropsWithChildren } from 'react'
import { formatCurrencyAmount } from 'utils/formatCurrencyAmount'
import Column from '../../Column'
import Row from '../../Row'
import TokenImg from '../../TokenImg'
const Percent = styled.span<{ gain: boolean }>`
color: ${({ gain, theme }) => (gain ? theme.success : theme.error)};
`
interface TokenValueProps {
input: CurrencyAmount<Currency>
usdc?: boolean
change?: number
usdc?: CurrencyAmount<Currency>
}
function TokenValue({ input, usdc, change }: TokenValueProps) {
function TokenValue({ input, usdc, children }: PropsWithChildren<TokenValueProps>) {
const { i18n } = useLingui()
const percent = useMemo(() => {
if (change) {
const percent = change.toPrecision(3)
return change > 0 ? `(+${percent}%)` : `(${percent}%)`
}
return undefined
}, [change])
const usdcAmount = useUSDCValue(input)
return (
<Column justify="flex-start">
<Row gap={0.375} justify="flex-start">
<TokenImg token={input.currency} />
<ThemedText.Body2>
<ThemedText.Body2 userSelect>
{formatCurrencyAmount(input, 6, i18n.locale)} {input.currency.symbol}
</ThemedText.Body2>
</Row>
{usdc && usdcAmount && (
<Row justify="flex-start">
<ThemedText.Caption color="secondary">
${formatCurrencyAmount(usdcAmount, 2, i18n.locale)}
{change && <Percent gain={change > 0}> {percent}</Percent>}
</ThemedText.Caption>
</Row>
{usdc && (
<ThemedText.Caption color="secondary" userSelect>
<Row justify="flex-start" gap={0.25}>
${formatCurrencyAmount(usdc, 6, 'en', 2)}
{children}
</Row>
</ThemedText.Caption>
)}
</Column>
)
@@ -57,23 +40,19 @@ function TokenValue({ input, usdc, change }: TokenValueProps) {
interface SummaryProps {
input: CurrencyAmount<Currency>
output: CurrencyAmount<Currency>
usdc?: boolean
inputUSDC?: CurrencyAmount<Currency>
outputUSDC?: CurrencyAmount<Currency>
impact?: PriceImpact
}
export default function Summary({ input, output, usdc }: SummaryProps) {
const inputUSDCValue = useUSDCValue(input)
const outputUSDCValue = useUSDCValue(output)
const priceImpact = useMemo(() => {
const computedChange = computeFiatValuePriceImpact(inputUSDCValue, outputUSDCValue)
return computedChange ? parseFloat(computedChange.multiply(-1)?.toSignificant(3)) : undefined
}, [inputUSDCValue, outputUSDCValue])
export default function Summary({ input, output, inputUSDC, outputUSDC, impact }: SummaryProps) {
return (
<Row gap={usdc ? 1 : 0.25}>
<TokenValue input={input} usdc={usdc} />
<Row gap={impact ? 1 : 0.25}>
<TokenValue input={input} usdc={inputUSDC} />
<ArrowRight />
<TokenValue input={output} usdc={usdc} change={priceImpact} />
<TokenValue input={output} usdc={outputUSDC}>
{impact && <ThemedText.Caption color={impact.warning}>({impact.toString()})</ThemedText.Caption>}
</TokenValue>
</Row>
)
}

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,60 +129,53 @@ export function SummaryDialog({ trade, allowedSlippage, onConfirm }: SummaryDial
}
}
return
}, [ackPriceImpact, doesTradeDiffer, priceImpact, trade])
}, [ackPriceImpact, doesTradeDiffer, highPriceImpact, isPending, trade])
if (!(inputAmount && outputAmount && inputCurrency && outputCurrency)) {
return null
}
return (
<ActionButton onClick={onClick} action={action}>
<Trans>Confirm swap</Trans>
</ActionButton>
)
}
interface SummaryDialogProps {
trade: Trade<Currency, Currency, TradeType>
slippage: Slippage
inputUSDC?: CurrencyAmount<Currency>
outputUSDC?: CurrencyAmount<Currency>
impact?: PriceImpact
onConfirm: () => Promise<void>
}
export function SummaryDialog({ trade, slippage, inputUSDC, outputUSDC, impact, onConfirm }: SummaryDialogProps) {
const { inputAmount, outputAmount } = trade
const [open, setOpen] = useState(false)
const onExpand = useCallback(() => setOpen((open) => !open), [])
return (
<>
<Header title={<Trans>Swap summary</Trans>} ruled />
<Body flex align="stretch" gap={0.75} padded open={open}>
<SummaryColumn gap={0.75} flex justify="center">
<Summary input={inputAmount} output={outputAmount} usdc={true} />
<ThemedText.Caption>
{formatLocaleNumber({ number: 1, sigFigs: 1, locale: i18n.locale })} {inputCurrency.symbol} ={' '}
{formatPrice(executionPrice, 6, i18n.locale)} {outputCurrency.symbol}
</ThemedText.Caption>
</SummaryColumn>
<Rule />
<Row>
<Row gap={0.5}>
{warning ? <AlertTriangle color={warning} /> : <Info color="secondary" />}
<ThemedText.Subhead2 color="secondary">
<Trans>Swap details</Trans>
</ThemedText.Subhead2>
</Row>
<IconButton color="secondary" onClick={() => setOpen(!open)} icon={Expando} iconProps={{ open }} />
</Row>
<ExpandoColumn flex align="stretch">
<Rule />
<DetailsColumn>
<Column gap={0.5} ref={setDetails} css={scrollbar}>
<Details trade={trade} allowedSlippage={allowedSlippage} />
</Column>
</DetailsColumn>
<Estimate color="secondary">
<Trans>Output is estimated.</Trans>{' '}
{tradeType === TradeType.EXACT_INPUT && (
<Trans>
You will receive at least{' '}
{formatCurrencyAmount(trade.minimumAmountOut(allowedSlippage), 6, i18n.locale)} {outputCurrency.symbol}{' '}
or the transaction will revert.
</Trans>
)}
{tradeType === TradeType.EXACT_OUTPUT && (
<Trans>
You will send at most {formatCurrencyAmount(trade.maximumAmountIn(allowedSlippage), 6, i18n.locale)}{' '}
{inputCurrency.symbol} or the transaction will revert.
</Trans>
)}
</Estimate>
<ActionButton onClick={onConfirm} action={action}>
<Trans>Confirm swap</Trans>
</ActionButton>
</ExpandoColumn>
<Body flex align="stretch" padded gap={0.75} open={open}>
<Heading gap={0.75} flex justify="center">
<Summary
input={inputAmount}
output={outputAmount}
inputUSDC={inputUSDC}
outputUSDC={outputUSDC}
impact={impact}
/>
<Price trade={trade} />
</Heading>
<Column gap={open ? 0 : 0.75} style={{ transition: 'gap 0.25s' }}>
<Expando title={<Subhead impact={impact} slippage={slippage} />} open={open} onExpand={onExpand} height={7}>
<Details trade={trade} slippage={slippage} impact={impact} />
</Expando>
<Footing>
<Estimate trade={trade} slippage={slippage} />
</Footing>
<ConfirmButton trade={trade} highPriceImpact={impact?.warning === 'error'} onConfirm={onConfirm} />
</Column>
</Body>
</>
)

View File

@@ -1,5 +1,4 @@
import { tokens } from '@uniswap/default-token-list'
import { DAI, USDC } from 'constants/tokens'
import { DAI, USDC_MAINNET } from 'constants/tokens'
import { useUpdateAtom } from 'jotai/utils'
import { useEffect } from 'react'
import { useSelect, useValue } from 'react-cosmos/fixture'
@@ -37,40 +36,35 @@ function Fixture() {
defaultValue: FEE_RECIPIENT_OPTIONS[1],
})
const optionsToAddressMap: Record<string, string> = {
none: '',
const optionsToAddressMap: Record<string, string | undefined> = {
None: undefined,
Native: 'NATIVE',
DAI: DAI.address,
USDC: USDC.address,
USDC: USDC_MAINNET.address,
}
const addressOptions = Object.keys(optionsToAddressMap)
const [defaultInput] = useSelect('defaultInputAddress', {
options: addressOptions,
defaultValue: addressOptions[2],
})
const inputOptions = ['', '0', '100', '-1']
const [defaultInputAmount] = useSelect('defaultInputAmount', {
options: inputOptions,
defaultValue: inputOptions[2],
})
const [defaultOutput] = useSelect('defaultOutputAddress', {
const [defaultInputToken] = useSelect('defaultInputToken', {
options: addressOptions,
defaultValue: addressOptions[1],
})
const [defaultOutputAmount] = useSelect('defaultOutputAmount', {
options: inputOptions,
defaultValue: inputOptions[0],
const [defaultInputAmount] = useValue('defaultInputAmount', { defaultValue: 1 })
const [defaultOutputToken] = useSelect('defaultOutputToken', {
options: addressOptions,
defaultValue: addressOptions[2],
})
const [defaultOutputAmount] = useValue('defaultOutputAmount', { defaultValue: 0 })
return (
<Swap
convenienceFee={convenienceFee}
convenienceFeeRecipient={convenienceFeeRecipient}
defaultInputAddress={optionsToAddressMap[defaultInput]}
defaultInputTokenAddress={optionsToAddressMap[defaultInputToken]}
defaultInputAmount={defaultInputAmount}
defaultOutputAddress={optionsToAddressMap[defaultOutput]}
defaultOutputTokenAddress={optionsToAddressMap[defaultOutputToken]}
defaultOutputAmount={defaultOutputAmount}
tokenList={tokens}
onConnectWallet={() => console.log('onConnectWallet')} // this handler is included as a test of functionality, but only logs
/>
)
}

View File

@@ -1,181 +0,0 @@
import { Trans } from '@lingui/macro'
import { Token } from '@uniswap/sdk-core'
import { useERC20PermitFromTrade } from 'hooks/useERC20Permit'
import { useUpdateAtom } from 'jotai/utils'
import { useSwapCurrencyAmount, useSwapInfo, useSwapTradeType } from 'lib/hooks/swap'
import useSwapApproval, {
ApprovalState,
useSwapApprovalOptimizedTrade,
useSwapRouterAddress,
} from 'lib/hooks/swap/useSwapApproval'
import { useSwapCallback } from 'lib/hooks/swap/useSwapCallback'
import { useAddTransaction } from 'lib/hooks/transactions'
import { usePendingApproval } from 'lib/hooks/transactions'
import useActiveWeb3React from 'lib/hooks/useActiveWeb3React'
import useTransactionDeadline from 'lib/hooks/useTransactionDeadline'
import { Spinner } from 'lib/icons'
import { displayTxHashAtom, Field } from 'lib/state/swap'
import { TransactionType } from 'lib/state/transactions'
import { useTheme } from 'lib/theme'
import { useCallback, useEffect, useMemo, useState } from 'react'
import invariant from 'tiny-invariant'
import { ExplorerDataType } from 'utils/getExplorerLink'
import ActionButton, { ActionButtonProps } from '../ActionButton'
import Dialog from '../Dialog'
import EtherscanLink from '../EtherscanLink'
import { SummaryDialog } from './Summary'
interface SwapButtonProps {
disabled?: boolean
}
function useIsPendingApproval(token?: Token, spender?: string): boolean {
return Boolean(usePendingApproval(token, spender))
}
export default function SwapButton({ disabled }: SwapButtonProps) {
const { account, chainId } = useActiveWeb3React()
const { tokenColorExtraction } = useTheme()
const {
trade,
allowedSlippage,
currencies: { [Field.INPUT]: inputCurrency },
currencyBalances: { [Field.INPUT]: inputCurrencyBalance },
currencyAmounts: { [Field.INPUT]: inputCurrencyAmount, [Field.OUTPUT]: outputCurrencyAmount },
feeOptions,
} = useSwapInfo()
const tradeType = useSwapTradeType()
const [activeTrade, setActiveTrade] = useState<typeof trade.trade | undefined>()
useEffect(() => {
setActiveTrade((activeTrade) => activeTrade && trade.trade)
}, [trade])
// TODO(zzmp): Return an optimized trade directly from useSwapInfo.
const optimizedTrade =
// Use trade.trade if there is no swap optimized trade. This occurs if approvals are still pending.
useSwapApprovalOptimizedTrade(trade.trade, allowedSlippage, useIsPendingApproval) || trade.trade
const approvalCurrencyAmount = useSwapCurrencyAmount(Field.INPUT)
const [approval, getApproval] = useSwapApproval(
optimizedTrade,
allowedSlippage,
useIsPendingApproval,
approvalCurrencyAmount
)
const approvalHash = usePendingApproval(
inputCurrency?.isToken ? inputCurrency : undefined,
useSwapRouterAddress(optimizedTrade)
)
const addTransaction = useAddTransaction()
const addApprovalTransaction = useCallback(() => {
getApproval().then((transaction) => {
if (transaction) {
addTransaction({ type: TransactionType.APPROVAL, ...transaction })
}
})
}, [addTransaction, getApproval])
const actionProps = useMemo((): Partial<ActionButtonProps> | undefined => {
if (!disabled && chainId) {
if (approval === ApprovalState.NOT_APPROVED) {
const currency = inputCurrency || approvalCurrencyAmount?.currency
invariant(currency)
return {
action: {
message: <Trans>Approve {currency.symbol} first</Trans>,
onClick: addApprovalTransaction,
children: <Trans>Approve</Trans>,
},
}
} else if (approval === ApprovalState.PENDING) {
return {
disabled: true,
action: {
message: (
<EtherscanLink type={ExplorerDataType.TRANSACTION} data={approvalHash}>
<Trans>Approval pending</Trans>
</EtherscanLink>
),
icon: Spinner,
onClick: addApprovalTransaction,
children: <Trans>Approve</Trans>,
},
}
} else if (inputCurrencyAmount && inputCurrencyBalance && !inputCurrencyBalance.lessThan(inputCurrencyAmount)) {
return {}
}
}
return { disabled: true }
}, [
addApprovalTransaction,
approval,
approvalCurrencyAmount?.currency,
approvalHash,
chainId,
disabled,
inputCurrency,
inputCurrencyAmount,
inputCurrencyBalance,
])
const deadline = useTransactionDeadline()
const { signatureData } = useERC20PermitFromTrade(optimizedTrade, allowedSlippage, deadline)
// the callback to execute the swap
const { callback: swapCallback } = useSwapCallback({
trade: optimizedTrade,
allowedSlippage,
recipientAddressOrName: account ?? null,
signatureData,
deadline,
feeOptions,
})
//@TODO(ianlapham): add a loading state, process errors
const setDisplayTxHash = useUpdateAtom(displayTxHashAtom)
const onConfirm = useCallback(() => {
swapCallback?.()
.then((response) => {
setDisplayTxHash(response.hash)
invariant(inputCurrencyAmount && outputCurrencyAmount)
addTransaction({
response,
type: TransactionType.SWAP,
tradeType,
inputCurrencyAmount,
outputCurrencyAmount,
})
})
.catch((error) => {
//@TODO(ianlapham): add error handling
console.log(error)
})
.finally(() => {
setActiveTrade(undefined)
})
}, [addTransaction, inputCurrencyAmount, outputCurrencyAmount, setDisplayTxHash, swapCallback, tradeType])
return (
<>
<ActionButton
color={tokenColorExtraction ? 'interactive' : 'accent'}
onClick={() => setActiveTrade(trade.trade)}
{...actionProps}
>
<Trans>Review swap</Trans>
</ActionButton>
{activeTrade && (
<Dialog color="dialog" onClose={() => setActiveTrade(undefined)}>
<SummaryDialog trade={activeTrade} allowedSlippage={allowedSlippage} onConfirm={onConfirm} />
</Dialog>
)}
</>
)
}

View File

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

View File

@@ -0,0 +1,88 @@
import { Trans } from '@lingui/macro'
import { Currency, CurrencyAmount, Token } from '@uniswap/sdk-core'
import { Action } from 'lib/components/ActionButton'
import EtherscanLink from 'lib/components/EtherscanLink'
import {
ApproveOrPermitState,
useApproveOrPermit,
useSwapApprovalOptimizedTrade,
useSwapRouterAddress,
} from 'lib/hooks/swap/useSwapApproval'
import { useAddTransaction, usePendingApproval } from 'lib/hooks/transactions'
import { Slippage } from 'lib/hooks/useSlippage'
import { Spinner } from 'lib/icons'
import { TransactionType } from 'lib/state/transactions'
import { useCallback, useEffect, useMemo, useState } from 'react'
import { ExplorerDataType } from 'utils/getExplorerLink'
export function useIsPendingApproval(token?: Token, spender?: string): boolean {
return Boolean(usePendingApproval(token, spender))
}
export default function useApprovalData(
trade: ReturnType<typeof useSwapApprovalOptimizedTrade>,
slippage: Slippage,
currencyAmount?: CurrencyAmount<Currency>
) {
const currency = currencyAmount?.currency
const { approvalState, signatureData, handleApproveOrPermit } = useApproveOrPermit(
trade,
slippage.allowed,
useIsPendingApproval,
currencyAmount
)
const [isPending, setIsPending] = useState(false)
const addTransaction = useAddTransaction()
const onApprove = useCallback(async () => {
setIsPending(true)
const transaction = await handleApproveOrPermit()
if (transaction) {
addTransaction({ type: TransactionType.APPROVAL, ...transaction })
}
setIsPending(false)
}, [addTransaction, handleApproveOrPermit])
// Reset the pending state if currency changes.
useEffect(() => setIsPending(false), [currency])
const approvalHash = usePendingApproval(currency?.isToken ? currency : undefined, useSwapRouterAddress(trade))
const approvalAction = useMemo((): Action | undefined => {
if (!trade || !currency) return
switch (approvalState) {
case ApproveOrPermitState.REQUIRES_APPROVAL:
if (isPending) {
return { message: <Trans>Approve in your wallet</Trans>, icon: Spinner }
}
return {
message: <Trans>Approve {currency.symbol} first</Trans>,
onClick: onApprove,
children: <Trans>Approve</Trans>,
}
case ApproveOrPermitState.REQUIRES_SIGNATURE:
if (isPending) {
return { message: <Trans>Allow in your wallet</Trans>, icon: Spinner }
}
return {
message: <Trans>Allow {currency.symbol} first</Trans>,
onClick: onApprove,
children: <Trans>Allow</Trans>,
}
case ApproveOrPermitState.PENDING_APPROVAL:
return {
message: (
<EtherscanLink type={ExplorerDataType.TRANSACTION} data={approvalHash}>
<Trans>Approval pending</Trans>
</EtherscanLink>
),
icon: Spinner,
}
case ApproveOrPermitState.PENDING_SIGNATURE:
return { message: <Trans>Allowance pending</Trans>, icon: Spinner }
case ApproveOrPermitState.APPROVED:
return
}
}, [approvalHash, approvalState, currency, isPending, onApprove, trade])
return { approvalAction, signatureData: signatureData ?? undefined }
}

View File

@@ -2,7 +2,7 @@ import 'setimmediate'
import { Trans } from '@lingui/macro'
import { Currency } from '@uniswap/sdk-core'
import { loadingOpacityCss } from 'lib/css/loading'
import { loadingTransitionCss } from 'lib/css/loading'
import styled, { keyframes, ThemedText } from 'lib/theme'
import { ReactNode, useCallback, useEffect, useMemo, useRef, useState } from 'react'
@@ -16,9 +16,10 @@ const TokenInputRow = styled(Row)`
grid-template-columns: 1fr;
`
const ValueInput = styled(DecimalInput)<{ $loading: boolean }>`
const ValueInput = styled(DecimalInput)`
color: ${({ theme }) => theme.primary};
height: 1em;
height: 1.5em;
margin: -0.25em 0;
:hover:not(:focus-within) {
color: ${({ theme }) => theme.onHover(theme.primary)};
@@ -28,7 +29,7 @@ const ValueInput = styled(DecimalInput)<{ $loading: boolean }>`
color: ${({ theme }) => theme.onHover(theme.secondary)};
}
${loadingOpacityCss}
${loadingTransitionCss}
`
const delayedFadeIn = keyframes`
@@ -108,7 +109,7 @@ export default function TokenInput({
onFocus={() => setShowMax(hasMax)}
onChange={onChangeInput}
disabled={disabled || !currency}
$loading={Boolean(loading)}
isLoading={Boolean(loading)}
ref={input}
></ValueInput>
</ThemedText.H2>

View File

@@ -1,16 +1,23 @@
import { Trans } from '@lingui/macro'
import { Currency, TradeType } from '@uniswap/sdk-core'
import useUSDCPrice from 'hooks/useUSDCPrice'
import { Currency, CurrencyAmount, TradeType } from '@uniswap/sdk-core'
import Column from 'lib/components/Column'
import Rule from 'lib/components/Rule'
import Tooltip from 'lib/components/Tooltip'
import { AlertTriangle, Icon, Info, Spinner } from 'lib/icons'
import { ThemedText } from 'lib/theme'
import { ReactNode, useMemo, useState } from 'react'
import { loadingCss } from 'lib/css/loading'
import { PriceImpact } from 'lib/hooks/useUSDCPriceImpact'
import { AlertTriangle, Icon, Info, InlineSpinner } from 'lib/icons'
import styled, { ThemedText } from 'lib/theme'
import { ReactNode, useCallback } from 'react'
import { InterfaceTrade } from 'state/routing/types'
import { TextButton } from '../../Button'
import Row from '../../Row'
import Price from '../Price'
import RoutingDiagram from '../RoutingDiagram'
const Loading = styled.span`
color: ${({ theme }) => theme.secondary};
${loadingCss};
`
interface CaptionProps {
icon?: Icon
caption: ReactNode
@@ -25,60 +32,95 @@ function Caption({ icon: Icon = AlertTriangle, caption }: CaptionProps) {
)
}
export function Connecting() {
return (
<Caption
icon={InlineSpinner}
caption={
<Loading>
<Trans>Connecting</Trans>
</Loading>
}
/>
)
}
export function ConnectWallet() {
return <Caption caption={<Trans>Connect wallet to swap</Trans>} />
}
export function UnsupportedNetwork() {
return <Caption caption={<Trans>Unsupported network - switch to another to trade.</Trans>} />
return <Caption caption={<Trans>Unsupported network - switch to another to trade</Trans>} />
}
export function InsufficientBalance({ currency }: { currency: Currency }) {
return <Caption caption={<Trans>Insufficient {currency?.symbol} balance</Trans>} />
}
export function InsufficientLiquidity() {
return <Caption caption={<Trans>Insufficient liquidity in the pool for your trade</Trans>} />
}
export function Error() {
return <Caption caption={<Trans>Error fetching trade</Trans>} />
}
export function Empty() {
return <Caption icon={Info} caption={<Trans>Enter an amount</Trans>} />
}
export function LoadingTrade() {
return <Caption icon={Spinner} caption={<Trans>Fetching best price</Trans>} />
return (
<Caption
icon={InlineSpinner}
caption={
<Loading>
<Trans>Fetching best price</Trans>
</Loading>
}
/>
)
}
export function Trade({ trade }: { trade: InterfaceTrade<Currency, Currency, TradeType> }) {
const [flip, setFlip] = useState(true)
const { inputAmount, outputAmount, executionPrice } = trade
const fiatValueInput = useUSDCPrice(inputAmount.currency)
const fiatValueOutput = useUSDCPrice(outputAmount.currency)
export function WrapCurrency({ inputCurrency, outputCurrency }: { inputCurrency: Currency; outputCurrency: Currency }) {
const Text = useCallback(
() => (
<Trans>
Convert {inputCurrency.symbol} to {outputCurrency.symbol} with no slippage
</Trans>
),
[inputCurrency.symbol, outputCurrency.symbol]
)
const ratio = useMemo(() => {
const [a, b] = flip ? [outputAmount, inputAmount] : [inputAmount, outputAmount]
const priceString = (!flip ? executionPrice : executionPrice?.invert())?.toSignificant(6)
const ratio = `1 ${a.currency.symbol} = ${priceString} ${b.currency.symbol}`
const usdc = !flip
? fiatValueInput
? ` ($${fiatValueInput.toSignificant(6)})`
: null
: fiatValueOutput
? ` ($${fiatValueOutput.toSignificant(6)})`
: null
return (
<Row gap={0.25} style={{ userSelect: 'text' }}>
{ratio}
{usdc && <ThemedText.Caption color="secondary">{usdc}</ThemedText.Caption>}
</Row>
)
}, [executionPrice, fiatValueInput, fiatValueOutput, flip, inputAmount, outputAmount])
return <Caption icon={Info} caption={<Text />} />
}
export function Trade({
trade,
outputUSDC,
impact,
}: {
trade: InterfaceTrade<Currency, Currency, TradeType>
outputUSDC?: CurrencyAmount<Currency>
impact?: PriceImpact
}) {
return (
<>
<Tooltip placement="bottom" icon={Info}>
<RoutingDiagram trade={trade} />
<Tooltip placement="bottom" icon={impact?.warning ? AlertTriangle : Info}>
<Column gap={0.75}>
{impact?.warning && (
<>
<ThemedText.Caption>
The output amount is estimated at {impact.toString()} less than the input amount due to high price
impact
</ThemedText.Caption>
<Rule />
</>
)}
<RoutingDiagram trade={trade} />
</Column>
</Tooltip>
<TextButton color="primary" onClick={() => setFlip(!flip)}>
{ratio}
</TextButton>
<Price trade={trade} outputUSDC={outputUSDC} />
</>
)
}

View File

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

View File

@@ -1,17 +1,15 @@
import { Trans } from '@lingui/macro'
import { TokenInfo } from '@uniswap/token-lists'
import { ALL_SUPPORTED_CHAIN_IDS } from 'constants/chains'
import { useAtom } from 'jotai'
import { SwapInfoUpdater } from 'lib/hooks/swap/useSwapInfo'
import useSyncConvenienceFee from 'lib/hooks/swap/useSyncConvenienceFee'
import useSyncSwapDefaults from 'lib/hooks/swap/useSyncSwapDefaults'
import { SwapInfoProvider } from 'lib/hooks/swap/useSwapInfo'
import useSyncConvenienceFee, { FeeOptions } from 'lib/hooks/swap/useSyncConvenienceFee'
import useSyncTokenDefaults, { TokenDefaults } from 'lib/hooks/swap/useSyncTokenDefaults'
import { usePendingTransactions } from 'lib/hooks/transactions'
import useActiveWeb3React from 'lib/hooks/useActiveWeb3React'
import useHasFocus from 'lib/hooks/useHasFocus'
import useTokenList, { useSyncTokenList } from 'lib/hooks/useTokenList'
import useOnSupportedNetwork from 'lib/hooks/useOnSupportedNetwork'
import { displayTxHashAtom } from 'lib/state/swap'
import { SwapTransactionInfo, Transaction, TransactionType } from 'lib/state/transactions'
import { useMemo, useState } from 'react'
import { SwapTransactionInfo, Transaction, TransactionType, WrapTransactionInfo } from 'lib/state/transactions'
import { useState } from 'react'
import Dialog from '../Dialog'
import Header from '../Header'
@@ -23,67 +21,62 @@ import ReverseButton from './ReverseButton'
import Settings from './Settings'
import { StatusDialog } from './Status'
import SwapButton from './SwapButton'
import SwapPropValidator from './SwapPropValidator'
import Toolbar from './Toolbar'
import useValidate from './useValidate'
export type DefaultAddress = string | { [chainId: number]: string | 'NATIVE' } | 'NATIVE'
function getSwapTx(txs: { [hash: string]: Transaction }, hash?: string): Transaction<SwapTransactionInfo> | undefined {
function getTransactionFromMap(
txs: { [hash: string]: Transaction },
hash?: string
): Transaction<SwapTransactionInfo | WrapTransactionInfo> | undefined {
if (hash) {
const tx = txs[hash]
if (tx?.info?.type === TransactionType.SWAP) {
return tx as Transaction<SwapTransactionInfo>
}
if (tx?.info?.type === TransactionType.WRAP) {
return tx as Transaction<WrapTransactionInfo>
}
}
return
}
export interface SwapProps {
tokenList?: string | TokenInfo[]
defaultInputAddress?: DefaultAddress
defaultInputAmount?: string
defaultOutputAddress?: DefaultAddress
defaultOutputAmount?: string
convenienceFee?: number
convenienceFeeRecipient?: string | { [chainId: number]: string }
export interface SwapProps extends TokenDefaults, FeeOptions {
onConnectWallet?: () => void
}
export default function Swap(props: SwapProps) {
useSyncTokenList(props.tokenList)
useSyncSwapDefaults(props)
useValidate(props)
useSyncConvenienceFee(props)
useSyncTokenDefaults(props)
const { active, account, chainId } = useActiveWeb3React()
const { active, account } = useActiveWeb3React()
const [wrapper, setWrapper] = useState<HTMLDivElement | null>(null)
const [displayTxHash, setDisplayTxHash] = useAtom(displayTxHashAtom)
const pendingTxs = usePendingTransactions()
const displayTx = getSwapTx(pendingTxs, displayTxHash)
const displayTx = getTransactionFromMap(pendingTxs, displayTxHash)
const tokenList = useTokenList()
const isSwapSupported = useMemo(
() => Boolean(chainId && ALL_SUPPORTED_CHAIN_IDS.includes(chainId) && tokenList?.length),
[chainId, tokenList]
)
const onSupportedNetwork = useOnSupportedNetwork()
const isDisabled = !(active && onSupportedNetwork)
const focused = useHasFocus(wrapper)
return (
<SwapPropValidator {...props}>
{isSwapSupported && <SwapInfoUpdater />}
<>
<Header title={<Trans>Swap</Trans>}>
{active && <Wallet disabled={!account} onClick={props.onConnectWallet} />}
<Settings disabled={!active} />
<Settings disabled={isDisabled} />
</Header>
<div ref={setWrapper}>
<BoundaryProvider value={wrapper}>
<Input disabled={!active} focused={focused} />
<ReverseButton disabled={!active} />
<Output disabled={!active} focused={focused}>
<Toolbar disabled={!active} />
<SwapButton disabled={!account} />
</Output>
<SwapInfoProvider disabled={isDisabled}>
<Input disabled={isDisabled} focused={focused} />
<ReverseButton disabled={isDisabled} />
<Output disabled={isDisabled} focused={focused}>
<Toolbar />
<SwapButton disabled={isDisabled} />
</Output>
</SwapInfoProvider>
</BoundaryProvider>
</div>
{displayTx && (
@@ -91,6 +84,6 @@ export default function Swap(props: SwapProps) {
<StatusDialog tx={displayTx} onClose={() => setDisplayTxHash()} />
</Dialog>
)}
</SwapPropValidator>
</>
)
}

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