Compare commits

...

159 Commits

Author SHA1 Message Date
Moody Salem
b8a9191653 fix: turn service workers back on (#1944)
reconfigure service workers to only cache used assets (excluding .po language files and non-.var.woff2 font files)

* fix: turn service workers back on

* chore: configure service worker caches

* chore: add newline

* Fix code style issues with ESLint

* chore: limit service-worker caching

Co-authored-by: Zach Pomerantz <zzmp@uniswap.org>
Co-authored-by: Lint Action <lint-action@samuelmeuli.com>
2021-08-25 12:46:56 -07:00
Moody Salem
1304acd8c7 fix: walletconnect on L2 issues with Rainbow (#2242)
* fix: remove the walletconnect bridge url

* fix: use the latest ethereum provider which fixes the optimism rainbow issue
2021-08-24 12:01:53 -05:00
Zach Pomerantz
8112e48e9a chore: rm redux-devtools-extension (#2230)
redux-devtools-extension integration is already provided through @reduxjs/toolkit.
2021-08-23 10:17:24 -05:00
Zach Pomerantz
f3c400c514 chore: remove unused cross-env (#2232) 2021-08-23 10:15:54 -05:00
Zach Pomerantz
c2c7d87ca3 chore: update multicodec usage (#2233) 2021-08-23 10:15:43 -05:00
Zach Pomerantz
db18b486b4 chore: rm unused styled-system (#2234) 2021-08-23 10:13:57 -05:00
Noah Zinsmeister
1613cc473b fix: proposal formatting 2021-08-20 16:57:45 -04:00
Noah Zinsmeister
e1f37b0ec6 fix(governance): hot fix for parseLog crash 2021-08-20 00:04:06 -04:00
Zach Pomerantz
805af2fdb4 chore(accessibility): increase contrast for connections (#2223) 2021-08-19 16:53:06 -04:00
Zach Pomerantz
b833d07b4f chore: change language icon to globe (#2222)
* chore: change language icon to globe
2021-08-19 16:44:28 -04:00
Moody Salem
ec5da7ec8f fix: update walletconnect for deep links and scalability (#2215) 2021-08-18 12:26:00 -05:00
Crowdin Bot
ff10346b06 chore(i18n): synchronize translations from crowdin [skip ci] 2021-08-17 11:04:45 +00:00
Crowdin Bot
f15522a47c chore(i18n): synchronize translations from crowdin [skip ci] 2021-08-17 00:09:22 +00:00
Crowdin Bot
67b66235ba chore(i18n): synchronize translations from crowdin [skip ci] 2021-08-16 23:05:26 +00:00
Zach Pomerantz
7153908935 chore: fix SEO and accessibility nits for Lighthouse (#2212)
* refactor: index.html

* chore: preconnect to GA

* chore(SEO): add meta content

* chore(SEO): add alt to ethereum logo

* chore(accessibility): add aria-labels to menus

* chore(accessibility): mark AppBody as main

* chore(accessibility): update nav link ids

* chore(accessibility): set html.lang to match i18n locale

* chore(refactor): mv html.lang to useEffect
2021-08-16 18:30:52 -04:00
Crowdin Bot
4541e37388 chore(i18n): synchronize translations from crowdin [skip ci] 2021-08-16 13:18:27 +00:00
Crowdin Bot
3bbcdcb8ae chore(i18n): synchronize translations from crowdin [skip ci] 2021-08-13 22:05:18 +00:00
Zach Pomerantz
2daee152ef chore(i18n): do not compile i18n messages (#2211) 2021-08-13 14:03:14 -07:00
Zach Pomerantz
9af7e0e0c3 chore(i18n): touch en-US to prevent lingui from adding a creation date (#2210)
Fixes #2198. Touching en-US.po before extracting messages prevents lingui from adding a creation date, maintaining deterministic builds and IPFS hashes.
2021-08-13 14:02:59 -07:00
Noah Zinsmeister
757741c3df fix: use @walletconnect/ethereum-provider instead of @walletconnect/web3-provider (#2175)
* use @walletconnect/ethereum-provider

* bump to support client-side disconnect
2021-08-13 10:01:56 -05:00
Moody Salem
a3aa6647e7 chore: set default_bump to false per https://github.com/mathieudutour/github-tag-action/issues/85 2021-08-11 10:19:37 -05:00
Moody Salem
f463df97bf fix: do not construct routes with the same pool twice (#2196) 2021-08-11 10:15:53 -05:00
Ian Lapham
99a282b7b4 update blocked address list (#2182)
Co-authored-by: Ian Lapham <ianlapham@Ians-MacBook-Pro.local>
2021-08-10 10:30:54 -04:00
Moody Salem
5a3165358f fix: #isTradeBetter should check that output currencies are equal (#2177)
* fix: isTradeBetter should check that input/output are equal

* fix the fix
2021-08-10 09:25:43 -05:00
Moody Salem
26e334c78f fix: proposal descriptions should be pulled from the right governor (#2176) 2021-08-10 08:53:22 -05:00
Justin Domingue
bd8192c81e update chinese simplified and traditional display names (#2180) 2021-08-10 09:05:17 -04:00
Justin Domingue
ff6bd38992 add ReactGA event for full range (#2181) 2021-08-10 09:05:05 -04:00
Justin Domingue
a48b7ce702 fix: avoid running BestV3Trade when tokens are the same (#2172)
* avoid running BestV3Trade when tokens are the same

* flipped condition
2021-08-09 10:38:23 -04:00
Crowdin Bot
fc08d1fc6a chore(i18n): synchronize translations from crowdin [skip ci] 2021-08-06 20:05:37 +00:00
Ian Lapham
b416603ff4 update unsupported list (#2143)
Co-authored-by: Ian Lapham <ianlapham@Ians-MacBook-Pro.local>
2021-08-05 20:52:17 -04:00
Justin Domingue
c4dc26e1b2 fix: rename weth9 to weth (#2133)
* update @uniswap/sdk-core and @uniswap/v3-sdk

* reorder
2021-08-05 14:43:02 -04:00
Justin Domingue
adfe7225a8 upgrade to walletconnect 1.5.2 (#2134) 2021-08-04 08:49:46 -04:00
dependabot[bot]
13e347ee02 chore(deps): bump tar from 6.1.0 to 6.1.4 (#2132)
Bumps [tar](https://github.com/npm/node-tar) from 6.1.0 to 6.1.4.
- [Release notes](https://github.com/npm/node-tar/releases)
- [Changelog](https://github.com/npm/node-tar/blob/main/CHANGELOG.md)
- [Commits](https://github.com/npm/node-tar/compare/v6.1.0...v6.1.4)

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-08-04 08:40:22 -04:00
Crowdin Bot
b527bfbeea chore(i18n): synchronize translations from crowdin [skip ci] 2021-08-03 12:05:13 +00:00
Crowdin Bot
a25df00ef6 chore(i18n): synchronize translations from crowdin [skip ci] 2021-08-03 08:05:04 +00:00
Crowdin Bot
2446a6e88b chore(i18n): synchronize translations from crowdin [skip ci] 2021-08-03 07:04:40 +00:00
Crowdin Bot
3b3bf14e8a chore(i18n): synchronize translations from crowdin [skip ci] 2021-08-02 19:04:33 +00:00
Crowdin Bot
017e79f7ae chore(i18n): synchronize translations from crowdin [skip ci] 2021-08-02 17:09:09 +00:00
Crowdin Bot
804fe8f5ee chore(i18n): synchronize translations from crowdin [skip ci] 2021-08-02 15:05:55 +00:00
Jordan Frankfurt
bf01b0d342 fix: optimism link and some copy changes (#2126)
* fix: optimism link

* fix: (un)planned downtime copy changes
2021-08-02 10:50:46 -04:00
Crowdin Bot
f46f73f35f chore(i18n): synchronize translations from crowdin [skip ci] 2021-08-02 06:05:04 +00:00
Crowdin Bot
cad3575247 chore(i18n): synchronize translations from crowdin [skip ci] 2021-08-02 03:10:20 +00:00
Crowdin Bot
17aa9fcdb0 chore(i18n): synchronize translations from crowdin [skip ci] 2021-07-31 12:05:49 +00:00
Crowdin Bot
087745d7c6 chore(i18n): synchronize translations from crowdin [skip ci] 2021-07-31 08:05:54 +00:00
Lint Action
20e6ca6fd5 Fix code style issues with ESLint 2021-07-30 22:06:48 +00:00
Justin Domingue
d51b7e779b update full range documentation link 2021-07-30 15:05:43 -07:00
Matthew Quinn
cfd0412d78 fix: center import pool text V2 (#2122)
* fix: center import pool text V2

* fix: updated based on feedback

Co-authored-by: Matthew Quinn <matthew.quinn@libertymutual.com>
2021-07-30 14:39:46 -04:00
Matthew Quinn
50afef03cb fix: update mobile header to fix overflow (#2125)
Co-authored-by: Matthew Quinn <matthew.quinn@libertymutual.com>
2021-07-30 14:38:40 -04:00
Crowdin Bot
43b6e7abf4 chore(i18n): synchronize translations from crowdin [skip ci] 2021-07-30 05:04:40 +00:00
Moody Salem
64b9df8710 fix: upgrade ethers to allow constructing transaction data compatible with eip-1559 (#2121)
* fix: upgrade ethers to allow constructing transaction data compatible with eip-1559

* bump the experimental provider
2021-07-29 17:46:35 -05:00
Crowdin Bot
d75271484a chore(i18n): synchronize translations from crowdin [skip ci] 2021-07-29 21:04:53 +00:00
Justin Domingue
952cc98df3 fix: add support for full range positions in add liquidity (#2090)
* remove arbitrary range buttons and move full range button

* better align full range to deposit

* support dragging range when in full range

* hack to support full range brushing

* restore zoom levels

* adjusted for mocks

* fix styling

* simplify type

* restore rate toggle change

* fix lower bound by looking at isSorted

* better align bottoms

* add reset button for full range positions

* only flip when not at limit

* fix +/- buttons in range selector

* add help link
2021-07-29 13:47:03 -07:00
Crowdin Bot
9b7637e012 chore(i18n): synchronize translations from crowdin [skip ci] 2021-07-29 19:04:45 +00:00
Jordan Frankfurt
a7f599127b add link (#2113) 2021-07-29 14:16:47 -04:00
Jordan Frankfurt
676890d89c fix(L2): update downtime warning copy (#2114) 2021-07-29 14:16:09 -04:00
Lint Action
beb1bf3bdc Fix code style issues with ESLint 2021-07-29 17:26:43 +00:00
Justin Domingue
631c202c49 document useQueryCacheInvalidator
explains why `chainId` is pulled directly from the store.
2021-07-29 10:25:29 -07:00
Moody Salem
491c9b4fd3 fix: upgrade walletconnect to 1.5.1 (#2120)
* upgrade walletconnect to 1.5.0

* bump again
2021-07-28 13:48:10 -05:00
Crowdin Bot
dcaed9094e chore(i18n): synchronize translations from crowdin [skip ci] 2021-07-28 11:04:43 +00:00
Jeffrey Lin
8996ba6e38 fix: toggle liquidity position when toggling rates (#2117)
If field A and field B are already populated, chances are the user wants
them to remain the same and only wants to invert the price range
selector.

The conversion isn't exact, but should be preferable to the previous
behavior.
2021-07-27 16:57:52 -07:00
Crowdin Bot
c8570e5427 chore(i18n): synchronize translations from crowdin [skip ci] 2021-07-27 20:04:39 +00:00
Ian Lapham
2806f6513a feat: use modal for txn confirmation on l2 (#2055)
* use modal for txn confirmation on l2

* update with error state

* remove 1 translation

* dedicated L2 component

* update styling on to be less jumpy

* add success animation to modal

* revert regular submitted content

* remove useless hook in popups

* remove import

Co-authored-by: Ian Lapham <ian_lapham@alumni.brown.edu>
2021-07-27 15:55:47 -04:00
Noah Zinsmeister
422c703e71 Revert "feat(uma): uma call option routing (#1385)"
This reverts commit 89d484d882.
2021-07-26 14:19:29 -04:00
Noah Zinsmeister
506493e8ab Revert "feat(routing): support mirror protocol routing as additional bases (#1375)"
This reverts commit 3198129af2.
2021-07-26 13:51:53 -04:00
Justin Domingue
8dfd143208 test: set up component snapshot testing (#2102)
* set up snapshot testing

* improvements

* add tests for TextInput as an example

* Fix code style issues with ESLint

* add comment to custom-test-env file

* only set up needed providers

* include style rules in snapshots

* disable redux storage warning

* added setupTests to avoid boilerplate

Co-authored-by: Lint Action <lint-action@samuelmeuli.com>
2021-07-26 10:41:08 -07:00
Moody Salem
718003b6f2 fix: prevent error if user selects ETH and WETH in add liquidity (#2112)
* fix: prevent error if user selects ETH and WETH in add liquidity

fixes https://github.com/Uniswap/uniswap-interface/issues/1763

* linting error

* revert changes to Tokens.ts
2021-07-26 11:08:15 -05:00
Moody Salem
80b3aa9e61 fix: remove html ids and unnecessary keys from the menu 2021-07-26 07:34:11 -05:00
Ian Lapham
4078390a48 update unsupported and broken lists (#2100) 2021-07-23 18:05:14 -04:00
Justin Domingue
e07599ef0f fix: better support decimal overflow (#2075)
* better support decimal overflow

* improve tests

* explicitly define num digits in toSignificant

* add tests for 0 decimals

* undo change to tryParseAmount and instead compute price without relying on parse

* remove console log and add test

* removed comment

* addressed pr feedback

* improve parsing logic

* addressed pr feedback
2021-07-23 11:11:30 -07:00
Ian Lapham
311bdc19b3 add UniH to unsupported list (#2097) 2021-07-23 13:46:05 -04:00
Moody Salem
a6342d40f1 feat: support connecting to a gnosis safe (#2096)
* feat: support connecting to a gnosis safe

* add the manifest.json attributes required by gnosis safe

* remove background fill

* copy

* only try injected provider if not active after trying safe
2021-07-23 12:04:18 -05:00
Moody Salem
3686506f2a perf: remove extra spread operators from the combine maps function 2021-07-23 09:27:23 -05:00
Moody Salem
634d010d92 perf: list to token map function was doing too many reduces (#2095) 2021-07-23 09:16:56 -05:00
Moody Salem
979f29ad62 chore: copy edit on CONTRIBUTING.md 2021-07-22 16:14:55 -05:00
Justin Domingue
4d3ed5d6ba feat: routing api integration (#2080)
* initial routing api integration

* add routing api slice

* display route in dialog

* addressed pr feedback

* switch to `get`

* renamed useRouterTradeExactIn to useRouter

* moving few files to later iteration

* removed unnecessary `as`

* switch to polling

* add todo for blocknumber freshness

* remove account-slippage-deadline
2021-07-21 15:08:35 -07:00
Moody Salem
ea79fbc2e0 chore: add blurb about release process (#2061)
* chore: add blurb about release process

* additional standard

* how to collect feedback
2021-07-20 16:10:58 -05:00
Jordan Frankfurt
4d19122bd2 set 5 min deadline on swaps and hide UI on L2 (#2081) 2021-07-20 12:41:49 -04:00
Moody Salem
13d7d2c992 fix(swap): double max gas for quoting a v3 swap for large trades 2021-07-19 19:00:52 -05:00
Brendan Weinstein
02cf33e115 Update WalletLink and @web3-react/walletlink-connector (#2079) 2021-07-19 14:52:11 -07:00
Justin Domingue
1f62cdf7a2 fix: invert selected range on RateToggle instead of clearing inputs (#2078) 2021-07-19 11:22:46 -07:00
Crowdin Bot
701e8fe116 chore(i18n): synchronize translations from crowdin [skip ci] 2021-07-19 13:09:18 +00:00
Crowdin Bot
3207e6026e chore(i18n): synchronize translations from crowdin [skip ci] 2021-07-19 10:05:27 +00:00
Justin Domingue
b0d93dbc0e increase zoom out range (#2076) 2021-07-16 19:14:39 -07:00
Justin Domingue
975a5e3434 remove unused dependencies (#2074) 2021-07-16 15:03:19 -07:00
Ian Lapham
07f52f02ff feat: only block on app url, mobile UI tweaks (#2073)
* start iframe ui updates

* replace hook with constant

* add origin check for blocked lists

* remove origin check for local unsupported list

* remove redundant retun

* remove iframe check

* undo local change
2021-07-16 15:28:19 -04:00
Crowdin Bot
5f112611c8 chore(i18n): synchronize translations from crowdin [skip ci] 2021-07-16 18:22:45 +00:00
Justin Domingue
1d82a4c71e fix: set language using url param in language dropdown (#2063)
* set language using url param in language dropdown

* refactor common code into a hook

* add hook

* address pr feedback
2021-07-16 10:56:33 -07:00
Justin Domingue
6004c4be3e fix: change panning cursor to col-resize (#2071)
* change panning cursor to col-resize

* revert to grabby, and support grabbing
2021-07-16 10:49:27 -07:00
Justin Domingue
a00432c1c3 fix: remove chart clamping and show indicator when handle is off screen (#2064)
* initial off screen indicator

* adjust offscreen indicator

* add off screen handle indicator

* hide reset until we get a better behavior

* add svg.tsx
2021-07-15 16:31:06 -07:00
Crowdin Bot
a184afa41e chore(i18n): synchronize translations from crowdin [skip ci] 2021-07-15 22:05:15 +00:00
Crowdin Bot
49fd256e79 chore(i18n): synchronize translations from crowdin [skip ci] 2021-07-15 17:09:03 +00:00
Crowdin Bot
c006edf696 chore(i18n): synchronize translations from crowdin [skip ci] 2021-07-15 16:05:00 +00:00
Moody Salem
b16dd2a930 chore: add one more unit test for ALL_SUPPORTED_CHAIN_IDS 2021-07-15 10:04:43 -05:00
Moody Salem
08a8f669e8 fix: all supported chain ids should not include the string keys of the enum 2021-07-15 09:59:26 -05:00
Justin Domingue
95d91c8528 fix: add language selector in top-level menu (#2057)
* initial language selector

* set > icon opacity for consistency

* always reset menu to main on close

* always reset menu on menu open
2021-07-15 07:46:05 -07:00
Justin Domingue
03902fe5c1 remove duplicate set price range title when there is liquidity (#2058) 2021-07-14 17:43:45 -07:00
Ian Lapham
631052e6a5 fix: update balance check in derived swap state (#2054)
* update balance check in derived swap state

* replace balance check with correct version of trade
2021-07-14 19:23:46 -04:00
Moody Salem
c63bb84f09 fix(block number): link to the correct page in etherscan for optimism block number links 2021-07-14 11:56:37 -05:00
Moody Salem
f0d10ba0df fix: add DAI and WBTC and USDT to routing for Optimism
fixes https://github.com/Uniswap/uniswap-interface/issues/2016
2021-07-14 11:50:04 -05:00
Moody Salem
88600d3a78 refactor: clean up some constants 2021-07-14 11:20:37 -05:00
Crowdin Bot
a1060d5f75 chore(i18n): synchronize translations from crowdin [skip ci] 2021-07-14 12:05:09 +00:00
Crowdin Bot
83762b3c6a chore(i18n): synchronize translations from crowdin [skip ci] 2021-07-14 07:04:47 +00:00
Crowdin Bot
7784f647e1 chore(i18n): synchronize translations from crowdin [skip ci] 2021-07-14 02:12:32 +00:00
Moody Salem
65ba56c5f0 fix(arbitrum): use infura endpoints 2021-07-13 20:44:37 -05:00
Crowdin Bot
963514cd51 chore(i18n): synchronize translations from crowdin [skip ci] 2021-07-14 01:25:14 +00:00
Crowdin Bot
6d0c8037a8 chore(i18n): synchronize translations from crowdin [skip ci] 2021-07-13 22:04:48 +00:00
Justin Domingue
d5ef7f9b24 fix: revert remove x-axis clamping behavior in liquidity chart (#2045)
* Revert "fix: remove x-axis clamping behavior in liquidity chart (#2042)"

This reverts commit f86db00464.

* removed extra curly braces around errorMessage
2021-07-13 14:33:13 -07:00
Justin Domingue
f86db00464 fix: remove x-axis clamping behavior in liquidity chart (#2042)
* remove x-axis clamping behavior

* remove extra curly braces around errorMessage
2021-07-13 14:17:52 -07:00
Justin Domingue
8884020fab chore: support stubbing subgraph in integration tests (#2039)
* upgrade to 7.0

* first iteration of stubbing subgraph in integration tests
2021-07-13 12:54:49 -07:00
Ian Lapham
db145658db update links (#2040) 2021-07-13 15:44:10 -04:00
Ian Lapham
55bbaffe15 fix: support for isolated pool creation (#2029)
* revert to remove ui polish changes

* left align blurb and fix bp issue

* fix translation wrapper

* remove preview screen for create, hide submitted txn UI for create, copy updates

* small fixes

* copy updates for create blurb

* add set price range title

* add translation

* fix some translations

Co-authored-by: Justin Domingue <judo@uniswap.org>
2021-07-13 15:18:09 -04:00
Callil Capuozzo
e1c3ad8f54 fix(L2): Design tweaks for L2 (Optimism) (#2038)
Style tweaks to header layout, network dropdown and pool pages

Co-authored-by: Jordan Frankfurt <jordanwfrankfurt@gmail.com>
2021-07-13 15:08:23 -04:00
Ian Lapham
730af2eed4 fix: dismiss txn confirmation modal if on L2 (#2022)
* dismiss txn confirmation modal if on l2

* line up far right margin;

* remove submitted view on txns if on L2

* remove uneeded chainid

* wrap dismissal in useEffect

* update variable names
2021-07-13 14:37:35 -04:00
Jordan Frankfurt
c625d15feb fix(L2): add downtime warning to add liquidity and adjust styles of network notification (#2034)
* add liquidity warning and network notification

* remove unneeded width

* use externalLink in LinkOutToBridge component

Co-authored-by: Moody Salem <moody.salem@gmail.com>
2021-07-13 12:30:24 -05:00
Justin Domingue
f9de85251c fix overflow on mobile (#2037) 2021-07-13 10:11:50 -07:00
Moody Salem
a186833b7a fix(L2): enable optimism token list when switched to optimism (#2036)
* add title text and opacity variability to token lists

* add optimism token list

* show tokens from active lists

* sort up token lists
with tokens on the current chain

* fix up some type issues

prune out chainId changes

* clean up leftover any

* refactor token count mechanism

* handle plurals in title text string

* new combineMaps implementation

* remove custom plural

* address a couple nits

* show the number of tokens on current chain

Co-authored-by: Jordan Frankfurt <jordanwfrankfurt@gmail.com>
Co-authored-by: Justin Domingue <judo@uniswap.org>
2021-07-13 11:51:59 -05:00
Luke Donato
aa06db7684 add optional chaining for stakingInfo (#1950) 2021-07-13 11:06:29 -05:00
Moody Salem
f5b601ee99 fix(L2): use 10 bips slippage for swaps by default on L2 (#2035)
* use 10bip slippage on L2

* update deps

Co-authored-by: Jordan Frankfurt <jordanwfrankfurt@gmail.com>
2021-07-13 10:41:46 -05:00
Justin Domingue
bf30013b6c fix: add liquidity ux polish (#2024)
* zoom out on intiial

* adjust initial zoom ranges

* remove unneeded reactga event

* adjust full range warning copy

* update zoom

* adjust zoom ranges and label around current price
2021-07-13 08:23:24 -07:00
Justin Domingue
2bc2a2c76e compare with undefined rather than falsy (#2027) 2021-07-12 16:13:45 -07:00
Justin Domingue
28bf95123e load pool data for each tier to determine state (#2026) 2021-07-12 15:56:55 -07:00
Justin Domingue
b1d88317b9 fix: properly set upper/lower base/quote on manual inversion (#2023) 2021-07-12 14:58:44 -07:00
Moody Salem
f332315de9 fix(routing): updates routing constants for layer 2 networks to use ETH (#2018) 2021-07-12 15:30:28 -05:00
Ian Lapham
2fbb86ac44 fix: update CMC priority within lists (#2019) 2021-07-12 16:07:08 -04:00
Justin Domingue
8d567e4d4a fix: add liquidity flow polish (#2017)
* addressed feedback

* set initial, min and max zoom levels

* better handle 0

* avoid formatting range selector

* polish `not created` state

* remove unused import
2021-07-12 10:19:12 -07:00
Moody Salem
77fbccd3f1 fix: remove dollar sign from translation of eth amount
fixes https://github.com/Uniswap/uniswap-interface/issues/1986
2021-07-12 11:09:45 -05:00
Moody Salem
41330ab080 fix: price tokens on optimism mainnet using DAI 2021-07-12 11:03:10 -05:00
Jordan Frankfurt
4ba9d98e67 fix(L2): polish (#1988)
* polish

move getINfoLinkByChainId to chain constants

pr review - translations

optimism token list, typo, text color for L1 switch

use <Trans /> instead of t

undo unintentional tokenlist order change

use {'Optimism'} instead of {name}

switch deadline implementation

remove unused TYPE import

switch to generalized CHAIN_INFO instead of L2_INFO so we don't have to check in components

add target chain id param to optimism bridge

fix a minor breakpoint issue

reduce sigfigs for header balance

update network card dropdown text for optimism

remove list code

refactor SupportedChainId

* SupportedL1ChainId | SupportedL2ChainId -> SupportedChainId
2021-07-12 11:44:05 -04:00
Justin Domingue
52790ac92e support optimism and arbitrum subgraphs (#2001) 2021-07-12 08:26:11 -07:00
Crowdin Bot
380ca11dfd chore(i18n): synchronize translations from crowdin [skip ci] 2021-07-10 23:05:01 +00:00
Crowdin Bot
e4261cc01b chore(i18n): synchronize translations from crowdin [skip ci] 2021-07-10 22:05:10 +00:00
Crowdin Bot
99148dfd3f chore(i18n): synchronize translations from crowdin [skip ci] 2021-07-09 18:05:05 +00:00
Moody Salem
8bf9a9ce8d chore(token lists): bump the token list spec version 2021-07-09 12:35:14 -05:00
Crowdin Bot
b6c0c1e607 chore(i18n): synchronize translations from crowdin [skip ci] 2021-07-09 16:05:04 +00:00
Crowdin Bot
0739613abe chore(i18n): synchronize translations from crowdin [skip ci] 2021-07-09 12:05:06 +00:00
Crowdin Bot
3401dfdebe chore(i18n): synchronize translations from crowdin [skip ci] 2021-07-09 06:05:09 +00:00
Justin Domingue
ab9c27edad fix: make liquidity chart and input step counter responsive (#2005)
* make chart responsive

* make input step counter text responsive

* clean up chart
2021-07-08 15:52:31 -07:00
Moody Salem
f2dfc57002 fix: retry calls that return header not found errors (#2004) 2021-07-08 17:12:55 -05:00
Justin Domingue
318252b6a9 fix: min/max price inverted in positions page (#2002)
* fix param ordering in inverter hook

* use object param
2021-07-08 17:06:24 -04:00
Noah Zinsmeister
440c50dd3a update multicall address 2021-07-08 15:49:06 -04:00
Crowdin Bot
eacf4e6049 chore(i18n): synchronize translations from crowdin [skip ci] 2021-07-08 19:04:32 +00:00
Justin Domingue
ffbd2d107a feat(pool): add liquidity distribution range slider and update add liquidity layout (#1829)
* first iteration of useTicks and useActiveLiquidity

* feat(pools): add liquidity depth chart (#1835)

* cleanup

* use area chart instead of bar chart

* check for undefined rather than falsy

* feat: range buttons based on fee amount (#1870)

* range buttons based on fee amount

* hardcode percentages as ticks

* increase blocksperfect

* feat: optimize add liquidity charts (#1880)

* ignore syncing state

* remove surrounding ticks

* avoid processing price1 as it is unused

* cleanup

* feat: add zoom buttons to liquidity depth chart (#1882)

* ignore syncing state

* remove surrounding ticks

* avoid processing price1 as it is unused

* cleanup

* first pass at +/- zoom buttons

* remove console.log, cleanup

* use real price for price line

* updated brush handles to latest spec

* added % labels to handles

* round tick to nearest usable tick

* first pass at brushable area chart with d3

* first pass at brushable area chart with d3

* rework

* address PR comments

* add brush handles

* address PR comments

* further improvements

* feat(pools): improve full range support + capital efficiency warning (#1903)

* handle min and max prices in add liquidity

* cleaned up

* use flag to denote full range

* reset inputs on fee select

* fixed merge conflict

* handle full range in positions preview

* fixed invalid range when tokens are reversed

* use formatTickData

* updated layout

* cleaned up layout

* fixed address

* avoid re-rendering deposit amounts

* added zoom behavior and more styling

* renamed chart

* renamed main chart file;

* add brush tooltips

* remove chart title

* added accents to brush handles

* more work

* moved to file

* modularize chart components

* fix maximum depth

* added brush labels

* cleanup

* cleanup

* set up zoom

* added new components

* improved brush and zoom integration

* cleaned up clip path

* fixed clip paths

* integrated with the graph changes

* adjust fee selector

* fix data error

* add bar chart

* polish

* merged

* clean up error

* cleaned up after merge

* visual improvements

* moved +/- buttons to the right/left

* removed margin bottom

* removed unsused

* fix brush labels % change

* use d3.axisBottom

* updated labels

* improve brush range

* fix one brush change only

* adjust zoom and clippath

* use bars

* use area

* adjust axis bottom to mocks;

* improved bars

* show bar colors

* better handle full range

* adjust colors for light mode

* updated to mocks

* adjusted handles for visibility

* switch to area

* add react ga events

* adjusted to mocks

* memo brush domain to avoid re-renders

* fix inputstepcounter color

* adjust handles

* rely on the graph sorting tickidx

* use curvestepafter

* updated polish

* merged main

* add clamping and other fixes

* highlight selected area using a mask

* use price instead of % for labels

* delete unused

* refine ux

* relayout

* improve hooks

* adjust layout for mobile

* fixed card color

* adjust padding

* preent tick overflow

* flip handles sooner

* delete bars.tsx
2021-07-08 11:30:43 -07:00
Justin Domingue
2db29f205b fix: simplify liqudity depth hooks and improve chart primitives (#1999)
* updated hooks and chart primitives

* add lodash.inrange to package.json
2021-07-08 11:12:36 -07:00
Crowdin Bot
3ae9e2af2a chore(i18n): synchronize translations from crowdin [skip ci] 2021-07-08 17:12:35 +00:00
Noah Zinsmeister
5ff2cff841 feat: Add Optimism (#1923)
* preliminary support

* fix multicall abi

add explorer support

* Fix code style issues with ESLint

* fix gas estimation

* fix todo tests

* fix gas estimation comments

* anonymize links to kovan etherscan and poll more frequently on optimistic kovan

* separate pool creation

* remove supported chain id

* remove the blocktag

* do not use the blocktag on optimism only

* give more gas to tokenURI for optimism

* update sdk

* temp fix to the block tag thing (do not update the block number from the fetch block number)

* remove unused import

* gasRequired -> multi-network

* bump quoter gas limit to 6m on optimism

* move the gas required parameter by chain id one level up

* missed a hook

* retry fetching receipts for optimism as many times as arbitrum

* fix duplicate enum, add optimism as well to retry options config

* fix: state not getting updated after a transaction confirmation

* bump sdk version

* update for mainnet optimism (#1998)

* fix for calculateGasMargin on optimism

Co-authored-by: Lint Action <lint-action@samuelmeuli.com>
Co-authored-by: Moody Salem <moody.salem@gmail.com>
2021-07-08 12:46:21 -04:00
Crowdin Bot
0c0968864b chore(i18n): synchronize translations from crowdin [skip ci] 2021-07-08 15:04:48 +00:00
Crowdin Bot
9230473c36 chore(i18n): synchronize translations from crowdin [skip ci] 2021-07-08 14:05:18 +00:00
ChrisFox.eth
b791afac59 Updating two public-facing URLs to docs (#1992) 2021-07-08 08:22:31 -05:00
Crowdin Bot
2899c8af45 chore(i18n): synchronize translations from crowdin [skip ci] 2021-07-08 06:04:59 +00:00
Moody Salem
5f13f7824a chore: update the contribution guidelines (#1987)
* chore: update the contribution guidelines

* move

* note about non-deterministic steps

* little bit of separation

* language nits, add accessibility

* add reasoning
2021-07-07 19:48:03 -05:00
Crowdin Bot
6690bc513d chore(i18n): synchronize translations from crowdin [skip ci] 2021-07-08 00:06:45 +00:00
Justin Domingue
aea3c1f6e6 feat(pools): add liquidity chart range input primitives (#1990)
* first iteration of liquidity chart primitives

* add tickprocessed type

* clean up
2021-07-07 16:39:44 -07:00
Justin Domingue
e0c625671b improve liquidity depth hooks and data fetching (#1989) 2021-07-07 14:37:29 -07:00
Crowdin Bot
aa3c867222 chore(i18n): synchronize translations from crowdin [skip ci] 2021-07-07 16:06:58 +00:00
Justin Domingue
2182e676d4 fix: enforce styled-components/macro usage and upgrade react-scripts (#1981)
* upgrade react-scripts and enforce macro rule

* Fix code style issues with ESLint

Co-authored-by: Lint Action <lint-action@samuelmeuli.com>
2021-07-07 08:45:47 -07:00
Justin Domingue
f0fc11a3b5 stabilize fee distribution in hook (#1979) 2021-07-07 08:27:18 -07:00
Crowdin Bot
53d93afd78 chore(i18n): synchronize translations from crowdin [skip ci] 2021-07-07 15:05:04 +00:00
Jordan Frankfurt
b612b15e1e fix(L2): routing fixes (#1980)
* Revert "fix last commit"

This reverts commit 9f5764aab3.

* Revert "fix l2 routing bug"

This reverts commit f6dea47907.

* drops routing changes for L2

* pool v2 switch networks message
2021-07-07 10:59:31 -04:00
Noah Zinsmeister
9f5764aab3 fix last commit 2021-07-07 09:44:28 -04:00
202 changed files with 21635 additions and 6402 deletions

1
.env
View File

@@ -1,2 +1 @@
REACT_APP_INFURA_KEY="4bf032f2d38a4ed6bb975b80d6340847"
REACT_APP_WALLETCONNECT_BRIDGE_URL="https://uniswap.bridge.walletconnect.org"

View File

@@ -28,6 +28,18 @@
"@typescript-eslint/ban-ts-comment": "off",
"@typescript-eslint/ban-ts-ignore": "off",
"@typescript-eslint/explicit-module-boundary-types": "off",
"react/react-in-jsx-scope": "off"
"react/react-in-jsx-scope": "off",
"no-restricted-imports": [
"error",
{
"paths": [
{
"name": "styled-components",
"message": "Please import from styled-components/macro."
}
],
"patterns": ["!styled-components/macro"]
}
]
}
}

View File

@@ -23,6 +23,7 @@ jobs:
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
release_branches: .*
default_bump: false
create_release:
name: Create Release

View File

@@ -1,46 +1,75 @@
# Development
## Install Dependencies
```bash
yarn install
```
## Generate locale files
```
yarn i18n:extract
```
## Run the interface
```bash
yarn start
```
# Contributing
Thank you for your interest in contributing to the Uniswap interface! 🦄
# Development
## Running the interface locally
1. `yarn install`
1. `yarn start`
## Creating a production build
1. `yarn install`
1. `yarn build`
## Engineering standards
Code merged into the `main` branch of this repository should adhere to high standards of correctness and maintainability.
Use your best judgment when applying these standards. If code is in the critical path, will be frequently visited, or
makes large architectural changes, consider following all the standards.
- Have at least one engineer approve of large code refactorings
- At least manually test small code changes, prefer automated tests
- Thoroughly unit test when code is not obviously correct
- If something breaks, add automated tests so it doesn't break again
- Add integration tests for new pages or flows
- Verify that all CI checks pass before merging
- Have at least one product manager or designer approve of significant UX changes
## Guidelines
The following points should help guide your development:
- Security: the interface is safe to use
- Avoid adding unnecessary dependencies due to [supply chain risk](https://github.com/LavaMoat/lavamoat#further-reading-on-software-supplychain-security)
- Reproducibility: anyone can build the interface
- Avoid adding steps to the development/build processes
- The build must be deterministic, i.e. a particular commit hash always produces the same build
- Decentralization: anyone can run the interface
- An Ethereum node should be the only critical dependency
- All other external dependencies should only enhance the UX ([graceful degradation](https://developer.mozilla.org/en-US/docs/Glossary/Graceful_degradation))
- Accessibility: anyone can use the interface
- The interface should be responsive, small and run well on low performance devices (majority of swaps on mobile!)
## Release process
Releases are cut automatically from the `main` branch Monday-Thursday in the morning according to the [release workflow](./.github/workflows/release.yaml).
Fix pull requests should be merged whenever ready and tested.
If a fix is urgently needed in production, releases can be manually triggered on [GitHub](https://github.com/Uniswap/uniswap-interface/actions/workflows/release.yaml)
after the fix is merged into `main`.
Features should not be merged into `main` until they are ready for users.
When building larger features or collaborating with other developers, create a new branch from `main` to track its development.
Use the automatic Vercel preview for sharing the feature to collect feedback.
When the feature is ready for review, create a new pull request from the feature branch into `main` and request reviews from
the appropriate UX reviewers (PMs or designers).
## Finding a first issue
Start with issues with the label
[`good first issue`](https://github.com/Uniswap/uniswap-interface/issues?q=is%3Aopen+is%3Aissue+label%3A%22good+first+issue%22).
## Pull requests
**Please open all pull requests against the `main` branch.**
CI checks will run against all PRs.
# Translations
Help Uniswap reach a global audience!
Uniswap uses [Crowdin](https://crowdin.com/project/uniswap-interface) for managing translations.
[This workflow](./.github/workflows/crowdin.yaml) uploads new strings for translation to the Crowdin project whenever code using the [lingui translation macros](https://lingui.js.org/ref/macro.html) is merged into `main`.
Uniswap uses [Crowdin](https://crowdin.com/project/uniswap-interface)
for managing translations. Whenever a new string is added to the project,
it gets uploaded to Crowdin for translation by [this workflow](./.github/workflows/crowdin.yaml).
Every hour, translations are synced from Crowdin in [this other workflow](./.github/workflows/crowdin-sync.yaml).
Every hour, translations are synced back down from Crowdin to the repository in [this other workflow](./.github/workflows/crowdin-sync.yaml).
We sync to the repository on a schedule, rather than download translations at build time, so that builds are always reproducible.
You can contribute by joining Crowdin to proofread existing translations [here](https://crowdin.com/project/uniswap-interface/invite?d=93i5n413q403t4g473p443o4c3t2g3s21343u2c3n403l4b3v2735353i4g4k4l4g453j4g4o4j4e4k4b323l4a3h463s4g453q443m4e3t2b303s2a35353l403o443v293e303k4g4n4r4g483i4g4r4j4e4o473i5n4a3t463t4o4)

16
custom-test-env.js Normal file
View File

@@ -0,0 +1,16 @@
// Custom test environment to provide `TextEncoder`/`TextDecoder`
// eslint-disable-next-line @typescript-eslint/no-var-requires
const Environment = require('jest-environment-jsdom')
module.exports = class CustomTestEnvironment extends Environment {
async setup() {
await super.setup()
if (typeof this.global.TextEncoder === 'undefined') {
// eslint-disable-next-line @typescript-eslint/no-var-requires
const { TextEncoder, TextDecoder } = require('util')
this.global.TextEncoder = TextEncoder
this.global.TextDecoder = TextDecoder
}
}
}

View File

@@ -2,7 +2,6 @@
"projectId": "yp82ef",
"baseUrl": "http://localhost:3000",
"pluginsFile": false,
"fixturesFolder": false,
"supportFile": "cypress/support/index.js",
"video": false,
"defaultCommandTimeout": 10000

View File

@@ -0,0 +1,25 @@
{
"_meta": {
"block": {
"number": 99999999
}
},
"asToken0": [
{
"feeTier": "500",
"totalValueLockedToken0": "0",
"totalValueLockedToken1": "1"
},
{
"feeTier": "3000",
"totalValueLockedToken0": "0",
"totalValueLockedToken1": "7"
},
{
"feeTier": "10000",
"totalValueLockedToken0": "0",
"totalValueLockedToken1": "2"
}
],
"asToken1": []
}

View File

@@ -1,4 +1,13 @@
import { CyHttpMessages } from 'cypress/types/net-stubbing'
import { aliasQuery, hasQuery } from '../utils/graphql-test-utils'
describe('Add Liquidity', () => {
beforeEach(() => {
cy.intercept('POST', '/subgraphs/name/uniswap/uniswap-v3', (req) => {
aliasQuery(req, 'feeTierDistribution')
})
})
it('loads the two correct tokens', () => {
cy.visit('/add/0xF9bA5210F91D0474bd1e1DcDAeC4C58E359AaD85/0xc778417E063141139Fce010982780140Aa0cD5Ab/500')
cy.get('#add-liquidity-input-tokena .token-symbol-container').should('contain.text', 'MKR')
@@ -23,4 +32,32 @@ describe('Add Liquidity', () => {
cy.visit('/add/0xF9bA5210F91D0474bd1e1DcDAeC4C58E359AaD85')
cy.get('#add-liquidity-input-tokena .token-symbol-container').should('contain.text', 'MKR')
})
it('loads fee tier distribution', () => {
cy.fixture('feeTierDistribution.json').then((feeTierDistribution) => {
cy.intercept('POST', '/subgraphs/name/uniswap/uniswap-v3', (req: CyHttpMessages.IncomingHttpRequest) => {
if (hasQuery(req, 'feeTierDistribution')) {
req.alias = 'feeTierDistributionQuery'
req.reply({
body: {
data: {
...feeTierDistribution,
},
},
headers: {
'access-control-allow-origin': '*',
},
})
}
})
cy.visit('/add/0xF9bA5210F91D0474bd1e1DcDAeC4C58E359AaD85/0xc778417E063141139Fce010982780140Aa0cD5Ab')
cy.wait('@feeTierDistributionQuery')
cy.get('#add-liquidity-selected-fee .selected-fee-label').should('contain.text', '0.3% fee tier')
cy.get('#add-liquidity-selected-fee .selected-fee-percentage').should('contain.text', '70%')
})
})
})

View File

@@ -0,0 +1,12 @@
// Utility to match GraphQL mutation based on the query name
export const hasQuery = (req: any, queryName: string) => {
const { body } = req
return body.hasOwnProperty('query') && body.query.includes(queryName)
}
// Alias query if queryName matches
export const aliasQuery = (req: any, queryName: string) => {
if (hasQuery(req, queryName)) {
req.alias = `${queryName}Query`
}
}

View File

@@ -4,7 +4,8 @@
"homepage": ".",
"private": true,
"devDependencies": {
"@ethersproject/experimental": "^5.2.0",
"@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",
@@ -18,13 +19,15 @@
"@reach/dialog": "^0.10.3",
"@reach/portal": "^0.10.3",
"@react-hook/window-scroll": "^1.3.0",
"@reduxjs/toolkit": "^1.6.0",
"@reduxjs/toolkit": "^1.6.1",
"@testing-library/jest-dom": "^5.14.1",
"@testing-library/react": "^12.0.0",
"@typechain/ethers-v5": "^7.0.0",
"@types/d3": "^6.7.1",
"@types/jest": "^25.2.1",
"@types/lingui__core": "^2.7.1",
"@types/lingui__macro": "^2.7.4",
"@types/lingui__react": "^2.8.3",
"@types/lodash.clonedeep": "^4.5.6",
"@types/lodash.flatmap": "^4.5.6",
"@types/luxon": "^1.24.4",
"@types/ms.macro": "^2.0.0",
@@ -48,35 +51,35 @@
"@uniswap/governance": "^1.0.2",
"@uniswap/liquidity-staker": "^1.0.2",
"@uniswap/merkle-distributor": "1.0.1",
"@uniswap/token-lists": "^1.0.0-beta.19",
"@uniswap/sdk-core": "^3.0.1",
"@uniswap/token-lists": "^1.0.0-beta.25",
"@uniswap/v2-core": "1.0.0",
"@uniswap/v2-periphery": "^1.1.0-beta.0",
"@uniswap/v2-sdk": "^3.0.0-alpha.2",
"@uniswap/v3-core": "1.0.0",
"@uniswap/v3-periphery": "^1.1.1",
"@uniswap/v3-sdk": "^3.0.0-alpha.9",
"@uniswap/v3-sdk": "^3.3.0",
"@web3-react/core": "^6.0.9",
"@web3-react/fortmatic-connector": "^6.0.9",
"@web3-react/injected-connector": "^6.0.7",
"@web3-react/portis-connector": "^6.0.9",
"@web3-react/walletconnect-connector": "^6.2.0",
"@web3-react/walletlink-connector": "^6.2.0",
"@web3-react/walletconnect-connector": "^7.0.2-alpha.0",
"@web3-react/walletlink-connector": "^6.2.3",
"ajv": "^6.12.3",
"cids": "^1.0.0",
"copy-to-clipboard": "^3.2.0",
"cross-env": "^7.0.2",
"cypress": "^4.11.0",
"cypress": "^7.7.0",
"d3": "^7.0.0",
"eslint": "^7.11.0",
"eslint-config-prettier": "^6.11.0",
"eslint-plugin-prettier": "^3.1.3",
"eslint-plugin-react": "^7.19.0",
"eslint-plugin-react-hooks": "^4.0.0",
"ethers": "^5.2.0",
"ethers": "^5.4.0",
"graphql": "^15.5.0",
"graphql-request": "^3.4.0",
"inter-ui": "^3.13.1",
"lightweight-charts": "^3.3.0",
"lodash.clonedeep": "^4.5.0",
"jest-styled-components": "^7.0.5",
"lodash.flatmap": "^4.5.0",
"luxon": "^1.25.0",
"ms.macro": "^2.0.0",
@@ -103,39 +106,35 @@
"react-virtualized-auto-sizer": "^1.0.2",
"react-window": "^1.8.5",
"rebass": "^4.0.7",
"redux-devtools-extension": "^2.13.9",
"redux-localstorage-simple": "^2.3.1",
"serve": "^11.3.2",
"start-server-and-test": "^1.11.0",
"styled-components": "^5.3.0",
"styled-system": "^5.1.5",
"typechain": "^5.0.0",
"typescript": "^4.2.3",
"ua-parser-js": "^0.7.28",
"use-count-up": "^2.2.5",
"wcag-contrast": "^3.0.0",
"workbox-core": "^6.1.0",
"workbox-expiration": "^6.1.0",
"workbox-precaching": "^6.1.0",
"workbox-routing": "^6.1.0",
"workbox-strategies": "^6.1.0"
"workbox-routing": "^6.1.0"
},
"resolutions": {
"@walletconnect/web3-provider": "1.5.0-rc.5"
"@walletconnect/ethereum-provider": "1.6.4"
},
"scripts": {
"compile-contract-types": "yarn compile-external-abi-types && yarn compile-v3-contract-types",
"compile-external-abi-types": "typechain --target ethers-v5 --out-dir src/abis/types './src/abis/**/*.json'",
"compile-v3-contract-types": "typechain --target ethers-v5 --out-dir src/types/v3 './node_modules/@uniswap/?(v3-core|v3-periphery)/artifacts/contracts/**/*.json'",
"build": "yarn compile-contract-types && yarn graphql:generate && yarn i18n:extract && yarn i18n:compile && react-scripts build",
"build": "yarn compile-contract-types && yarn graphql:generate && yarn i18n:extract && react-scripts build",
"i18n:extract": "lingui extract --locale en-US",
"i18n:compile": "lingui compile",
"integration-test": "start-server-and-test 'serve build -l 3000' http://localhost:3000 'cypress run --record'",
"graphql:generate": "graphql-codegen --config codegen.yml",
"postinstall": "yarn compile-contract-types",
"start": "yarn compile-contract-types && react-scripts start",
"test": "react-scripts test --env=jsdom",
"prestart": "yarn graphql:generate && touch src/locales/en-US.po"
"test": "react-scripts test --env=./custom-test-env.js",
"prei18n:extract": "touch src/locales/en-US.po",
"prestart": "yarn graphql:generate && yarn prei18n:extract"
},
"eslintConfig": {
"extends": "react-app",

View File

@@ -0,0 +1,11 @@
<svg width="257" height="256" viewBox="0 0 257 256" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M96.4347 60.2768C94.3434 59.9524 94.2552 59.9143 95.2394 59.7631C97.1254 59.473 101.579 59.8683 104.648 60.5981C111.812 62.3011 118.331 66.6637 125.29 74.4117L127.139 76.4701L129.784 76.0449C140.926 74.2544 152.26 75.6774 161.741 80.057C164.349 81.2619 168.461 83.6603 168.975 84.2766C169.138 84.473 169.439 85.7374 169.642 87.0866C170.347 91.7544 169.994 95.3323 168.567 98.0046C167.79 99.4588 167.746 99.9197 168.268 101.164C168.686 102.157 169.848 102.892 170.999 102.891C173.355 102.888 175.891 99.0791 177.066 93.78L177.532 91.6751L178.457 92.7224C183.528 98.4681 187.512 106.304 188.196 111.881L188.374 113.336L187.522 112.014C186.054 109.739 184.58 108.19 182.693 106.941C179.29 104.69 175.693 103.923 166.164 103.421C157.559 102.968 152.689 102.233 147.859 100.658C139.643 97.9792 135.501 94.4114 125.74 81.6059C121.405 75.918 118.726 72.7711 116.06 70.2368C110.004 64.4784 104.053 61.4584 96.4347 60.2768Z" fill="#FF007A"/>
<path d="M170.916 72.9763C171.132 69.1649 171.649 66.651 172.688 64.3552C173.099 63.4465 173.485 62.7027 173.544 62.7027C173.604 62.7027 173.425 63.3735 173.147 64.1931C172.391 66.4212 172.267 69.4687 172.788 73.0144C173.448 77.5132 173.824 78.1623 178.579 83.022C180.809 85.3014 183.403 88.1762 184.344 89.4105L186.054 91.6547L184.344 90.0508C182.253 88.0895 177.444 84.2644 176.381 83.7176C175.669 83.3509 175.563 83.3573 175.124 83.7946C174.719 84.1975 174.634 84.803 174.577 87.6654C174.49 92.1267 173.882 94.9901 172.414 97.8533C171.621 99.4019 171.495 99.0714 172.214 97.3235C172.75 96.0184 172.805 95.4446 172.801 91.1259C172.792 82.4485 171.762 80.3624 165.721 76.7887C164.19 75.8834 161.668 74.5778 160.117 73.8872C158.565 73.1965 157.333 72.595 157.378 72.5501C157.549 72.3798 163.441 74.0995 165.812 75.0117C169.339 76.3686 169.922 76.5444 170.35 76.3807C170.637 76.271 170.776 75.4347 170.916 72.9763Z" fill="#FF007A"/>
<path d="M100.497 87.8209C96.2514 81.9758 93.6246 73.0138 94.1933 66.3144L94.3691 64.2413L95.3355 64.4176C97.1504 64.7486 100.28 65.9133 101.745 66.8033C105.766 69.2453 107.506 72.4605 109.277 80.7164C109.796 83.1346 110.477 85.8712 110.79 86.7976C111.294 88.2889 113.199 91.7721 114.748 94.0343C115.864 95.6636 115.123 96.4356 112.657 96.213C108.89 95.873 103.788 92.3519 100.497 87.8209Z" fill="#FF007A"/>
<path d="M165.766 131.323C145.925 123.335 138.937 116.4 138.937 104.7C138.937 102.979 138.996 101.57 139.068 101.57C139.14 101.57 139.908 102.138 140.774 102.833C144.797 106.06 149.303 107.438 161.776 109.258C169.115 110.328 173.245 111.193 177.056 112.457C189.166 116.473 196.658 124.624 198.445 135.725C198.964 138.951 198.66 145 197.818 148.188C197.153 150.706 195.125 155.245 194.588 155.419C194.439 155.468 194.292 154.896 194.254 154.118C194.05 149.95 191.943 145.891 188.406 142.851C184.383 139.395 178.979 136.643 165.766 131.323Z" fill="#FF007A"/>
<path d="M151.837 134.642C151.588 133.163 151.157 131.273 150.879 130.444L150.372 128.935L151.313 129.991C152.614 131.451 153.642 133.32 154.514 135.81C155.179 137.71 155.254 138.275 155.249 141.362C155.244 144.393 155.161 145.029 154.546 146.739C153.578 149.436 152.376 151.348 150.359 153.4C146.735 157.089 142.075 159.131 135.351 159.978C134.182 160.125 130.775 160.373 127.78 160.529C120.232 160.922 115.264 161.733 110.801 163.3C110.159 163.525 109.586 163.662 109.528 163.604C109.347 163.425 112.386 161.613 114.897 160.404C118.436 158.699 121.96 157.768 129.854 156.454C133.754 155.804 137.781 155.016 138.804 154.702C148.461 151.741 153.426 144.1 151.837 134.642Z" fill="#FF007A"/>
<path d="M160.932 150.795C158.296 145.128 157.691 139.657 159.135 134.554C159.289 134.009 159.538 133.562 159.687 133.562C159.837 133.562 160.459 133.899 161.07 134.31C162.284 135.127 164.721 136.505 171.212 140.044C179.311 144.46 183.929 147.879 187.07 151.786C189.82 155.208 191.522 159.104 192.341 163.856C192.805 166.548 192.533 173.024 191.843 175.735C189.665 184.281 184.604 190.993 177.385 194.911C176.327 195.484 175.377 195.955 175.275 195.958C175.172 195.96 175.557 194.98 176.131 193.78C178.56 188.703 178.836 183.765 177 178.269C175.876 174.904 173.584 170.797 168.956 163.857C163.575 155.788 162.256 153.641 160.932 150.795Z" fill="#FF007A"/>
<path d="M86.4067 181.371C93.7696 175.154 102.931 170.738 111.276 169.382C114.872 168.798 120.864 169.03 124.194 169.882C129.532 171.248 134.307 174.308 136.791 177.954C139.218 181.517 140.259 184.622 141.343 191.53C141.771 194.255 142.236 196.992 142.377 197.611C143.191 201.192 144.775 204.054 146.739 205.491C149.857 207.773 155.227 207.915 160.509 205.855C161.405 205.505 162.184 205.263 162.238 205.318C162.43 205.508 159.77 207.288 157.894 208.226C155.369 209.487 153.361 209.975 150.693 209.975C145.855 209.975 141.839 207.514 138.487 202.495C137.828 201.508 136.346 198.55 135.193 195.922C131.655 187.851 129.907 185.393 125.799 182.702C122.223 180.361 117.612 179.941 114.143 181.642C109.586 183.876 108.315 189.699 111.579 193.39C112.876 194.856 115.295 196.121 117.273 196.367C120.973 196.828 124.153 194.013 124.153 190.277C124.153 187.851 123.221 186.467 120.873 185.407C117.668 183.961 114.222 185.652 114.238 188.663C114.245 189.947 114.805 190.754 116.092 191.336C116.918 191.71 116.937 191.74 116.263 191.6C113.322 190.99 112.633 187.444 114.998 185.09C117.837 182.264 123.709 183.511 125.725 187.368C126.572 188.988 126.67 192.215 125.932 194.164C124.279 198.525 119.46 200.819 114.571 199.571C111.243 198.721 109.887 197.801 105.874 193.667C98.9012 186.484 96.1941 185.092 86.141 183.523L84.2146 183.222L86.4067 181.371Z" fill="#FF007A"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M33.8241 20.4645C57.1114 48.7285 93.0139 92.734 94.7963 95.1977C96.2679 97.232 95.7141 99.0609 93.1929 100.494C91.7909 101.291 88.9084 102.099 87.4652 102.099C85.8329 102.099 83.9939 101.313 82.6548 100.042C81.7084 99.1447 77.8882 93.4402 69.0694 79.7566C62.3216 69.2863 56.6747 60.6007 56.5206 60.4553C56.1644 60.119 56.1705 60.1303 68.3813 81.9787C76.0487 95.6979 78.6371 100.548 78.6371 101.197C78.6371 102.516 78.2771 103.21 76.6495 105.025C73.9363 108.052 72.7235 111.453 71.8479 118.492C70.8664 126.382 68.1064 131.955 60.4577 141.494C55.9803 147.078 55.2477 148.102 54.118 150.352C52.695 153.187 52.3037 154.774 52.1451 158.353C51.9775 162.137 52.3039 164.581 53.46 168.199C54.4721 171.366 55.5285 173.458 58.2292 177.641C60.5599 181.251 61.9019 183.933 61.9019 184.983C61.9019 185.818 62.0613 185.819 65.6729 185.003C74.316 183.052 81.3341 179.619 85.2812 175.412C87.724 172.808 88.2975 171.371 88.3161 167.802C88.3283 165.469 88.2462 164.98 87.6153 163.637C86.5884 161.452 84.7188 159.635 80.5983 156.818C75.1992 153.127 72.8932 150.156 72.2562 146.07C71.7337 142.717 72.3399 140.351 75.3267 134.091C78.4182 127.612 79.1843 124.85 79.7025 118.319C80.0372 114.1 80.5008 112.435 81.7131 111.1C82.9776 109.707 84.1158 109.235 87.245 108.808C92.3466 108.111 95.5951 106.791 98.2652 104.33C100.582 102.196 101.551 100.139 101.7 97.0427L101.813 94.6959L100.518 93.1867C95.8304 87.7209 30.6848 16.168 30.3963 16.168C30.3347 16.168 31.8773 18.1015 33.8241 20.4645ZM64.5333 162.634C65.5932 160.757 65.0301 158.344 63.2572 157.166C61.5821 156.052 58.9799 156.576 58.9799 158.028C58.9799 158.471 59.2248 158.793 59.7768 159.077C60.7063 159.555 60.7738 160.093 60.0425 161.192C59.3019 162.306 59.3616 163.284 60.2111 163.949C61.5802 165.021 63.5183 164.432 64.5333 162.634Z" fill="#FF007A"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M105.032 110.039C102.637 110.774 100.309 113.312 99.5884 115.974C99.1487 117.598 99.3982 120.446 100.057 121.325C101.121 122.746 102.149 123.12 104.935 123.101C110.389 123.063 115.131 120.724 115.682 117.799C116.134 115.402 114.051 112.08 111.183 110.622C109.703 109.87 106.555 109.571 105.032 110.039ZM111.408 115.024C112.249 113.829 111.881 112.537 110.451 111.664C107.726 110 103.607 111.377 103.607 113.95C103.607 115.231 105.755 116.629 107.724 116.629C109.035 116.629 110.829 115.847 111.408 115.024Z" fill="#FF007A"/>
</svg>

After

Width:  |  Height:  |  Size: 7.9 KiB

View File

@@ -2,6 +2,14 @@
<html translate="no">
<head>
<meta charset="utf-8" />
<title>Uniswap Interface</title>
<meta name="description" content="Uniswap Interface" />
<!--
%PUBLIC_URL% will be replaced with the URL of the `public` folder during build.
Only files inside the `public` folder can be referenced from the HTML.
-->
<link rel="shortcut icon" type="image/png" href="%PUBLIC_URL%/favicon.png" />
<link rel="apple-touch-icon" sizes="192x192" href="%PUBLIC_URL%/images/192x192_App_Icon.png" />
<link rel="apple-touch-icon" sizes="512x512" href="%PUBLIC_URL%/images/512x512_App_Icon.png" />
@@ -9,20 +17,14 @@
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#ff007a" />
<meta name="fortmatic-site-verification" content="j93LgcVZk79qcgyo" />
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
manifest.json provides metadata used when the app is installed as a PWA.
See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<link rel="preconnect" href="https://www.google-analytics.com/">
<style>
* {
@@ -83,8 +85,6 @@
}
}
</style>
<title>Uniswap Interface</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>

View File

@@ -2,6 +2,7 @@
"background_color": "#fff",
"display": "standalone",
"homepage_url": "https://app.uniswap.org",
"providedBy": { "name": "Uniswap", "url": "https://uniswap.org" },
"icons": [
{
"src": "./images/192x192_App_Icon.png",
@@ -18,6 +19,8 @@
],
"orientation": "portrait",
"name": "Uniswap",
"description": "Swap or provide liquidity on the Uniswap Protocol",
"iconPath": "./images/256x256_App_Icon_Pink.svg",
"short_name": "Uniswap",
"start_url": ".",
"theme_color": "#ff007a"

View File

@@ -1,5 +1,5 @@
import { useCallback, useContext } from 'react'
import styled, { ThemeContext } from 'styled-components'
import styled, { ThemeContext } from 'styled-components/macro'
import { SUPPORTED_WALLETS } from '../../constants/wallet'
import { useActiveWeb3React } from '../../hooks/web3'
import { clearAllTransactions } from '../../state/transactions/actions'

View File

@@ -1,6 +1,6 @@
import { t, Trans } from '@lingui/macro'
import { useContext, useCallback, ReactNode } from 'react'
import styled, { ThemeContext } from 'styled-components'
import styled, { ThemeContext } from 'styled-components/macro'
import useENS from '../../hooks/useENS'
import { useActiveWeb3React } from '../../hooks/web3'
import { ExternalLink, TYPE } from '../../theme'

View File

@@ -1,6 +1,6 @@
import { readableColor } from 'polished'
import { PropsWithChildren } from 'react'
import styled, { DefaultTheme } from 'styled-components'
import styled, { DefaultTheme } from 'styled-components/macro'
import { Color } from 'theme/styled'
export enum BadgeVariant {

View File

@@ -9,6 +9,7 @@ const BLOCKED_ADDRESSES: string[] = [
'0x901bb9583b24D97e995513C6778dc6888AB6870e',
'0xA7e5d5A720f06526557c513402f2e6B5fA20b008',
'0x8576aCC5C05D6Ce88f4e49bf65BdF0C62F91353C',
'0xC8a65Fadf0e0dDAf421F28FEAb69Bf6E2E589963',
]
export default function Blocklist({ children }: { children: ReactNode }) {

View File

@@ -41,10 +41,6 @@ const Base = styled(RebassButton)<
transition: transform 450ms ease;
transform: perspective(1px) translateZ(0);
&:hover {
transform: scale(0.99);
}
> * {
user-select: none;
}
@@ -153,7 +149,6 @@ export const ButtonOutlined = styled(Base)`
border: 1px solid ${({ theme }) => theme.bg2};
background-color: transparent;
color: ${({ theme }) => theme.text1};
&:focus {
box-shadow: 0 0 0 1px ${({ theme }) => theme.bg4};
}
@@ -169,6 +164,27 @@ export const ButtonOutlined = styled(Base)`
}
`
export const ButtonYellow = styled(Base)`
background-color: ${({ theme }) => theme.yellow3};
color: white;
&:focus {
box-shadow: 0 0 0 1pt ${({ theme }) => darken(0.05, theme.yellow3)};
background-color: ${({ theme }) => darken(0.05, theme.yellow3)};
}
&:hover {
background-color: ${({ theme }) => darken(0.05, theme.yellow3)};
}
&:active {
box-shadow: 0 0 0 1pt ${({ theme }) => darken(0.1, theme.yellow3)};
background-color: ${({ theme }) => darken(0.1, theme.yellow3)};
}
&:disabled {
background-color: ${({ theme }) => theme.yellow3};
opacity: 50%;
cursor: auto;
}
`
export const ButtonEmpty = styled(Base)`
background-color: transparent;
color: ${({ theme }) => theme.primary1};

View File

@@ -50,7 +50,7 @@ export default function CurrencyLogo({
}, [currency, uriLocations])
if (currency?.isNative) {
return <StyledEthereumLogo src={EthereumLogo} size={size} style={style} {...rest} />
return <StyledEthereumLogo src={EthereumLogo} alt="ethereum logo" size={size} style={style} {...rest} />
}
return <StyledLogo size={size} srcs={srcs} alt={`${currency?.symbol ?? 'token'} logo`} style={style} {...rest} />

View File

@@ -1,6 +1,6 @@
import React, { useCallback, useEffect, useRef, useState } from 'react'
import { FeeAmount } from '@uniswap/v3-sdk'
import { Token } from '@uniswap/sdk-core'
import { Currency } from '@uniswap/sdk-core'
import { Trans } from '@lingui/macro'
import { AutoColumn } from 'components/Column'
import { DynamicSection } from 'pages/AddLiquidity/styled'
@@ -62,7 +62,7 @@ const FeeTierPercentageBadge = ({ percentage }: { percentage: number | undefined
return (
<Badge>
<TYPE.label fontSize={12}>
{Boolean(percentage) ? <Trans>{percentage?.toFixed(0)}% select</Trans> : <Trans>Not created</Trans>}
{percentage !== undefined ? <Trans>{percentage?.toFixed(0)}% select</Trans> : <Trans>Not created</Trans>}
</TYPE.label>
</Badge>
)
@@ -72,16 +72,16 @@ export default function FeeSelector({
disabled = false,
feeAmount,
handleFeePoolSelect,
token0,
token1,
currencyA,
currencyB,
}: {
disabled?: boolean
feeAmount?: FeeAmount
handleFeePoolSelect: (feeAmount: FeeAmount) => void
token0?: Token | undefined
token1?: Token | undefined
currencyA?: Currency | undefined
currencyB?: Currency | undefined
}) {
const { isLoading, isError, largestUsageFeeTier, distributions } = useFeeTierDistribution(token0, token1)
const { isLoading, isError, largestUsageFeeTier, distributions } = useFeeTierDistribution(currencyA, currencyB)
const [showOptions, setShowOptions] = useState(false)
const [pulsing, setPulsing] = useState(false)
@@ -137,7 +137,7 @@ export default function FeeSelector({
<DynamicSection gap="md" disabled={disabled}>
<FocusedOutlineCard pulsing={pulsing} onAnimationEnd={() => setPulsing(false)}>
<RowBetween>
<AutoColumn>
<AutoColumn id="add-liquidity-selected-fee">
{!feeAmount ? (
<>
<TYPE.label>
@@ -149,10 +149,10 @@ export default function FeeSelector({
</>
) : (
<>
<TYPE.label>
<TYPE.label className="selected-fee-label">
<Trans>{FeeAmountLabel[feeAmount].label}% fee tier</Trans>
</TYPE.label>
<Box style={{ width: 'fit-content', marginTop: '8px' }}>
<Box style={{ width: 'fit-content', marginTop: '8px' }} className="selected-fee-percentage">
{distributions && feeAmount && <FeeTierPercentageBadge percentage={distributions[feeAmount]} />}
</Box>
</>
@@ -173,7 +173,7 @@ export default function FeeSelector({
onClick={() => handleFeePoolSelectWithEvent(FeeAmount.LOW)}
>
<AutoColumn gap="sm" justify="flex-start">
<AutoColumn justify="flex-start" gap="4px">
<AutoColumn justify="flex-start" gap="6px">
<ResponsiveText>
<Trans>0.05% fee</Trans>
</ResponsiveText>

View File

@@ -3,32 +3,24 @@ import { YellowCard } from 'components/Card'
import { useOnClickOutside } from 'hooks/useOnClickOutside'
import { useActiveWeb3React } from 'hooks/web3'
import { useEffect, useRef, useState } from 'react'
import { ArrowDownCircle } from 'react-feather'
import { ArrowDownCircle, ChevronDown, ToggleLeft } from 'react-feather'
import { ApplicationModal } from 'state/application/actions'
import { useModalOpen, useToggleModal } from 'state/application/hooks'
import styled, { css } from 'styled-components'
import { ExternalLink, MEDIA_WIDTHS } from 'theme'
import styled, { css } from 'styled-components/macro'
import { ExternalLink } from 'theme'
import { switchToNetwork } from 'utils/switchToNetwork'
import { L2_CHAIN_IDS, L2_INFO, NETWORK_LABELS, SupportedChainId } from '../../constants/chains'
const StopOverflowQuery = `@media screen and (min-width: ${MEDIA_WIDTHS.upToMedium}px) and (max-width: ${
MEDIA_WIDTHS.upToMedium + 400
}px)`
import { CHAIN_INFO, L2_CHAIN_IDS, SupportedChainId, SupportedL2ChainId } from '../../constants/chains'
const BaseWrapper = css`
position: relative;
${StopOverflowQuery} {
position: absolute;
top: 80px;
right: 20px;
}
margin-right: 8px;
${({ theme }) => theme.mediaWidth.upToMedium`
margin-left: 12px;
justify-self: end;
`};
${({ theme }) => theme.mediaWidth.upToSmall`
margin: 0 0.5rem 0 0;
width: initial;
overflow: hidden;
text-overflow: ellipsis;
flex-shrink: 1;
`};
@@ -45,7 +37,7 @@ const BaseMenuItem = css`
display: flex;
flex: 1;
flex-direction: row;
font-size: 14px;
font-size: 16px;
font-weight: 400;
justify-content: space-between;
:hover {
@@ -73,28 +65,26 @@ const FallbackWrapper = styled(YellowCard)`
width: auto;
border-radius: 12px;
padding: 8px 12px;
width: 100%;
`
const Icon = styled.img`
width: 17px;
`
const L1Tag = styled.div`
color: #c4d9f8;
opacity: 40%;
`
const L2Tag = styled.div<{ chainId: SupportedChainId }>`
background-color: ${({ chainId }) => (chainId === SupportedChainId.ARBITRUM_ONE ? '#28A0F0' : '#FF0420')};
border-radius: 6px;
color: white;
font-size: 12px;
font-weight: 600;
padding: 2px 6px;
width: 16px;
margin-right: 2px;
${({ theme }) => theme.mediaWidth.upToSmall`
margin-right: 4px;
`};
`
const MenuFlyout = styled.span`
background-color: ${({ theme }) => theme.bg2};
background-color: ${({ theme }) => theme.bg1};
border: 1px solid ${({ 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),
0px 24px 32px rgba(0, 0, 0, 0.01);
border-radius: 20px;
padding: 12px;
border-radius: 12px;
padding: 1rem;
display: flex;
flex-direction: column;
font-size: 1rem;
@@ -104,7 +94,11 @@ const MenuFlyout = styled.span`
z-index: 100;
width: 237px;
${({ theme }) => theme.mediaWidth.upToMedium`
top: -10.25rem;
bottom: unset;
top: 4.5em
right: 0;
`};
> {
padding: 12px;
@@ -115,15 +109,12 @@ const MenuFlyout = styled.span`
> :not(:last-child) {
margin-bottom: 8px;
}
${StopOverflowQuery} {
left: unset;
right: 0rem;
}
`
const LinkOutCircle = styled(ArrowDownCircle)`
transform: rotate(230deg);
width: 20px;
height: 20px;
width: 16px;
height: 16px;
opacity: 0.6;
`
const MenuItem = styled(ExternalLink)`
${BaseMenuItem}
@@ -131,31 +122,44 @@ const MenuItem = styled(ExternalLink)`
const ButtonMenuItem = styled.button`
${BaseMenuItem}
border: none;
outline: none;
box-shadow: none;
color: ${({ theme }) => theme.text2};
outline: none;
padding: 0;
`
const NetworkInfo = styled.button`
const NetworkInfo = styled.button<{ chainId: SupportedChainId }>`
align-items: center;
background-color: ${({ theme }) => theme.bg2};
border-radius: 8px;
border: none;
background-color: ${({ theme }) => theme.bg0};
border-radius: 12px;
border: 1px solid ${({ theme }) => theme.bg0};
color: ${({ theme }) => theme.text1};
display: flex;
flex-direction: row;
font-weight: 500;
font-size: 12px;
height: 100%;
justify-content: space-between;
margin: 0;
padding: 8px;
width: 172px;
height: 38px;
padding: 0.7rem;
:hover,
:focus {
cursor: pointer;
outline: none;
background-color: ${({ theme }) => theme.bg3};
border: 1px solid ${({ theme }) => theme.bg3};
}
`
const NetworkName = styled.div<{ chainId: SupportedChainId }>`
border-radius: 6px;
font-size: 16px;
font-weight: 500;
padding: 0 2px 0.5px 4px;
margin: 0 2px;
white-space: pre;
${({ theme }) => theme.mediaWidth.upToSmall`
display: none;
`};
`
export default function NetworkCard() {
const { chainId, library } = useActiveWeb3React()
@@ -177,31 +181,29 @@ export default function NetworkCard() {
.catch(() => setImplements3085(false))
}, [library, chainId])
if (!chainId || chainId === SupportedChainId.MAINNET || !NETWORK_LABELS[chainId] || !library) {
const info = chainId ? CHAIN_INFO[chainId] : undefined
if (!chainId || chainId === SupportedChainId.MAINNET || !info || !library) {
return null
}
if (L2_CHAIN_IDS.includes(chainId)) {
const info = L2_INFO[chainId]
const info = CHAIN_INFO[chainId as SupportedL2ChainId]
const isArbitrum = [SupportedChainId.ARBITRUM_ONE, SupportedChainId.ARBITRUM_RINKEBY].includes(chainId)
return (
<L2Wrapper ref={node}>
<NetworkInfo onClick={toggle}>
<NetworkInfo onClick={toggle} chainId={chainId}>
<Icon src={info.logoUrl} />
<span>{NETWORK_LABELS[chainId]}</span>
<L2Tag chainId={chainId}>L2 Alpha</L2Tag>
<NetworkName chainId={chainId}>{info.label}</NetworkName>
<ChevronDown size={16} style={{ marginTop: '2px' }} strokeWidth={2.5} />
</NetworkInfo>
{open && (
<MenuFlyout>
<MenuItem href={info.bridge}>
<div>
<Trans>{NETWORK_LABELS[chainId]} Bridge</Trans>
</div>
<div>{isArbitrum ? <Trans>{info.label} Bridge</Trans> : <Trans>Optimistic L2 Gateway</Trans>}</div>
<LinkOutCircle />
</MenuItem>
<MenuItem href={info.explorer}>
<div>
<Trans>{NETWORK_LABELS[chainId]} Explorer</Trans>
</div>
{isArbitrum ? <Trans>{info.label} Explorer</Trans> : <Trans>Optimistic Etherscan</Trans>}
<LinkOutCircle />
</MenuItem>
<MenuItem href={info.docs}>
@@ -213,9 +215,9 @@ export default function NetworkCard() {
{implements3085 ? (
<ButtonMenuItem onClick={() => switchToNetwork({ library, chainId: SupportedChainId.MAINNET })}>
<div>
<Trans>Switch to Ethereum</Trans>
<Trans>Switch to L1 (Mainnet)</Trans>
</div>
<L1Tag>L1</L1Tag>
<ToggleLeft opacity={0.6} size={16} />
</ButtonMenuItem>
) : (
<DisabledMenuItem>
@@ -228,5 +230,5 @@ export default function NetworkCard() {
)
}
return <FallbackWrapper title={NETWORK_LABELS[chainId]}>{NETWORK_LABELS[chainId]}</FallbackWrapper>
return <FallbackWrapper title={info.label}>{info.label}</FallbackWrapper>
}

View File

@@ -1,5 +1,5 @@
import { useEffect, useState } from 'react'
import styled, { keyframes } from 'styled-components'
import styled, { keyframes } from 'styled-components/macro'
import { useActiveWeb3React } from '../../hooks/web3'
import { useBlockNumber } from '../../state/application/hooks'

View File

@@ -1,22 +1,23 @@
import { Trans } from '@lingui/macro'
import { CurrencyAmount, Token } from '@uniswap/sdk-core'
import { CHAIN_INFO, SupportedChainId } from 'constants/chains'
import { useMemo } from 'react'
import { X } from 'react-feather'
import styled from 'styled-components/macro'
import tokenLogo from '../../assets/images/token-logo.png'
import { UNI } from '../../constants/tokens'
import { useTotalSupply } from '../../hooks/useTotalSupply'
import { useActiveWeb3React } from '../../hooks/web3'
import { useMerkleDistributorContract } from '../../hooks/useContract'
import useCurrentBlockTimestamp from '../../hooks/useCurrentBlockTimestamp'
import { useTotalSupply } from '../../hooks/useTotalSupply'
import useUSDCPrice from '../../hooks/useUSDCPrice'
import { useActiveWeb3React } from '../../hooks/web3'
import { useTotalUniEarned } from '../../state/stake/hooks'
import { useAggregateUniBalance, useTokenBalance } from '../../state/wallet/hooks'
import { ExternalLink, StyledInternalLink, TYPE, UniTokenAnimated } from '../../theme'
import { computeUniCirculation } from '../../utils/computeUniCirculation'
import useUSDCPrice from '../../hooks/useUSDCPrice'
import { AutoColumn } from '../Column'
import { RowBetween } from '../Row'
import { Break, CardBGImage, CardNoise, CardSection, DataCard } from '../earn/styled'
import { Trans } from '@lingui/macro'
import { RowBetween } from '../Row'
const ContentWrapper = styled(AutoColumn)`
width: 100%;
@@ -59,6 +60,8 @@ export default function UniBalanceContent({ setShowUniBalanceModal }: { setShowU
[blockTimestamp, chainId, totalSupply, unclaimedUni, uni]
)
const { infoLink } = CHAIN_INFO[chainId ? chainId : SupportedChainId.MAINNET]
return (
<ContentWrapper gap="lg">
<ModalUpper>
@@ -128,7 +131,7 @@ export default function UniBalanceContent({ setShowUniBalanceModal }: { setShowU
<TYPE.white color="white">{totalSupply?.toFixed(0, { groupSeparator: ',' })}</TYPE.white>
</RowBetween>
{uni && uni.chainId === 1 ? (
<ExternalLink href={`https://info.uniswap.org/token/${uni.address}`}>
<ExternalLink href={`${infoLink}/token/${uni.address}`}>
<Trans>View UNI Analytics</Trans>
</ExternalLink>
) : null}

View File

@@ -1,8 +1,8 @@
import { Trans } from '@lingui/macro'
import useScrollPosition from '@react-hook/window-scroll'
import { CHAIN_INFO, SupportedChainId } from 'constants/chains'
import { darken } from 'polished'
import { useState } from 'react'
import { Moon, Sun } from 'react-feather'
import { NavLink } from 'react-router-dom'
import { Text } from 'rebass'
import { useShowClaimPopup, useToggleSelfClaimModal } from 'state/application/hooks'
@@ -19,7 +19,7 @@ import ClaimModal from '../claim/ClaimModal'
import { CardNoise } from '../earn/styled'
import Menu from '../Menu'
import Modal from '../Modal'
import Row, { RowFixed } from '../Row'
import Row from '../Row'
import { Dots } from '../swap/styleds'
import Web3Status from '../Web3Status'
import NetworkCard from './NetworkCard'
@@ -38,22 +38,27 @@ const HeaderFrame = styled.div<{ showBackground: boolean }>`
padding: 1rem;
z-index: 21;
position: relative;
/* Background slide effect on scroll. */
background-image: ${({ theme }) => `linear-gradient(to bottom, transparent 50%, ${theme.bg0} 50% )}}`}
background-image: ${({ theme }) => `linear-gradient(to bottom, transparent 50%, ${theme.bg0} 50% )}}`};
background-position: ${({ showBackground }) => (showBackground ? '0 -100%' : '0 0')};
background-size: 100% 200%;
box-shadow: 0px 0px 0px 1px ${({ theme, showBackground }) => (showBackground ? theme.bg2 : 'transparent;')};
transition: background-position .1s, box-shadow .1s;
transition: background-position 0.1s, box-shadow 0.1s;
background-blend-mode: hard-light;
${({ theme }) => theme.mediaWidth.upToLarge`
grid-template-columns: 48px 1fr 1fr;
`};
${({ theme }) => theme.mediaWidth.upToMedium`
padding: 1rem;
grid-template-columns: auto 1fr;
grid-template-columns: 1fr 1fr;
`};
${({ theme }) => theme.mediaWidth.upToExtraSmall`
padding: 1rem;
`}
${({ theme }) => theme.mediaWidth.upToSmall`
padding: 1rem;
grid-template-columns: 36px 1fr;
`};
`
const HeaderControls = styled.div`
@@ -61,23 +66,6 @@ const HeaderControls = styled.div`
flex-direction: row;
align-items: center;
justify-self: flex-end;
${({ theme }) => theme.mediaWidth.upToMedium`
flex-direction: row;
justify-content: space-between;
justify-self: center;
width: 100%;
max-width: 960px;
padding: 1rem;
position: fixed;
bottom: 0px;
left: 0px;
width: 100%;
z-index: 99;
height: 72px;
border-radius: 12px 12px 0 0;
background-color: ${({ theme }) => theme.bg1};
`};
`
const HeaderElement = styled.div`
@@ -90,22 +78,10 @@ const HeaderElement = styled.div`
}
${({ theme }) => theme.mediaWidth.upToMedium`
flex-direction: row-reverse;
align-items: center;
`};
`
const HeaderElementWrap = styled.div`
display: flex;
align-items: center;
`
const HeaderRow = styled(RowFixed)`
${({ theme }) => theme.mediaWidth.upToMedium`
width: 100%;
`};
`
const HeaderLinks = styled(Row)`
justify-self: center;
background-color: ${({ theme }) => theme.bg0};
@@ -116,8 +92,25 @@ const HeaderLinks = styled(Row)`
grid-auto-flow: column;
grid-gap: 10px;
overflow: auto;
align-items: center;
${({ theme }) => theme.mediaWidth.upToLarge`
justify-self: start;
`};
${({ theme }) => theme.mediaWidth.upToMedium`
justify-self: flex-end;
justify-self: center;
`};
${({ theme }) => theme.mediaWidth.upToMedium`
flex-direction: row;
justify-content: space-between;
justify-self: center;
z-index: 99;
position: fixed;
bottom: 0; right: 50%;
transform: translate(50%,-50%);
margin: 0 auto;
background-color: ${({ theme }) => theme.bg0};
border: 1px solid ${({ theme }) => theme.bg2};
box-shadow: 0px 6px 10px rgb(0 0 0 / 2%);
`};
`
@@ -125,7 +118,7 @@ const AccountElement = styled.div<{ active: boolean }>`
display: flex;
flex-direction: row;
align-items: center;
background-color: ${({ theme, active }) => (!active ? theme.bg1 : theme.bg2)};
background-color: ${({ theme, active }) => (!active ? theme.bg1 : theme.bg1)};
border-radius: 12px;
white-space: nowrap;
width: 100%;
@@ -159,12 +152,6 @@ const UNIWrapper = styled.span`
}
`
const HideSmall = styled.span`
${({ theme }) => theme.mediaWidth.upToSmall`
display: none;
`};
`
const BalanceText = styled(Text)`
${({ theme }) => theme.mediaWidth.upToExtraSmall`
display: none;
@@ -205,14 +192,15 @@ const StyledNavLink = styled(NavLink).attrs({
text-decoration: none;
color: ${({ theme }) => theme.text2};
font-size: 1rem;
width: fit-content;
font-weight: 500;
padding: 8px 12px;
word-break: break-word;
overflow: hidden;
white-space: nowrap;
&.${activeClassName} {
border-radius: 12px;
font-weight: 600;
justify-content: center;
color: ${({ theme }) => theme.text1};
background-color: ${({ theme }) => theme.bg2};
}
@@ -249,47 +237,13 @@ const StyledExternalLink = styled(ExternalLink).attrs({
color: ${({ theme }) => darken(0.1, theme.text1)};
text-decoration: none;
}
${({ theme }) => theme.mediaWidth.upToExtraSmall`
display: none;
`}
`
const StyledMenuButton = styled.button`
position: relative;
width: 100%;
height: 100%;
border: none;
background-color: transparent;
margin: 0;
padding: 0;
height: 35px;
background-color: ${({ theme }) => theme.bg2};
margin-left: 8px;
padding: 0.15rem 0.5rem;
border-radius: 0.5rem;
:hover,
:focus {
cursor: pointer;
outline: none;
background-color: ${({ theme }) => theme.bg4};
}
svg {
margin-top: 2px;
}
> * {
stroke: ${({ theme }) => theme.text1};
}
`
export default function Header() {
const { account } = useActiveWeb3React()
const { account, chainId } = useActiveWeb3React()
const userEthBalance = useETHBalances(account ? [account] : [])?.[account ?? '']
// const [isDark] = useDarkModeManager()
const [darkMode, toggleDarkMode] = useDarkModeManager()
const [darkMode] = useDarkModeManager()
const toggleClaimModal = useToggleSelfClaimModal()
@@ -302,19 +256,18 @@ export default function Header() {
const scrollY = useScrollPosition()
const { infoLink } = CHAIN_INFO[chainId ? chainId : SupportedChainId.MAINNET]
return (
<HeaderFrame showBackground={scrollY > 45}>
<ClaimModal />
<Modal isOpen={showUniBalanceModal} onDismiss={() => setShowUniBalanceModal(false)}>
<UniBalanceContent setShowUniBalanceModal={setShowUniBalanceModal} />
</Modal>
<HeaderRow>
<Title href=".">
<UniIcon>
<img width={'24px'} src={darkMode ? LogoDark : Logo} alt="logo" />
</UniIcon>
</Title>
</HeaderRow>
<Title href=".">
<UniIcon>
<img width={'24px'} src={darkMode ? LogoDark : Logo} alt="logo" />
</UniIcon>
</Title>
<HeaderLinks>
<StyledNavLink id={`swap-nav-link`} to={'/swap'}>
<Trans>Swap</Trans>
@@ -332,19 +285,20 @@ export default function Header() {
>
<Trans>Pool</Trans>
</StyledNavLink>
<StyledNavLink id={`stake-nav-link`} to={'/vote'}>
<Trans>Vote</Trans>
</StyledNavLink>
<StyledExternalLink id={`stake-nav-link`} href={'https://info.uniswap.org'}>
{chainId && chainId === SupportedChainId.MAINNET && (
<StyledNavLink id={`vote-nav-link`} to={'/vote'}>
<Trans>Vote</Trans>
</StyledNavLink>
)}
<StyledExternalLink id={`charts-nav-link`} href={infoLink}>
<Trans>Charts</Trans>
<sup></sup>
</StyledExternalLink>
</HeaderLinks>
<HeaderControls>
<NetworkCard />
<HeaderElement>
<HideSmall>
<NetworkCard />
</HideSmall>
{availableClaim && !showClaimPopup && (
<UNIWrapper onClick={toggleClaimModal}>
<UNIAmount active={!!account && !availableClaim} style={{ pointerEvents: 'auto' }}>
@@ -364,18 +318,13 @@ export default function Header() {
<AccountElement active={!!account} style={{ pointerEvents: 'auto' }}>
{account && userEthBalance ? (
<BalanceText style={{ flexShrink: 0 }} pl="0.75rem" pr="0.5rem" fontWeight={500}>
<Trans>{userEthBalance?.toSignificant(4)} ETH</Trans>
<Trans>{userEthBalance?.toSignificant(3)} ETH</Trans>
</BalanceText>
) : null}
<Web3Status />
</AccountElement>
</HeaderElement>
<HeaderElementWrap>
<StyledMenuButton onClick={() => toggleDarkMode()}>
{darkMode ? <Moon size={20} /> : <Sun size={20} />}
</StyledMenuButton>
<Menu />
</HeaderElementWrap>
</HeaderElement>
</HeaderControls>
</HeaderFrame>
)

View File

@@ -1,14 +1,13 @@
import { useState, useCallback, useEffect, ReactNode } from 'react'
import { LightCard } from 'components/Card'
import { RowBetween } from 'components/Row'
import { OutlineCard } from 'components/Card'
import { Input as NumericalInput } from '../NumericalInput'
import styled, { keyframes } from 'styled-components'
import styled, { keyframes } from 'styled-components/macro'
import { TYPE } from 'theme'
import { AutoColumn } from 'components/Column'
import { ButtonPrimary } from 'components/Button'
import { ButtonGray } from 'components/Button'
import { FeeAmount } from '@uniswap/v3-sdk'
import { formattedFeeAmount } from 'utils'
import { Trans } from '@lingui/macro'
import { Plus, Minus } from 'react-feather'
const pulse = (color: string) => keyframes`
0% {
@@ -24,25 +23,37 @@ const pulse = (color: string) => keyframes`
}
`
const SmallButton = styled(ButtonPrimary)`
/* background-color: ${({ theme }) => theme.bg2}; */
border-radius: 8px;
padding: 4px 6px;
width: 48%;
const InputRow = styled.div`
display: grid;
grid-template-columns: 30px 1fr 30px;
`
const FocusedOutlineCard = styled(LightCard)<{ active?: boolean; pulsing?: boolean }>`
const SmallButton = styled(ButtonGray)`
border-radius: 8px;
padding: 4px;
`
const FocusedOutlineCard = styled(OutlineCard)<{ active?: boolean; pulsing?: boolean }>`
border-color: ${({ active, theme }) => active && theme.blue1};
padding: 12px;
animation: ${({ pulsing, theme }) => pulsing && pulse(theme.blue1)} 0.8s linear;
`
const StyledInput = styled(NumericalInput)<{ usePercent?: boolean }>`
/* background-color: ${({ theme }) => theme.bg0}; */
background-color: transparent;
text-align: center;
margin-right: 12px;
width: 100%;
font-weight: 500;
padding: 0 10px;
${({ theme }) => theme.mediaWidth.upToSmall`
font-size: 16px;
`};
${({ theme }) => theme.mediaWidth.upToExtraSmall`
font-size: 12px;
`};
`
const InputTitle = styled(TYPE.small)`
@@ -51,11 +62,17 @@ const InputTitle = styled(TYPE.small)`
font-weight: 500;
`
const ButtonLabel = styled(TYPE.white)<{ disabled: boolean }>`
color: ${({ theme, disabled }) => (disabled ? theme.text2 : theme.text1)} !important;
`
interface StepCounterProps {
value: string
onUserInput: (value: string) => void
decrement: () => string
increment: () => string
decrementDisabled?: boolean
incrementDisabled?: boolean
feeAmount?: FeeAmount
label?: string
width?: string
@@ -69,7 +86,8 @@ const StepCounter = ({
value,
decrement,
increment,
feeAmount,
decrementDisabled = false,
incrementDisabled = false,
width,
locked,
onUserInput,
@@ -87,9 +105,6 @@ const StepCounter = ({
// animation if parent value updates local value
const [pulsing, setPulsing] = useState<boolean>(false)
// format fee amount
const feeAmountFormatted = feeAmount ? formattedFeeAmount(feeAmount * 2) : ''
const handleOnFocus = () => {
setUseLocalValue(true)
setActive(true)
@@ -126,39 +141,45 @@ const StepCounter = ({
return (
<FocusedOutlineCard pulsing={pulsing} active={active} onFocus={handleOnFocus} onBlur={handleOnBlur} width={width}>
<AutoColumn gap="6px" style={{ marginBottom: '12px' }}>
<AutoColumn gap="6px">
<InputTitle fontSize={12} textAlign="center">
{title}
</InputTitle>
<StyledInput
className="rate-input-0"
value={localValue}
fontSize="20px"
disabled={locked}
onUserInput={(val) => {
setLocalValue(val)
}}
/>
<InputRow>
{!locked && (
<SmallButton onClick={handleDecrement} disabled={decrementDisabled}>
<ButtonLabel disabled={decrementDisabled} fontSize="12px">
<Minus size={18} />
</ButtonLabel>
</SmallButton>
)}
<StyledInput
className="rate-input-0"
value={localValue}
fontSize="20px"
disabled={locked}
onUserInput={(val) => {
setLocalValue(val)
}}
/>
{!locked && (
<SmallButton onClick={handleIncrement} disabled={incrementDisabled}>
<ButtonLabel disabled={incrementDisabled} fontSize="12px">
<Plus size={18} />
</ButtonLabel>
</SmallButton>
)}
</InputRow>
<InputTitle fontSize={12} textAlign="center">
<Trans>
{tokenB} per {tokenA}
</Trans>
</InputTitle>
</AutoColumn>
{!locked ? (
<RowBetween>
<SmallButton onClick={handleDecrement}>
<TYPE.white fontSize="12px">
<Trans>-{feeAmountFormatted}%</Trans>
</TYPE.white>
</SmallButton>
<SmallButton onClick={handleIncrement}>
<TYPE.white fontSize="12px">
<Trans>+{feeAmountFormatted}%</Trans>
</TYPE.white>
</SmallButton>
</RowBetween>
) : null}
</FocusedOutlineCard>
)
}

View File

@@ -1,169 +0,0 @@
import React, { useRef, useState, useEffect, useCallback, Dispatch, SetStateAction, ReactNode } from 'react'
import { createChart, IChartApi } from 'lightweight-charts'
import { darken } from 'polished'
import { RowBetween } from 'components/Row'
import Card from '../Card'
import styled from 'styled-components/macro'
import useTheme from 'hooks/useTheme'
const Wrapper = styled(Card)`
width: 100%;
padding: 1rem;
display: flex;
background-color: ${({ theme }) => theme.bg0};
flex-direction: column;
> * {
font-size: 1rem;
}
`
const DEFAULT_HEIGHT = 300
type LineChartProps = {
data: any[]
color?: string | undefined
height?: number | undefined
setValue?: Dispatch<SetStateAction<number | undefined>> // used for value on hover
topLeft?: ReactNode | undefined
topRight?: ReactNode | undefined
bottomLeft?: ReactNode | undefined
bottomRight?: ReactNode | undefined
} & React.HTMLAttributes<HTMLDivElement>
const LineChart = ({
data,
color = '#56B2A4',
setValue,
topLeft,
topRight,
bottomLeft,
bottomRight,
height = DEFAULT_HEIGHT,
...rest
}: LineChartProps) => {
const theme = useTheme()
const chartRef = useRef<HTMLDivElement>(null)
const [chartCreated, setChart] = useState<IChartApi | undefined>()
// for reseting value on hover exit
const currenValue = data[data.length - 1].value
const handleResize = useCallback(() => {
if (chartCreated && chartRef?.current?.parentElement) {
chartCreated.resize(chartRef.current.parentElement.clientWidth - 32, height)
chartCreated.timeScale().fitContent()
chartCreated.timeScale().scrollToPosition(0, false)
}
}, [chartCreated, chartRef, height])
// add event listener for resize
const isClient = typeof window === 'object'
useEffect(() => {
if (!isClient) {
return
}
window.addEventListener('resize', handleResize)
return () => window.removeEventListener('resize', handleResize)
}, [isClient, chartRef, handleResize]) // Empty array ensures that effect is only run on mount and unmount
const textColor = theme.text2
// if chart not instantiated in canvas, create it
useEffect(() => {
if (!chartCreated && data && !!chartRef?.current?.parentElement) {
const chart = createChart(chartRef.current, {
height: height,
width: chartRef.current.parentElement.clientWidth - 32,
layout: {
backgroundColor: 'transparent',
textColor: textColor,
fontFamily: 'Inter',
},
rightPriceScale: {
scaleMargins: {
top: 0.1,
bottom: 0.1,
},
borderVisible: false,
},
timeScale: {
borderVisible: false,
},
watermark: {
color: 'rgba(0, 0, 0, 0)',
},
grid: {
horzLines: {
visible: false,
},
vertLines: {
visible: false,
},
},
crosshair: {
horzLine: {
visible: true,
style: 3,
width: 1,
color: '#505050',
labelBackgroundColor: color,
},
vertLine: {
visible: true,
style: 3,
width: 1,
color: '#505050',
labelBackgroundColor: color,
},
},
})
const series = chart.addAreaSeries({
lineColor: color,
topColor: darken(0.4, color),
bottomColor: theme.bg0,
lineWidth: 2,
priceLineVisible: false,
})
series.setData(data)
// update the title when hovering on the chart
chart.subscribeCrosshairMove(function (param) {
if (
chartRef?.current &&
(param === undefined ||
param.time === undefined ||
(param && param.point && param.point.x < 0) ||
(param && param.point && param.point.x > chartRef.current.clientWidth) ||
(param && param.point && param.point.y < 0) ||
(param && param.point && param.point.y > height))
) {
setValue && setValue(currenValue)
} else {
const price = parseFloat(param.seriesPrices.get(series)?.toString() ?? currenValue)
setValue && setValue(price)
}
})
chart.timeScale().fitContent()
setChart(chart)
}
}, [color, chartCreated, currenValue, data, height, setValue, textColor, theme])
return (
<Wrapper>
<RowBetween>
{topLeft ?? null}
{topRight ?? null}
</RowBetween>
<div ref={chartRef} id={'line-chart'} {...rest} />
<RowBetween>
{bottomLeft ?? null}
{bottomRight ?? null}
</RowBetween>
</Wrapper>
)
}
export default LineChart

View File

@@ -0,0 +1,44 @@
import React, { useMemo } from 'react'
import { area, curveStepAfter, ScaleLinear } from 'd3'
import styled from 'styled-components/macro'
import { ChartEntry } from './types'
import inRange from 'lodash/inRange'
const Path = styled.path<{ fill: string | undefined }>`
opacity: 0.5;
stroke: ${({ fill, theme }) => fill ?? theme.blue2};
fill: ${({ fill, theme }) => fill ?? theme.blue2};
`
export const Area = ({
series,
xScale,
yScale,
xValue,
yValue,
fill,
}: {
series: ChartEntry[]
xScale: ScaleLinear<number, number>
yScale: ScaleLinear<number, number>
xValue: (d: ChartEntry) => number
yValue: (d: ChartEntry) => number
fill?: string | undefined
}) =>
useMemo(
() => (
<Path
fill={fill}
d={
area()
.curve(curveStepAfter)
.x((d: unknown) => xScale(xValue(d as ChartEntry)))
.y1((d: unknown) => yScale(yValue(d as ChartEntry)))
.y0(yScale(0))(
series.filter((d) => inRange(xScale(xValue(d)), 0, innerWidth)) as Iterable<[number, number]>
) ?? undefined
}
/>
),
[fill, series, xScale, xValue, yScale, yValue]
)

View File

@@ -0,0 +1,43 @@
import React, { useMemo } from 'react'
import { Axis as d3Axis, axisBottom, NumberValue, ScaleLinear, select } from 'd3'
import styled from 'styled-components/macro'
const StyledGroup = styled.g`
line {
display: none;
}
text {
color: ${({ theme }) => theme.text2};
transform: translateY(5px);
}
`
const Axis = ({ axisGenerator }: { axisGenerator: d3Axis<NumberValue> }) => {
const axisRef = (axis: SVGGElement) => {
axis &&
select(axis)
.call(axisGenerator)
.call((g) => g.select('.domain').remove())
}
return <g ref={axisRef} />
}
export const AxisBottom = ({
xScale,
innerHeight,
offset = 0,
}: {
xScale: ScaleLinear<number, number>
innerHeight: number
offset?: number
}) =>
useMemo(
() => (
<StyledGroup transform={`translate(0, ${innerHeight + offset})`}>
<Axis axisGenerator={axisBottom(xScale).ticks(6)} />
</StyledGroup>
),
[innerHeight, offset, xScale]
)

View File

@@ -0,0 +1,265 @@
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { BrushBehavior, brushX, D3BrushEvent, ScaleLinear, select } from 'd3'
import styled from 'styled-components/macro'
import { brushHandleAccentPath, brushHandlePath, OffScreenHandle } from 'components/LiquidityChartRangeInput/svg'
import usePrevious from 'hooks/usePrevious'
const Handle = styled.path<{ color: string }>`
cursor: ew-resize;
pointer-events: none;
stroke-width: 3;
stroke: ${({ color }) => color};
fill: ${({ color }) => color};
`
const HandleAccent = styled.path`
cursor: ew-resize;
pointer-events: none;
stroke-width: 1.5;
stroke: ${({ theme }) => theme.white};
opacity: 0.6;
`
const LabelGroup = styled.g<{ visible: boolean }>`
opacity: ${({ visible }) => (visible ? '1' : '0')};
transition: opacity 300ms;
`
const TooltipBackground = styled.rect`
fill: ${({ theme }) => theme.bg2};
`
const Tooltip = styled.text`
text-anchor: middle;
font-size: 13px;
fill: ${({ theme }) => theme.text1};
`
// flips the handles draggers when close to the container edges
const FLIP_HANDLE_THRESHOLD_PX = 20
// margin to prevent tick snapping from putting the brush off screen
const BRUSH_EXTENT_MARGIN_PX = 2
const compare = (a1: [number, number], a2: [number, number], xScale: ScaleLinear<number, number>): boolean =>
xScale(a1[0]) !== xScale(a2[0]) || xScale(a1[1]) !== xScale(a2[1])
export const Brush = ({
id,
xScale,
interactive,
brushLabelValue,
brushExtent,
setBrushExtent,
innerWidth,
innerHeight,
westHandleColor,
eastHandleColor,
}: {
id: string
xScale: ScaleLinear<number, number>
interactive: boolean
brushLabelValue: (d: 'w' | 'e', x: number) => string
brushExtent: [number, number]
setBrushExtent: (extent: [number, number], mode: string | undefined) => void
innerWidth: number
innerHeight: number
westHandleColor: string
eastHandleColor: string
}) => {
const brushRef = useRef<SVGGElement | null>(null)
const brushBehavior = useRef<BrushBehavior<SVGGElement> | null>(null)
// only used to drag the handles on brush for performance
const [localBrushExtent, setLocalBrushExtent] = useState<[number, number] | null>(brushExtent)
const [showLabels, setShowLabels] = useState(false)
const [hovering, setHovering] = useState(false)
const previousBrushExtent = usePrevious(brushExtent)
const brushed = useCallback(
(event: D3BrushEvent<unknown>) => {
const { type, selection, mode } = event
if (!selection) {
setLocalBrushExtent(null)
return
}
const scaled = (selection as [number, number]).map(xScale.invert) as [number, number]
// avoid infinite render loop by checking for change
if (type === 'end' && compare(brushExtent, scaled, xScale)) {
setBrushExtent(scaled, mode)
}
setLocalBrushExtent(scaled)
},
[xScale, brushExtent, setBrushExtent]
)
// keep local and external brush extent in sync
// i.e. snap to ticks on bruhs end
useEffect(() => {
setLocalBrushExtent(brushExtent)
}, [brushExtent])
// initialize the brush
useEffect(() => {
if (!brushRef.current) return
brushBehavior.current = brushX<SVGGElement>()
.extent([
[Math.max(0 + BRUSH_EXTENT_MARGIN_PX, xScale(0)), 0],
[innerWidth - BRUSH_EXTENT_MARGIN_PX, innerHeight],
])
.handleSize(30)
.filter(() => interactive)
.on('brush end', brushed)
brushBehavior.current(select(brushRef.current))
if (previousBrushExtent && compare(brushExtent, previousBrushExtent, xScale)) {
select(brushRef.current)
.transition()
.call(brushBehavior.current.move as any, brushExtent.map(xScale))
}
// brush linear gradient
select(brushRef.current)
.selectAll('.selection')
.attr('stroke', 'none')
.attr('fill-opacity', '0.1')
.attr('fill', `url(#${id}-gradient-selection)`)
}, [brushExtent, brushed, id, innerHeight, innerWidth, interactive, previousBrushExtent, xScale])
// respond to xScale changes only
useEffect(() => {
if (!brushRef.current || !brushBehavior.current) return
brushBehavior.current.move(select(brushRef.current) as any, brushExtent.map(xScale) as any)
}, [brushExtent, xScale])
// show labels when local brush changes
useEffect(() => {
setShowLabels(true)
const timeout = setTimeout(() => setShowLabels(false), 1500)
return () => clearTimeout(timeout)
}, [localBrushExtent])
// variables to help render the SVGs
const flipWestHandle = localBrushExtent && xScale(localBrushExtent[0]) > FLIP_HANDLE_THRESHOLD_PX
const flipEastHandle = localBrushExtent && xScale(localBrushExtent[1]) > innerWidth - FLIP_HANDLE_THRESHOLD_PX
const showWestArrow = localBrushExtent && (xScale(localBrushExtent[0]) < 0 || xScale(localBrushExtent[1]) < 0)
const showEastArrow =
localBrushExtent && (xScale(localBrushExtent[0]) > innerWidth || xScale(localBrushExtent[1]) > innerWidth)
const westHandleInView =
localBrushExtent && xScale(localBrushExtent[0]) >= 0 && xScale(localBrushExtent[0]) <= innerWidth
const eastHandleInView =
localBrushExtent && xScale(localBrushExtent[1]) >= 0 && xScale(localBrushExtent[1]) <= innerWidth
return useMemo(
() => (
<>
<defs>
<linearGradient id={`${id}-gradient-selection`} x1="0%" y1="100%" x2="100%" y2="100%">
<stop stopColor={westHandleColor} />
<stop stopColor={eastHandleColor} offset="1" />
</linearGradient>
{/* clips at exactly the svg area */}
<clipPath id={`${id}-brush-clip`}>
<rect x="0" y="0" width={innerWidth} height={innerHeight} />
</clipPath>
</defs>
{/* will host the d3 brush */}
<g
ref={brushRef}
clipPath={`url(#${id}-brush-clip)`}
onMouseEnter={() => setHovering(true)}
onMouseLeave={() => setHovering(false)}
/>
{/* custom brush handles */}
{localBrushExtent && (
<>
{/* west handle */}
{westHandleInView ? (
<g
transform={`translate(${Math.max(0, xScale(localBrushExtent[0]))}, 0), scale(${
flipWestHandle ? '-1' : '1'
}, 1)`}
>
<g>
<Handle color={westHandleColor} d={brushHandlePath(innerHeight)} />
<HandleAccent d={brushHandleAccentPath()} />
</g>
<LabelGroup
transform={`translate(50,0), scale(${flipWestHandle ? '1' : '-1'}, 1)`}
visible={showLabels || hovering}
>
<TooltipBackground y="0" x="-30" height="30" width="60" rx="8" />
<Tooltip transform={`scale(-1, 1)`} y="15" dominantBaseline="middle">
{brushLabelValue('w', localBrushExtent[0])}
</Tooltip>
</LabelGroup>
</g>
) : null}
{/* east handle */}
{eastHandleInView ? (
<g transform={`translate(${xScale(localBrushExtent[1])}, 0), scale(${flipEastHandle ? '-1' : '1'}, 1)`}>
<g>
<Handle color={eastHandleColor} d={brushHandlePath(innerHeight)} />
<HandleAccent d={brushHandleAccentPath()} />
</g>
<LabelGroup
transform={`translate(50,0), scale(${flipEastHandle ? '-1' : '1'}, 1)`}
visible={showLabels || hovering}
>
<TooltipBackground y="0" x="-30" height="30" width="60" rx="8" />
<Tooltip y="15" dominantBaseline="middle">
{brushLabelValue('e', localBrushExtent[1])}
</Tooltip>
</LabelGroup>
</g>
) : null}
{showWestArrow && <OffScreenHandle color={westHandleColor} />}
{showEastArrow && (
<g transform={`translate(${innerWidth}, 0) scale(-1, 1)`}>
<OffScreenHandle color={eastHandleColor} />
</g>
)}
</>
)}
</>
),
[
brushLabelValue,
eastHandleColor,
eastHandleInView,
flipEastHandle,
flipWestHandle,
hovering,
id,
innerHeight,
innerWidth,
localBrushExtent,
showEastArrow,
showLabels,
showWestArrow,
westHandleColor,
westHandleInView,
xScale,
]
)
}

View File

@@ -0,0 +1,146 @@
import { max, scaleLinear, ZoomTransform } from 'd3'
import { useEffect, useMemo, useRef, useState } from 'react'
import { Bound } from 'state/mint/v3/actions'
import { Area } from './Area'
import { AxisBottom } from './AxisBottom'
import { Brush } from './Brush'
import { Line } from './Line'
import { ChartEntry, LiquidityChartRangeInputProps } from './types'
import Zoom, { ZoomOverlay } from './Zoom'
export const xAccessor = (d: ChartEntry) => d.price0
export const yAccessor = (d: ChartEntry) => d.activeLiquidity
export function Chart({
id = 'liquidityChartRangeInput',
data: { series, current },
ticksAtLimit,
styles,
dimensions: { width, height },
margins,
interactive = true,
brushDomain,
brushLabels,
onBrushDomainChange,
zoomLevels,
}: LiquidityChartRangeInputProps) {
const zoomRef = useRef<SVGRectElement | null>(null)
const [zoom, setZoom] = useState<ZoomTransform | null>(null)
const [innerHeight, innerWidth] = useMemo(
() => [height - margins.top - margins.bottom, width - margins.left - margins.right],
[width, height, margins]
)
const { xScale, yScale } = useMemo(() => {
const scales = {
xScale: scaleLinear()
.domain([current * zoomLevels.initialMin, current * zoomLevels.initialMax] as number[])
.range([0, innerWidth]),
yScale: scaleLinear()
.domain([0, max(series, yAccessor)] as number[])
.range([innerHeight, 0]),
}
if (zoom) {
const newXscale = zoom.rescaleX(scales.xScale)
scales.xScale.domain(newXscale.domain())
}
return scales
}, [current, zoomLevels.initialMin, zoomLevels.initialMax, innerWidth, series, innerHeight, zoom])
useEffect(() => {
// reset zoom as necessary
setZoom(null)
}, [zoomLevels])
useEffect(() => {
if (!brushDomain) {
onBrushDomainChange(xScale.domain() as [number, number], undefined)
}
}, [brushDomain, onBrushDomainChange, xScale])
return (
<>
<Zoom
svg={zoomRef.current}
xScale={xScale}
setZoom={setZoom}
width={innerWidth}
height={
// allow zooming inside the x-axis
height
}
resetBrush={() => {
onBrushDomainChange(
[current * zoomLevels.initialMin, current * zoomLevels.initialMax] as [number, number],
'reset'
)
}}
showResetButton={Boolean(ticksAtLimit[Bound.LOWER] || ticksAtLimit[Bound.UPPER])}
zoomLevels={zoomLevels}
/>
<svg width="100%" height="100%" viewBox={`0 0 ${width} ${height}`} style={{ overflow: 'visible' }}>
<defs>
<clipPath id={`${id}-chart-clip`}>
<rect x="0" y="0" width={innerWidth} height={height} />
</clipPath>
{brushDomain && (
// mask to highlight selected area
<mask id={`${id}-chart-area-mask`}>
<rect
fill="white"
x={xScale(brushDomain[0])}
y="0"
width={xScale(brushDomain[1]) - xScale(brushDomain[0])}
height={innerHeight}
/>
</mask>
)}
</defs>
<g transform={`translate(${margins.left},${margins.top})`}>
<g clipPath={`url(#${id}-chart-clip)`}>
<Area series={series} xScale={xScale} yScale={yScale} xValue={xAccessor} yValue={yAccessor} />
{brushDomain && (
// duplicate area chart with mask for selected area
<g mask={`url(#${id}-chart-area-mask)`}>
<Area
series={series}
xScale={xScale}
yScale={yScale}
xValue={xAccessor}
yValue={yAccessor}
fill={styles.area.selection}
/>
</g>
)}
<Line value={current} xScale={xScale} innerHeight={innerHeight} />
<AxisBottom xScale={xScale} innerHeight={innerHeight} />
</g>
<ZoomOverlay width={innerWidth} height={height} ref={zoomRef} />
<Brush
id={id}
xScale={xScale}
interactive={interactive}
brushLabelValue={brushLabels}
brushExtent={brushDomain ?? (xScale.domain() as [number, number])}
innerWidth={innerWidth}
innerHeight={innerHeight}
setBrushExtent={onBrushDomainChange}
westHandleColor={styles.brush.handle.west}
eastHandleColor={styles.brush.handle.east}
/>
</g>
</svg>
</>
)
}

View File

@@ -0,0 +1,24 @@
import React, { useMemo } from 'react'
import { ScaleLinear } from 'd3'
import styled from 'styled-components/macro'
const StyledLine = styled.line`
opacity: 0.5;
stroke-width: 2;
stroke: ${({ theme }) => theme.text1};
fill: none;
`
export const Line = ({
value,
xScale,
innerHeight,
}: {
value: number
xScale: ScaleLinear<number, number>
innerHeight: number
}) =>
useMemo(
() => <StyledLine x1={xScale(value)} y1="0" x2={xScale(value)} y2={innerHeight} />,
[value, xScale, innerHeight]
)

View File

@@ -0,0 +1,130 @@
import React, { useEffect, useMemo, useRef } from 'react'
import { ButtonGray } from 'components/Button'
import styled from 'styled-components/macro'
import { ScaleLinear, select, ZoomBehavior, zoom, ZoomTransform, zoomIdentity } from 'd3'
import { RefreshCcw, ZoomIn, ZoomOut } from 'react-feather'
import { ZoomLevels } from './types'
const Wrapper = styled.div<{ count: number }>`
display: grid;
grid-template-columns: repeat(${({ count }) => count.toString()}, 1fr);
grid-gap: 6px;
position: absolute;
top: -75px;
right: 0;
`
const Button = styled(ButtonGray)`
&:hover {
background-color: ${({ theme }) => theme.bg2};
color: ${({ theme }) => theme.text1};
}
width: 32px;
height: 32px;
padding: 4px;
`
export const ZoomOverlay = styled.rect`
fill: transparent;
cursor: grab;
&:active {
cursor: grabbing;
}
`
export default function Zoom({
svg,
xScale,
setZoom,
width,
height,
resetBrush,
showResetButton,
zoomLevels,
}: {
svg: SVGElement | null
xScale: ScaleLinear<number, number>
setZoom: (transform: ZoomTransform) => void
width: number
height: number
resetBrush: () => void
showResetButton: boolean
zoomLevels: ZoomLevels
}) {
const zoomBehavior = useRef<ZoomBehavior<Element, unknown>>()
const [zoomIn, zoomOut, zoomInitial, zoomReset] = useMemo(
() => [
() =>
svg &&
zoomBehavior.current &&
select(svg as Element)
.transition()
.call(zoomBehavior.current.scaleBy, 2),
() =>
svg &&
zoomBehavior.current &&
select(svg as Element)
.transition()
.call(zoomBehavior.current.scaleBy, 0.5),
() =>
svg &&
zoomBehavior.current &&
select(svg as Element)
.transition()
.call(zoomBehavior.current.scaleTo, 0.5),
() =>
svg &&
zoomBehavior.current &&
select(svg as Element)
.call(zoomBehavior.current.transform, zoomIdentity.translate(0, 0).scale(1))
.transition()
.call(zoomBehavior.current.scaleTo, 0.5),
],
[svg]
)
useEffect(() => {
if (!svg) return
zoomBehavior.current = zoom()
.scaleExtent([zoomLevels.min, zoomLevels.max])
.extent([
[0, 0],
[width, height],
])
.on('zoom', ({ transform }: { transform: ZoomTransform }) => setZoom(transform))
select(svg as Element).call(zoomBehavior.current)
}, [height, width, setZoom, svg, xScale, zoomBehavior, zoomLevels, zoomLevels.max, zoomLevels.min])
useEffect(() => {
// reset zoom to initial on zoomLevel change
zoomInitial()
}, [zoomInitial, zoomLevels])
return (
<Wrapper count={showResetButton ? 3 : 2}>
{showResetButton && (
<Button
onClick={() => {
resetBrush()
zoomReset()
}}
disabled={false}
>
<RefreshCcw size={16} />
</Button>
)}
<Button onClick={zoomIn} disabled={false}>
<ZoomIn size={16} />
</Button>
<Button onClick={zoomOut} disabled={false}>
<ZoomOut size={16} />
</Button>
</Wrapper>
)
}

View File

@@ -0,0 +1,56 @@
import { useCallback, useMemo } from 'react'
import { Currency } from '@uniswap/sdk-core'
import { FeeAmount } from '@uniswap/v3-sdk'
import { usePoolActiveLiquidity } from 'hooks/usePoolTickData'
import { ChartEntry } from './types'
import JSBI from 'jsbi'
export interface TickProcessed {
liquidityActive: JSBI
price0: string
}
export function useDensityChartData({
currencyA,
currencyB,
feeAmount,
}: {
currencyA: Currency | undefined
currencyB: Currency | undefined
feeAmount: FeeAmount | undefined
}) {
const { isLoading, isUninitialized, isError, error, data } = usePoolActiveLiquidity(currencyA, currencyB, feeAmount)
const formatData = useCallback(() => {
if (!data?.length) {
return undefined
}
const newData: ChartEntry[] = []
for (let i = 0; i < data.length; i++) {
const t: TickProcessed = data[i]
const chartEntry = {
activeLiquidity: parseFloat(t.liquidityActive.toString()),
price0: parseFloat(t.price0),
}
if (chartEntry.activeLiquidity > 0) {
newData.push(chartEntry)
}
}
return newData
}, [data])
return useMemo(() => {
return {
isLoading,
isUninitialized,
isError,
error,
formattedData: !isLoading && !isUninitialized ? formatData() : undefined,
}
}, [isLoading, isUninitialized, isError, error, formatData])
}

View File

@@ -0,0 +1,208 @@
import React, { ReactNode, useCallback, useMemo } from 'react'
import { Trans } from '@lingui/macro'
import { Currency, Price, Token } from '@uniswap/sdk-core'
import { AutoColumn, ColumnCenter } from 'components/Column'
import Loader from 'components/Loader'
import { useColor } from 'hooks/useColor'
import useTheme from 'hooks/useTheme'
import { saturate } from 'polished'
import { BarChart2, Inbox, CloudOff } from 'react-feather'
import { batch } from 'react-redux'
import styled from 'styled-components/macro'
import { TYPE } from '../../theme'
import { Chart } from './Chart'
import { useDensityChartData } from './hooks'
import { format } from 'd3'
import { Bound } from 'state/mint/v3/actions'
import { FeeAmount } from '@uniswap/v3-sdk'
import ReactGA from 'react-ga'
import { ZoomLevels } from './types'
const ZOOM_LEVELS: Record<FeeAmount, ZoomLevels> = {
[FeeAmount.LOW]: {
initialMin: 0.999,
initialMax: 1.001,
min: 0.00001,
max: 1.5,
},
[FeeAmount.MEDIUM]: {
initialMin: 0.5,
initialMax: 2,
min: 0.00001,
max: 20,
},
[FeeAmount.HIGH]: {
initialMin: 0.5,
initialMax: 2,
min: 0.00001,
max: 20,
},
}
const ChartWrapper = styled.div`
position: relative;
justify-content: center;
align-content: center;
`
function InfoBox({ message, icon }: { message?: ReactNode; icon: ReactNode }) {
return (
<ColumnCenter style={{ height: '100%', justifyContent: 'center' }}>
{icon}
{message && (
<TYPE.mediumHeader padding={10} marginTop="20px" textAlign="center">
{message}
</TYPE.mediumHeader>
)}
</ColumnCenter>
)
}
export default function LiquidityChartRangeInput({
currencyA,
currencyB,
feeAmount,
ticksAtLimit,
price,
priceLower,
priceUpper,
onLeftRangeInput,
onRightRangeInput,
interactive,
}: {
currencyA: Currency | undefined
currencyB: Currency | undefined
feeAmount?: FeeAmount
ticksAtLimit: { [bound in Bound]?: boolean | undefined }
price: number | undefined
priceLower?: Price<Token, Token>
priceUpper?: Price<Token, Token>
onLeftRangeInput: (typedValue: string) => void
onRightRangeInput: (typedValue: string) => void
interactive: boolean
}) {
const theme = useTheme()
const tokenAColor = useColor(currencyA?.wrapped)
const tokenBColor = useColor(currencyB?.wrapped)
const isSorted = currencyA && currencyB && currencyA?.wrapped.sortsBefore(currencyB?.wrapped)
const { isLoading, isUninitialized, isError, error, formattedData } = useDensityChartData({
currencyA,
currencyB,
feeAmount,
})
const onBrushDomainChangeEnded = useCallback(
(domain, mode) => {
let leftRangeValue = Number(domain[0])
const rightRangeValue = Number(domain[1])
if (leftRangeValue <= 0) {
leftRangeValue = 1 / 10 ** 6
}
batch(() => {
// simulate user input for auto-formatting and other validations
if (
(!ticksAtLimit[isSorted ? Bound.LOWER : Bound.UPPER] || mode === 'handle' || mode === 'reset') &&
leftRangeValue > 0
) {
onLeftRangeInput(leftRangeValue.toFixed(6))
}
if ((!ticksAtLimit[isSorted ? Bound.UPPER : Bound.LOWER] || mode === 'reset') && rightRangeValue > 0) {
// todo: remove this check. Upper bound for large numbers
// sometimes fails to parse to tick.
if (rightRangeValue < 1e35) {
onRightRangeInput(rightRangeValue.toFixed(6))
}
}
})
},
[isSorted, onLeftRangeInput, onRightRangeInput, ticksAtLimit]
)
interactive = interactive && Boolean(formattedData?.length)
const brushDomain: [number, number] | undefined = useMemo(() => {
const leftPrice = isSorted ? priceLower : priceUpper?.invert()
const rightPrice = isSorted ? priceUpper : priceLower?.invert()
return leftPrice && rightPrice
? [parseFloat(leftPrice?.toSignificant(6)), parseFloat(rightPrice?.toSignificant(6))]
: undefined
}, [isSorted, priceLower, priceUpper])
const brushLabelValue = useCallback(
(d: 'w' | 'e', x: number) => {
if (!price) return ''
if (d === 'w' && ticksAtLimit[isSorted ? Bound.LOWER : Bound.UPPER]) return '0'
if (d === 'e' && ticksAtLimit[isSorted ? Bound.UPPER : Bound.LOWER]) return '∞'
const percent = (x < price ? -1 : 1) * ((Math.max(x, price) - Math.min(x, price)) / price) * 100
return price ? `${format(Math.abs(percent) > 1 ? '.2~s' : '.2~f')(percent)}%` : ''
},
[isSorted, price, ticksAtLimit]
)
if (isError) {
ReactGA.exception({
...error,
category: 'Liquidity',
fatal: false,
})
}
return (
<AutoColumn gap="md" style={{ minHeight: '200px' }}>
{isUninitialized ? (
<InfoBox
message={<Trans>Your position will appear here.</Trans>}
icon={<Inbox size={56} stroke={theme.text1} />}
/>
) : isLoading ? (
<InfoBox icon={<Loader size="40px" stroke={theme.text4} />} />
) : isError ? (
<InfoBox
message={<Trans>Liquidity data not available.</Trans>}
icon={<CloudOff size={56} stroke={theme.text4} />}
/>
) : !formattedData || formattedData === [] || !price ? (
<InfoBox
message={<Trans>There is no liquidity data.</Trans>}
icon={<BarChart2 size={56} stroke={theme.text4} />}
/>
) : (
<ChartWrapper>
<Chart
data={{ series: formattedData, current: price }}
dimensions={{ width: 400, height: 200 }}
margins={{ top: 10, right: 2, bottom: 20, left: 0 }}
styles={{
area: {
selection: theme.blue1,
},
brush: {
handle: {
west: saturate(0.1, tokenAColor) ?? theme.red1,
east: saturate(0.1, tokenBColor) ?? theme.blue1,
},
},
}}
interactive={interactive}
brushLabels={brushLabelValue}
brushDomain={brushDomain}
onBrushDomainChange={onBrushDomainChangeEnded}
zoomLevels={ZOOM_LEVELS[feeAmount ?? FeeAmount.MEDIUM]}
ticksAtLimit={ticksAtLimit}
/>
</ChartWrapper>
)}
</AutoColumn>
)
}

View File

@@ -0,0 +1,61 @@
/*
* Generates an SVG path for the east brush handle.
* Apply `scale(-1, 1)` to generate west brush handle.
*
* |```````\
* | | | |
* |______/
* |
* |
* |
* |
* |
*
* https://medium.com/@dennismphil/one-side-rounded-rectangle-using-svg-fb31cf318d90
*/
export const brushHandlePath = (height: number) =>
[
// handle
`M 0 0`, // move to origin
`v ${height}`, // vertical line
'm 1 0', // move 1px to the right
`V 0`, // second vertical line
`M 0 1`, // move to origin
// head
'h 12', // horizontal line
'q 2 0, 2 2', // rounded corner
'v 22', // vertical line
'q 0 2 -2 2', // rounded corner
'h -12', // horizontal line
`z`, // close path
].join(' ')
export const brushHandleAccentPath = () =>
[
'm 5 7', // move to first accent
'v 14', // vertical line
'M 0 0', // move to origin
'm 9 7', // move to second accent
'v 14', // vertical line
'z',
].join(' ')
export const OffScreenHandle = ({
color,
size = 10,
margin = 10,
}: {
color: string
size?: number
margin?: number
}) => (
<polygon
points={`0 0, ${size} ${size}, 0 ${size}`}
transform={` translate(${size + margin}, ${margin}) rotate(45) `}
fill={color}
stroke={color}
strokeWidth="4"
strokeLinejoin="round"
/>
)

View File

@@ -0,0 +1,61 @@
import { Bound } from 'state/mint/v3/actions'
export interface ChartEntry {
activeLiquidity: number
price0: number
}
export interface Dimensions {
width: number
height: number
}
export interface Margins {
top: number
right: number
bottom: number
left: number
}
export interface ZoomLevels {
initialMin: number
initialMax: number
min: number
max: number
}
export interface LiquidityChartRangeInputProps {
// to distringuish between multiple charts in the DOM
id?: string
data: {
series: ChartEntry[]
current: number
}
ticksAtLimit: { [bound in Bound]?: boolean | undefined }
styles: {
area: {
// color of the ticks in range
selection: string
}
brush: {
handle: {
west: string
east: string
}
}
}
dimensions: Dimensions
margins: Margins
interactive?: boolean
brushLabels: (d: 'w' | 'e', x: number) => string
brushDomain: [number, number] | undefined
onBrushDomainChange: (domain: [number, number], mode: string | undefined) => void
zoomLevels: ZoomLevels
}

View File

@@ -1,4 +1,4 @@
import styled, { keyframes } from 'styled-components'
import styled, { keyframes } from 'styled-components/macro'
const rotate = keyframes`
from {

View File

@@ -1,7 +1,8 @@
import React, { useRef } from 'react'
import { BookOpen, Code, Info, MessageCircle, PieChart } from 'react-feather'
import { t } from '@lingui/macro'
import React, { useEffect, useRef, useState } from 'react'
import { BookOpen, Code, Info, MessageCircle, PieChart, Moon, Sun, Globe, ChevronLeft, Check } from 'react-feather'
import { Link } from 'react-router-dom'
import styled, { css } from 'styled-components'
import styled, { css } from 'styled-components/macro'
import { ReactComponent as MenuIcon } from '../../assets/images/menu.svg'
import { useActiveWeb3React } from '../../hooks/web3'
import { useOnClickOutside } from '../../hooks/useOnClickOutside'
@@ -10,7 +11,12 @@ import { useModalOpen, useToggleModal } from '../../state/application/hooks'
import { Trans } from '@lingui/macro'
import { ExternalLink } from '../../theme'
import { ButtonPrimary } from '../Button'
import { L2_CHAIN_IDS } from 'constants/chains'
import { useDarkModeManager } from 'state/user/hooks'
import { L2_CHAIN_IDS, CHAIN_INFO, SupportedChainId } from 'constants/chains'
import { LOCALE_LABEL, SupportedLocale, SUPPORTED_LOCALES } from 'constants/locales'
import { useLocationLinkProps } from 'hooks/useLocationLinkProps'
import { useActiveLocale } from 'hooks/useActiveLocale'
export enum FlyoutAlignment {
LEFT = 'LEFT',
@@ -30,17 +36,18 @@ const StyledMenuButton = styled.button`
background-color: transparent;
margin: 0;
padding: 0;
height: 35px;
background-color: ${({ theme }) => theme.bg2};
height: 38px;
background-color: ${({ theme }) => theme.bg0};
border: 1px solid ${({ theme }) => theme.bg0};
padding: 0.15rem 0.5rem;
border-radius: 0.5rem;
border-radius: 12px;
:hover,
:focus {
cursor: pointer;
outline: none;
background-color: ${({ theme }) => theme.bg3};
border: 1px solid ${({ theme }) => theme.bg3};
}
svg {
@@ -65,18 +72,22 @@ const StyledMenu = styled.div`
`
const MenuFlyout = styled.span<{ flyoutAlignment?: FlyoutAlignment }>`
min-width: 8.125rem;
background-color: ${({ theme }) => theme.bg2};
min-width: 196px;
max-height: 350px;
overflow: auto;
background-color: ${({ theme }) => theme.bg1};
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),
0px 24px 32px rgba(0, 0, 0, 0.01);
border: 1px solid ${({ theme }) => theme.bg0};
border-radius: 12px;
padding: 0.5rem;
display: flex;
flex-direction: column;
font-size: 1rem;
font-size: 16px;
position: absolute;
top: 3rem;
z-index: 100;
${({ flyoutAlignment = FlyoutAlignment.RIGHT }) =>
flyoutAlignment === FlyoutAlignment.RIGHT
? css`
@@ -86,8 +97,9 @@ const MenuFlyout = styled.span<{ flyoutAlignment?: FlyoutAlignment }>`
left: 0rem;
`};
${({ theme }) => theme.mediaWidth.upToMedium`
top: unset;
bottom: 3em
bottom: unset;
right: 0;
left: unset;
`};
`
@@ -97,15 +109,13 @@ const MenuItem = styled(ExternalLink)`
flex-direction: row;
align-items: center;
padding: 0.5rem 0.5rem;
justify-content: space-between;
color: ${({ theme }) => theme.text2};
:hover {
color: ${({ theme }) => theme.text1};
cursor: pointer;
text-decoration: none;
}
> svg {
margin-right: 8px;
}
`
const InternalMenuItem = styled(Link)`
@@ -122,8 +132,71 @@ const InternalMenuItem = styled(Link)`
}
`
const InternalLinkMenuItem = styled(InternalMenuItem)`
display: flex;
flex-direction: row;
align-items: center;
padding: 0.5rem 0.5rem;
justify-content: space-between;
text-decoration: none;
:hover {
color: ${({ theme }) => theme.text1};
cursor: pointer;
text-decoration: none;
}
`
const ToggleMenuItem = styled.button`
background-color: transparent;
margin: 0;
padding: 0;
border: none;
display: flex;
flex: 1;
flex-direction: row;
align-items: center;
padding: 0.5rem 0.5rem;
justify-content: space-between;
font-size: 1rem;
font-weight: 500;
color: ${({ theme }) => theme.text2};
:hover {
color: ${({ theme }) => theme.text1};
cursor: pointer;
text-decoration: none;
}
`
const CODE_LINK = 'https://github.com/Uniswap/uniswap-interface'
function LanguageMenuItem({ locale, active, key }: { locale: SupportedLocale; active: boolean; key: string }) {
const { to, onClick } = useLocationLinkProps(locale)
if (!to) return null
return (
<InternalLinkMenuItem onClick={onClick} key={key} to={to}>
<div>{LOCALE_LABEL[locale]}</div>
{active && <Check opacity={0.6} size={16} />}
</InternalLinkMenuItem>
)
}
function LanguageMenu({ close }: { close: () => void }) {
const activeLocale = useActiveLocale()
return (
<MenuFlyout>
<ToggleMenuItem onClick={close}>
<ChevronLeft size={16} />
</ToggleMenuItem>
{SUPPORTED_LOCALES.map((locale) => (
<LanguageMenuItem locale={locale} active={activeLocale === locale} key={locale} />
))}
</MenuFlyout>
)
}
export default function Menu() {
const { account, chainId } = useActiveWeb3React()
@@ -133,53 +206,87 @@ export default function Menu() {
useOnClickOutside(node, open ? toggle : undefined)
const openClaimModal = useToggleModal(ApplicationModal.ADDRESS_CLAIM)
const showUNIClaimOption = Boolean(!!account && !!chainId && !L2_CHAIN_IDS.includes(chainId))
const { infoLink } = CHAIN_INFO[chainId ? chainId : SupportedChainId.MAINNET]
const [darkMode, toggleDarkMode] = useDarkModeManager()
const [menu, setMenu] = useState<'main' | 'lang'>('main')
useEffect(() => {
setMenu('main')
}, [open])
return (
// https://github.com/DefinitelyTyped/DefinitelyTyped/issues/30451
<StyledMenu ref={node as any}>
<StyledMenuButton onClick={toggle}>
<StyledMenuButton onClick={toggle} aria-label={t`Menu`}>
<StyledMenuIcon />
</StyledMenuButton>
{open && (
<MenuFlyout>
<MenuItem href="https://uniswap.org/">
<Info size={14} />
<div>
<Trans>About</Trans>
</div>
</MenuItem>
<MenuItem href="https://docs.uniswap.org/">
<BookOpen size={14} />
<div>
<Trans>Docs</Trans>
</div>
</MenuItem>
<MenuItem href={CODE_LINK}>
<Code size={14} />
<div>
<Trans>Code</Trans>
</div>
</MenuItem>
<MenuItem href="https://discord.gg/FCfyBSbCU5">
<MessageCircle size={14} />
<div>
<Trans>Discord</Trans>
</div>
</MenuItem>
<MenuItem href="https://info.uniswap.org/">
<PieChart size={14} />
<div>
<Trans>Analytics</Trans>
</div>
</MenuItem>
{showUNIClaimOption && (
<UNIbutton onClick={openClaimModal} padding="8px 16px" width="100%" $borderRadius="12px" mt="0.5rem">
<Trans>Claim UNI</Trans>
</UNIbutton>
)}
</MenuFlyout>
)}
{open &&
(() => {
switch (menu) {
case 'lang':
return <LanguageMenu close={() => setMenu('main')} />
case 'main':
default:
return (
<MenuFlyout>
<MenuItem href="https://uniswap.org/">
<div>
<Trans>About</Trans>
</div>
<Info opacity={0.6} size={16} />
</MenuItem>
<MenuItem href="https://docs.uniswap.org/">
<div>
<Trans>Docs</Trans>
</div>
<BookOpen opacity={0.6} size={16} />
</MenuItem>
<MenuItem href={CODE_LINK}>
<div>
<Trans>Code</Trans>
</div>
<Code opacity={0.6} size={16} />
</MenuItem>
<MenuItem href="https://discord.gg/FCfyBSbCU5">
<div>
<Trans>Discord</Trans>
</div>
<MessageCircle opacity={0.6} size={16} />
</MenuItem>
<MenuItem href={infoLink}>
<div>
<Trans>Analytics</Trans>
</div>
<PieChart opacity={0.6} size={16} />
</MenuItem>
<ToggleMenuItem onClick={() => setMenu('lang')}>
<div>
<Trans>Language</Trans>
</div>
<Globe opacity={0.6} size={16} />
</ToggleMenuItem>
<ToggleMenuItem onClick={() => toggleDarkMode()}>
<div>{darkMode ? <Trans>Light Theme</Trans> : <Trans>Dark Theme</Trans>}</div>
{darkMode ? <Moon opacity={0.6} size={16} /> : <Sun opacity={0.6} size={16} />}
</ToggleMenuItem>
{showUNIClaimOption && (
<UNIbutton
onClick={openClaimModal}
padding="8px 16px"
width="100%"
$borderRadius="12px"
mt="0.5rem"
>
<Trans>Claim UNI</Trans>
</UNIbutton>
)}
</MenuFlyout>
)
}
})()}
</StyledMenu>
)
}
@@ -220,11 +327,11 @@ export const NewMenu = ({ flyoutAlignment = FlyoutAlignment.RIGHT, ToggleUI, men
<NewMenuFlyout flyoutAlignment={flyoutAlignment}>
{menuItems.map(({ content, link, external }, i) =>
external ? (
<ExternalMenuItem id="link" href={link} key={link + i}>
<ExternalMenuItem href={link} key={i}>
{content}
</ExternalMenuItem>
) : (
<NewMenuItem id="link" to={link} key={link + i}>
<NewMenuItem to={link} key={i}>
{content}
</NewMenuItem>
)

View File

@@ -1,5 +1,5 @@
import React from 'react'
import styled, { css } from 'styled-components'
import styled, { css } from 'styled-components/macro'
import { animated, useTransition, useSpring } from 'react-spring'
import { DialogOverlay, DialogContent } from '@reach/dialog'
import { isMobile } from 'react-device-detect'

View File

@@ -3,7 +3,7 @@ import { useActiveWeb3React } from '../../hooks/web3'
import { ExplorerDataType, getExplorerLink } from '../../utils/getExplorerLink'
import { AutoColumn, ColumnCenter } from '../Column'
import styled, { ThemeContext } from 'styled-components'
import styled, { ThemeContext } from 'styled-components/macro'
import { RowBetween } from '../Row'
import { TYPE, CloseIcon, CustomLightSpinner } from '../../theme'
import { ArrowUpCircle } from 'react-feather'

View File

@@ -13,6 +13,8 @@ import { resetMintState } from 'state/mint/actions'
import { resetMintState as resetMintV3State } from 'state/mint/v3/actions'
import { TYPE } from 'theme'
import useTheme from 'hooks/useTheme'
import { ReactNode } from 'react'
import { Box } from 'rebass'
const Tabs = styled.div`
${({ theme }) => theme.flexRowNoWrap}
@@ -49,6 +51,15 @@ const StyledNavLink = styled(NavLink).attrs({
}
`
const StyledHistoryLink = styled(HistoryLink)<{ flex: string | undefined }>`
flex: ${({ flex }) => flex ?? 'none'};
${({ theme }) => theme.mediaWidth.upToMedium`
flex: none;
margin-right: 10px;
`};
`
const ActiveText = styled.div`
font-weight: 500;
font-size: 20px;
@@ -74,11 +85,11 @@ export function SwapPoolTabs({ active }: { active: 'swap' | 'pool' }) {
export function FindPoolTabs({ origin }: { origin: string }) {
return (
<Tabs>
<RowBetween style={{ padding: '1rem 1rem 0 1rem' }}>
<RowBetween style={{ padding: '1rem 1rem 0 1rem', position: 'relative' }}>
<HistoryLink to={origin}>
<StyledArrowLeft />
</HistoryLink>
<ActiveText>
<ActiveText style={{ position: 'absolute', left: '50%', transform: 'translateX(-50%)' }}>
<Trans>Import V2 Pool</Trans>
</ActiveText>
</RowBetween>
@@ -91,11 +102,14 @@ export function AddRemoveTabs({
creating,
defaultSlippage,
positionID,
children,
}: {
adding: boolean
creating: boolean
defaultSlippage: Percent
positionID?: string | undefined
showBackLink?: boolean
children?: ReactNode | undefined
}) {
const theme = useTheme()
// reset states on back
@@ -110,7 +124,7 @@ export function AddRemoveTabs({
return (
<Tabs>
<RowBetween style={{ padding: '1rem 1rem 0 1rem' }}>
<HistoryLink
<StyledHistoryLink
to={poolLink}
onClick={() => {
if (adding) {
@@ -119,10 +133,15 @@ export function AddRemoveTabs({
dispatch(resetMintV3State())
}
}}
flex={children ? '1' : undefined}
>
<StyledArrowLeft stroke={theme.text2} />
</HistoryLink>
<TYPE.mediumHeader fontWeight={500} fontSize={20}>
</StyledHistoryLink>
<TYPE.mediumHeader
fontWeight={500}
fontSize={20}
style={{ flex: '1', margin: 'auto', textAlign: children ? 'start' : 'center' }}
>
{creating ? (
<Trans>Create a pair</Trans>
) : adding ? (
@@ -131,6 +150,7 @@ export function AddRemoveTabs({
<Trans>Remove Liquidity</Trans>
)}
</TYPE.mediumHeader>
<Box style={{ marginRight: '.5rem' }}>{children}</Box>
<SettingsTab placeholderSlippage={defaultSlippage} />
</RowBetween>
</Tabs>

View File

@@ -0,0 +1,132 @@
import { Trans } from '@lingui/macro'
import {
ArbitrumWrapperBackgroundDarkMode,
ArbitrumWrapperBackgroundLightMode,
OptimismWrapperBackgroundDarkMode,
OptimismWrapperBackgroundLightMode,
} from 'components/NetworkAlert/NetworkAlert'
import { CHAIN_INFO, L2_CHAIN_IDS, SupportedChainId, SupportedL2ChainId } from 'constants/chains'
import { useActiveWeb3React } from 'hooks/web3'
import { ArrowDownCircle } from 'react-feather'
import { useArbitrumAlphaAlert, useDarkModeManager } from 'state/user/hooks'
import styled from 'styled-components/macro'
import { ExternalLink, MEDIA_WIDTHS } from 'theme'
import { ReadMoreLink } from './styles'
const L2Icon = styled.img`
display: none;
height: 40px;
margin: auto 20px auto 4px;
width: 40px;
@media screen and (min-width: ${MEDIA_WIDTHS.upToMedium}px) {
display: block;
}
`
const DesktopTextBreak = styled.div`
display: none;
@media screen and (min-width: ${MEDIA_WIDTHS.upToMedium}px) {
display: block;
}
`
const Wrapper = styled.div<{ chainId: SupportedL2ChainId; darkMode: boolean; logoUrl: string }>`
${({ chainId, darkMode }) =>
[SupportedChainId.OPTIMISM, SupportedChainId.OPTIMISTIC_KOVAN].includes(chainId)
? darkMode
? OptimismWrapperBackgroundDarkMode
: OptimismWrapperBackgroundLightMode
: darkMode
? ArbitrumWrapperBackgroundDarkMode
: ArbitrumWrapperBackgroundLightMode};
border-radius: 20px;
display: flex;
flex-direction: column;
overflow: hidden;
padding: 12px;
position: relative;
width: 100%;
:before {
background-image: url(${({ logoUrl }) => logoUrl});
background-repeat: no-repeat;
background-size: 300px;
content: '';
height: 300px;
opacity: 0.1;
position: absolute;
transform: rotate(25deg) translate(-90px, -40px);
width: 300px;
z-index: -1;
}
@media screen and (min-width: ${MEDIA_WIDTHS.upToMedium}px) {
flex-direction: row;
padding: 16px 20px;
}
`
const Body = styled.div`
font-size: 12px;
line-height: 143%;
margin: 12px;
@media screen and (min-width: ${MEDIA_WIDTHS.upToMedium}px) {
flex: 1 1 auto;
margin: auto 0;
}
`
const LinkOutCircle = styled(ArrowDownCircle)`
transform: rotate(230deg);
width: 20px;
height: 20px;
margin-left: 12px;
`
const LinkOutToBridge = styled(ExternalLink)`
align-items: center;
background-color: black;
border-radius: 16px;
color: white;
display: flex;
font-size: 14px;
justify-content: space-between;
margin: 0;
max-height: 47px;
padding: 16px 12px;
text-decoration: none;
width: auto;
:hover,
:focus,
:active {
background-color: black;
}
@media screen and (min-width: ${MEDIA_WIDTHS.upToMedium}px) {
margin: auto 0 auto auto;
padding: 14px 16px;
min-width: 226px;
}
`
export function AddLiquidityNetworkAlert() {
const { chainId } = useActiveWeb3React()
const [darkMode] = useDarkModeManager()
const [arbitrumAlphaAcknowledged] = useArbitrumAlphaAlert()
if (!chainId || !L2_CHAIN_IDS.includes(chainId) || arbitrumAlphaAcknowledged) {
return null
}
const info = CHAIN_INFO[chainId as SupportedL2ChainId]
const depositUrl = [SupportedChainId.OPTIMISM, SupportedChainId.OPTIMISTIC_KOVAN].includes(chainId)
? `${info.bridge}?chainId=1`
: info.bridge
return (
<Wrapper darkMode={darkMode} chainId={chainId} logoUrl={info.logoUrl}>
<L2Icon src={info.logoUrl} />
<Body>
<Trans>This is an alpha release of Uniswap on the {info.label} network.</Trans>
<DesktopTextBreak /> <Trans>You must bridge L1 assets to the network to use them.</Trans>{' '}
<ReadMoreLink href="https://help.uniswap.org/en/articles/5392809-how-to-deposit-tokens-to-optimism">
<Trans>Read more</Trans>
</ReadMoreLink>
</Body>
<LinkOutToBridge href={depositUrl}>
<Trans>Deposit to {info.label}</Trans>
<LinkOutCircle />
</LinkOutToBridge>
</Wrapper>
)
}

View File

@@ -5,12 +5,13 @@ import {
OptimismWrapperBackgroundDarkMode,
OptimismWrapperBackgroundLightMode,
} from 'components/NetworkAlert/NetworkAlert'
import { L2_CHAIN_IDS, L2_INFO, NETWORK_LABELS, SupportedChainId } from 'constants/chains'
import { CHAIN_INFO, L2_CHAIN_IDS, SupportedChainId, SupportedL2ChainId } from 'constants/chains'
import { useActiveWeb3React } from 'hooks/web3'
import { ArrowDownCircle } from 'react-feather'
import { useArbitrumAlphaAlert, useDarkModeManager } from 'state/user/hooks'
import styled from 'styled-components/macro'
import { MEDIA_WIDTHS } from 'theme'
import { ExternalLink, MEDIA_WIDTHS } from 'theme'
import { ReadMoreLink } from './styles'
const L2Icon = styled.img`
display: none;
@@ -27,9 +28,9 @@ const DesktopTextBreak = styled.div`
display: block;
}
`
const Wrapper = styled.div<{ chainId: SupportedChainId; darkMode: boolean; logoUrl: string }>`
const Wrapper = styled.div<{ chainId: SupportedL2ChainId; darkMode: boolean; logoUrl: string }>`
${({ chainId, darkMode }) =>
chainId === SupportedChainId.OPTIMISM
[SupportedChainId.OPTIMISM, SupportedChainId.OPTIMISTIC_KOVAN].includes(chainId)
? darkMode
? OptimismWrapperBackgroundDarkMode
: OptimismWrapperBackgroundLightMode
@@ -62,29 +63,31 @@ const Wrapper = styled.div<{ chainId: SupportedChainId; darkMode: boolean; logoU
}
`
const Body = styled.div`
font-size: 12px;
line-height: 143%;
margin: 12px;
@media screen and (min-width: ${MEDIA_WIDTHS.upToSmall}px) {
margin: 16px 20px 31px;
flex: 1 1 auto;
margin: 0;
margin: auto 0;
}
`
const LinkOutCircle = styled(ArrowDownCircle)`
transform: rotate(230deg);
width: 20px;
height: 20px;
margin-left: 12px;
`
const LinkOutToBridge = styled.a`
const LinkOutToBridge = styled(ExternalLink)`
align-items: center;
background-color: black;
border-radius: 16px;
color: white;
display: flex;
font-size: 14px;
justify-content: space-between;
margin: 0;
max-height: 47px;
padding: 14px;
padding: 16px 8px;
text-decoration: none;
width: auto;
:hover,
@@ -106,16 +109,22 @@ export function MinimalNetworkAlert() {
if (!chainId || !L2_CHAIN_IDS.includes(chainId) || arbitrumAlphaAcknowledged) {
return null
}
const info = L2_INFO[chainId]
const info = CHAIN_INFO[chainId as SupportedL2ChainId]
const depositUrl = [SupportedChainId.OPTIMISM, SupportedChainId.OPTIMISTIC_KOVAN].includes(chainId)
? `${info.bridge}?chainId=1`
: info.bridge
return (
<Wrapper darkMode={darkMode} chainId={chainId} logoUrl={info.logoUrl}>
<L2Icon src={info.logoUrl} />
<Body>
<Trans>This is an alpha release of Uniswap on the {NETWORK_LABELS[chainId]} network.</Trans>
<DesktopTextBreak /> <Trans>You must bridge L1 assets to the network to swap them.</Trans>
<Trans>This is an alpha release of Uniswap on the {info.label} network.</Trans>
<DesktopTextBreak /> <Trans>You must bridge L1 assets to the network to use them.</Trans>{' '}
<ReadMoreLink href="https://help.uniswap.org/en/articles/5392809-how-to-deposit-tokens-to-optimism">
<Trans>Read more</Trans>
</ReadMoreLink>
</Body>
<LinkOutToBridge href={info.bridge} target="_blank" rel="noopener noreferrer">
<Trans>Deposit to {NETWORK_LABELS[chainId]}</Trans>
<LinkOutToBridge href={depositUrl}>
<Trans>Deposit to {info.label}</Trans>
<LinkOutCircle />
</LinkOutToBridge>
</Wrapper>

View File

@@ -1,13 +1,14 @@
import { Trans } from '@lingui/macro'
import { L2_CHAIN_IDS, NETWORK_LABELS, SupportedChainId } from 'constants/chains'
import { L2_CHAIN_IDS, SupportedChainId, SupportedL2ChainId } from 'constants/chains'
import { useActiveWeb3React } from 'hooks/web3'
import { useCallback, useState } from 'react'
import { ArrowDownCircle, X } from 'react-feather'
import { useArbitrumAlphaAlert, useDarkModeManager } from 'state/user/hooks'
import { useETHBalances } from 'state/wallet/hooks'
import styled, { css } from 'styled-components/macro'
import { MEDIA_WIDTHS, TYPE } from 'theme'
import { L2_INFO } from '../../constants/chains'
import { ExternalLink, MEDIA_WIDTHS } from 'theme'
import { CHAIN_INFO } from '../../constants/chains'
import { ReadMoreLink } from './styles'
const L2Icon = styled.img`
width: 40px;
@@ -50,7 +51,7 @@ export const OptimismWrapperBackgroundLightMode = css`
`
const RootWrapper = styled.div<{ chainId: SupportedChainId; darkMode: boolean; logoUrl: string }>`
${({ chainId, darkMode }) =>
chainId === SupportedChainId.OPTIMISM
[SupportedChainId.OPTIMISM, SupportedChainId.OPTIMISTIC_KOVAN].includes(chainId)
? darkMode
? OptimismWrapperBackgroundDarkMode
: OptimismWrapperBackgroundLightMode
@@ -61,7 +62,7 @@ const RootWrapper = styled.div<{ chainId: SupportedChainId; darkMode: boolean; l
display: flex;
flex-direction: column;
max-width: 480px;
min-height: 218px;
min-height: 174px;
overflow: hidden;
position: relative;
width: 100%;
@@ -79,11 +80,14 @@ const RootWrapper = styled.div<{ chainId: SupportedChainId; darkMode: boolean; l
z-index: -1;
}
`
const Header = styled(TYPE.largeHeader)`
const Header = styled.h2`
font-weight: 600;
font-size: 20px;
margin: 0;
padding-right: 30px;
`
const Body = styled.p`
font-size: 12px;
grid-column: 1 / 3;
line-height: 143%;
margin: 0;
@@ -96,15 +100,17 @@ const LinkOutCircle = styled(ArrowDownCircle)`
width: 20px;
height: 20px;
`
const LinkOutToBridge = styled.a`
const LinkOutToBridge = styled(ExternalLink)`
align-items: center;
background-color: black;
border-radius: 16px;
color: white;
display: flex;
font-size: 16px;
height: 44px;
justify-content: space-between;
margin: 0 18px 18px 18px;
padding: 14px 24px;
margin: 0 20px 20px 20px;
padding: 12px 16px;
text-decoration: none;
width: auto;
:hover,
@@ -130,24 +136,31 @@ export function NetworkAlert() {
if (!chainId || !L2_CHAIN_IDS.includes(chainId) || arbitrumAlphaAcknowledged || locallyDismissed) {
return null
}
const info = L2_INFO[chainId]
const info = CHAIN_INFO[chainId as SupportedL2ChainId]
const depositUrl = [SupportedChainId.OPTIMISM, SupportedChainId.OPTIMISTIC_KOVAN].includes(chainId)
? `${info.bridge}?chainId=1`
: info.bridge
return (
<RootWrapper chainId={chainId} darkMode={darkMode} logoUrl={info.logoUrl}>
<CloseIcon onClick={dismiss} />
<ContentWrapper>
<L2Icon src={info.logoUrl} />
<Header>
<Trans>Uniswap on {NETWORK_LABELS[chainId]}</Trans>
<Trans>Uniswap on {info.label}</Trans>
</Header>
<Body>
<Trans>
This is an alpha release of Uniswap on the {NETWORK_LABELS[chainId]} network. You must bridge L1 assets to
the network to swap them.
</Trans>
This is an alpha release of Uniswap on the {info.label} network. You must bridge L1 assets to the network to
swap them.
</Trans>{' '}
<ReadMoreLink href="https://help.uniswap.org/en/articles/5392809-how-to-deposit-tokens-to-optimism">
<Trans>Read more</Trans>
</ReadMoreLink>
</Body>
</ContentWrapper>
<LinkOutToBridge href={info.bridge} target="_blank" rel="noopener noreferrer">
<Trans>Deposit to {NETWORK_LABELS[chainId]}</Trans>
<LinkOutToBridge href={depositUrl}>
<Trans>Deposit to {info.label}</Trans>
<LinkOutCircle />
</LinkOutToBridge>
</RootWrapper>

View File

@@ -0,0 +1,7 @@
import styled from 'styled-components/macro'
import { ExternalLink } from 'theme'
export const ReadMoreLink = styled(ExternalLink)`
color: ${({ theme }) => theme.text1};
text-decoration: underline;
`

View File

@@ -0,0 +1,63 @@
import { Trans } from '@lingui/macro'
import { SupportedChainId } from 'constants/chains'
import { useActiveWeb3React } from 'hooks/web3'
import { AlertOctagon } from 'react-feather'
import styled from 'styled-components/macro'
import { ExternalLink } from 'theme'
const Root = styled.div`
background-color: ${({ theme }) => theme.yellow3};
border-radius: 18px;
color: black;
margin-top: 16px;
padding: 16px;
width: 100%;
max-width: 880px;
`
const WarningIcon = styled(AlertOctagon)`
margin: 0 8px 0 0;
`
const TitleRow = styled.div`
align-items: center;
display: flex;
flex-direction: row;
justify-content: flex-start;
margin: 0;
font-size: 20px;
font-weight: 600;
line-height: 25px;
`
const Body = styled.div`
font-size: 12px;
line-height: 15px;
margin: 8px 0 0 0;
`
const ReadMoreLink = styled(ExternalLink)`
color: black;
text-decoration: underline;
`
export default function OptimismDowntimeWarning() {
const { chainId } = useActiveWeb3React()
if (!chainId || ![SupportedChainId.OPTIMISM, SupportedChainId.OPTIMISTIC_KOVAN].includes(chainId)) {
return null
}
return (
<Root>
<TitleRow>
<WarningIcon />
<Trans>Optimism Planned Downtime</Trans>
</TitleRow>
<Body>
<Trans>
Optimism expects planned downtime in the near future. Unplanned downtime may also occur. While the network is
down, fees will not be generated and you will be unable to remove liquidity.{' '}
<ReadMoreLink href="https://help.uniswap.org/en/articles/5406082-what-happens-if-the-optimistic-ethereum-network-experiences-downtime">
Read more.
</ReadMoreLink>
</Trans>
</Body>
</Root>
)
}

View File

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

View File

@@ -1,7 +1,7 @@
import { useCallback, useContext, useEffect } from 'react'
import { X } from 'react-feather'
import { useSpring } from 'react-spring/web'
import styled, { ThemeContext } from 'styled-components'
import styled, { ThemeContext } from 'styled-components/macro'
import { animated } from 'react-spring'
import { PopupContent } from '../../state/application/actions'
import { useRemovePopup } from '../../state/application/hooks'

View File

@@ -1,6 +1,6 @@
import { useContext } from 'react'
import { AlertCircle, CheckCircle } from 'react-feather'
import styled, { ThemeContext } from 'styled-components'
import styled, { ThemeContext } from 'styled-components/macro'
import { useActiveWeb3React } from '../../hooks/web3'
import { TYPE } from '../../theme'
import { ExternalLink } from '../../theme/components'

View File

@@ -4,6 +4,9 @@ import { AutoColumn } from '../Column'
import PopupItem from './PopupItem'
import ClaimPopup from './ClaimPopup'
import { useURLWarningVisible } from '../../state/user/hooks'
import { useActiveWeb3React } from 'hooks/web3'
import { SupportedChainId } from 'constants/chains'
import { MEDIA_WIDTHS } from 'theme'
const MobilePopupWrapper = styled.div<{ height: string | number }>`
position: relative;
@@ -30,9 +33,13 @@ const MobilePopupInner = styled.div`
}
`
const FixedPopupColumn = styled(AutoColumn)<{ extraPadding: boolean }>`
const StopOverflowQuery = `@media screen and (min-width: ${MEDIA_WIDTHS.upToMedium + 1}px) and (max-width: ${
MEDIA_WIDTHS.upToMedium + 500
}px)`
const FixedPopupColumn = styled(AutoColumn)<{ extraPadding: boolean; xlPadding: boolean }>`
position: fixed;
top: ${({ extraPadding }) => (extraPadding ? '80px' : '88px')};
top: ${({ extraPadding }) => (extraPadding ? '64px' : '56px')};
right: 1rem;
max-width: 355px !important;
width: 100%;
@@ -41,6 +48,10 @@ const FixedPopupColumn = styled(AutoColumn)<{ extraPadding: boolean }>`
${({ theme }) => theme.mediaWidth.upToSmall`
display: none;
`};
${StopOverflowQuery} {
top: ${({ extraPadding, xlPadding }) => (xlPadding ? '64px' : extraPadding ? '64px' : '56px')};
}
`
export default function Popups() {
@@ -49,9 +60,13 @@ export default function Popups() {
const urlWarningActive = useURLWarningVisible()
// need extra padding if network is not L1 Ethereum
const { chainId } = useActiveWeb3React()
const isNotOnMainnet = Boolean(chainId && chainId !== SupportedChainId.MAINNET)
return (
<>
<FixedPopupColumn gap="20px" extraPadding={urlWarningActive}>
<FixedPopupColumn gap="20px" extraPadding={urlWarningActive} xlPadding={isNotOnMainnet}>
<ClaimPopup />
{activePopups.map((item) => (
<PopupItem key={item.key} content={item.content} popKey={item.key} removeAfterMs={item.removeAfterMs} />

View File

@@ -47,7 +47,7 @@ export default function PositionList({ positions }: PositionListProps) {
{positions && ' (' + positions.length + ')'}
</div>
<div>
<Trans>Price range</Trans>
<Trans>Status</Trans>
</div>
</DesktopHeader>
<MobileHeader>

View File

@@ -9,14 +9,16 @@ import styled from 'styled-components/macro'
import { HideSmall, MEDIA_WIDTHS, SmallOnly } from 'theme'
import { PositionDetails } from 'types/position'
import { Price, Token, Percent } from '@uniswap/sdk-core'
import { formatPrice } from 'utils/formatCurrencyAmount'
import { formatTickPrice } from 'utils/formatTickPrice'
import Loader from 'components/Loader'
import { unwrappedToken } from 'utils/unwrappedToken'
import RangeBadge from 'components/Badge/RangeBadge'
import { RowFixed } from 'components/Row'
import { RowBetween } from 'components/Row'
import HoverInlineText from 'components/HoverInlineText'
import { DAI, USDC, USDT, WBTC, WETH9_EXTENDED } from '../../constants/tokens'
import { Trans } from '@lingui/macro'
import useIsTickAtLimit from 'hooks/useIsTickAtLimit'
import { Bound } from 'state/mint/v3/actions'
const LinkRow = styled(Link)`
align-items: center;
@@ -24,6 +26,9 @@ const LinkRow = styled(Link)`
display: flex;
cursor: pointer;
user-select: none;
display: flex;
flex-direction: column;
justify-content: space-between;
color: ${({ theme }) => theme.text1};
margin: 8px 0;
@@ -32,25 +37,23 @@ const LinkRow = styled(Link)`
font-weight: 500;
background-color: ${({ theme }) => theme.bg1};
&:first-of-type {
margin: 0 0 8px 0;
}
&:last-of-type {
margin: 8px 0 0 0;
}
& > div:not(:first-child) {
text-align: right;
text-align: center;
}
:hover {
background-color: ${({ theme }) => theme.bg2};
}
@media screen and (min-width: ${MEDIA_WIDTHS.upToSmall}px) {
flex-direction: row;
/* flex-direction: row; */
}
${({ theme }) => theme.mediaWidth.upToSmall`
flex-direction: column;
row-gap: 24px;
row-gap: 12px;
`};
`
@@ -70,11 +73,14 @@ const RangeLineItem = styled(DataLineItem)`
display: flex;
flex-direction: row;
align-items: center;
justify-self: flex-end;
margin-top: 4px;
width: 100%;
${({ theme }) => theme.mediaWidth.upToSmall`
flex-direction: column;
row-gap: 4px;
background-color: ${({ theme }) => theme.bg2};
border-radius: 12px;
padding: 8px 0;
`};
`
@@ -97,6 +103,9 @@ const ExtentsText = styled.span`
color: ${({ theme }) => theme.text3};
font-size: 14px;
margin-right: 4px;
${({ theme }) => theme.mediaWidth.upToSmall`
display: none;
`};
`
const PrimaryPositionIdData = styled.div`
@@ -201,6 +210,8 @@ export default function PositionListItem({ positionDetails }: PositionListItemPr
return undefined
}, [liquidity, pool, tickLower, tickUpper])
const tickAtLimit = useIsTickAtLimit(feeAmount, tickLower, tickUpper)
// prices
const { priceLower, priceUpper, quote, base } = getPriceOrderingFromPositionForUI(position)
@@ -216,7 +227,7 @@ export default function PositionListItem({ positionDetails }: PositionListItemPr
return (
<LinkRow to={positionSummaryLink}>
<RowFixed>
<RowBetween>
<PrimaryPositionIdData>
<DoubleCurrencyLogo currency0={currencyBase} currency1={currencyQuote} size={18} margin />
<DataText>
@@ -230,7 +241,7 @@ export default function PositionListItem({ positionDetails }: PositionListItemPr
</Badge>
</PrimaryPositionIdData>
<RangeBadge removed={removed} inRange={!outOfRange} />
</RowFixed>
</RowBetween>
{priceLower && priceUpper ? (
<RangeLineItem>
@@ -239,23 +250,23 @@ export default function PositionListItem({ positionDetails }: PositionListItemPr
<Trans>Min: </Trans>
</ExtentsText>
<Trans>
{formatPrice(priceLower, 5)} <HoverInlineText text={currencyQuote?.symbol} /> per{' '}
<HoverInlineText text={currencyBase?.symbol ?? ''} />
{formatTickPrice(priceLower, tickAtLimit, Bound.LOWER)} <HoverInlineText text={currencyQuote?.symbol} />{' '}
per <HoverInlineText text={currencyBase?.symbol ?? ''} />
</Trans>
</RangeText>{' '}
<HideSmall>
<DoubleArrow></DoubleArrow>{' '}
</HideSmall>
<SmallOnly>
<DoubleArrow></DoubleArrow>{' '}
<DoubleArrow></DoubleArrow>{' '}
</SmallOnly>
<RangeText>
<ExtentsText>
<Trans>Max:</Trans>
</ExtentsText>
<Trans>
{formatPrice(priceUpper, 5)} <HoverInlineText text={currencyQuote?.symbol} /> per{' '}
<HoverInlineText maxCharacters={10} text={currencyBase?.symbol} />
{formatTickPrice(priceUpper, tickAtLimit, Bound.UPPER)} <HoverInlineText text={currencyQuote?.symbol} />{' '}
per <HoverInlineText maxCharacters={10} text={currencyBase?.symbol} />
</Trans>
</RangeText>
</RangeLineItem>

View File

@@ -12,19 +12,23 @@ import { Currency } from '@uniswap/sdk-core'
import RateToggle from 'components/RateToggle'
import DoubleCurrencyLogo from 'components/DoubleLogo'
import RangeBadge from 'components/Badge/RangeBadge'
import { ThemeContext } from 'styled-components'
import { ThemeContext } from 'styled-components/macro'
import JSBI from 'jsbi'
import { Bound } from 'state/mint/v3/actions'
import { formatTickPrice } from 'utils/formatTickPrice'
export const PositionPreview = ({
position,
title,
inRange,
baseCurrencyDefault,
ticksAtLimit,
}: {
position: Position
title?: ReactNode
inRange: boolean
baseCurrencyDefault?: Currency | undefined
ticksAtLimit: { [bound: string]: boolean | undefined }
}) => {
const theme = useContext(ThemeContext)
@@ -121,7 +125,11 @@ export const PositionPreview = ({
<TYPE.main fontSize="12px">
<Trans>Min Price</Trans>
</TYPE.main>
<TYPE.mediumHeader textAlign="center">{`${priceLower.toSignificant(5)}`}</TYPE.mediumHeader>
<TYPE.mediumHeader textAlign="center">{`${formatTickPrice(
priceLower,
ticksAtLimit,
Bound.LOWER
)}`}</TYPE.mediumHeader>
<TYPE.main textAlign="center" fontSize="12px">
<Trans>
{quoteCurrency.symbol} per {baseCurrency.symbol}
@@ -138,7 +146,11 @@ export const PositionPreview = ({
<TYPE.main fontSize="12px">
<Trans>Max Price</Trans>
</TYPE.main>
<TYPE.mediumHeader textAlign="center">{`${priceUpper.toSignificant(5)}`}</TYPE.mediumHeader>
<TYPE.mediumHeader textAlign="center">{`${formatTickPrice(
priceUpper,
ticksAtLimit,
Bound.UPPER
)}`}</TYPE.mediumHeader>
<TYPE.main textAlign="center" fontSize="12px">
<Trans>
{quoteCurrency.symbol} per {baseCurrency.symbol}

View File

@@ -1,7 +1,7 @@
import { useContext } from 'react'
import styled from 'styled-components/macro'
import { AutoColumn } from '../Column'
import { ThemeContext } from 'styled-components'
import { ThemeContext } from 'styled-components/macro'
import { TYPE } from '../../theme'
const Wrapper = styled(AutoColumn)`

View File

@@ -0,0 +1,35 @@
import React from 'react'
import { ButtonOutlined } from 'components/Button'
import { AutoRow } from 'components/Row'
import { TYPE } from 'theme'
import styled from 'styled-components/macro'
import { Trans } from '@lingui/macro'
import ReactGA from 'react-ga'
const Button = styled(ButtonOutlined).attrs(() => ({
padding: '8px',
$borderRadius: '8px',
}))`
color: ${({ theme }) => theme.text1};
flex: 1;
`
export default function PresetsButtons({ setFullRange }: { setFullRange: () => void }) {
return (
<AutoRow gap="4px" width="auto">
<Button
onClick={() => {
setFullRange()
ReactGA.event({
category: 'Liquidity',
action: 'Full Range Clicked',
})
}}
>
<TYPE.body fontSize={12}>
<Trans>Full Range</Trans>
</TYPE.body>
</Button>
</AutoRow>
)
}

View File

@@ -2,6 +2,8 @@ import { Trans } from '@lingui/macro'
import { Currency, Price, Token } from '@uniswap/sdk-core'
import StepCounter from 'components/InputStepCounter/InputStepCounter'
import { RowBetween } from 'components/Row'
import { AutoColumn } from 'components/Column'
import { Bound } from 'state/mint/v3/actions'
// currencyA is the base token
export default function RangeSelector({
@@ -16,6 +18,7 @@ export default function RangeSelector({
currencyA,
currencyB,
feeAmount,
ticksAtLimit,
}: {
priceLower?: Price<Token, Token>
priceUpper?: Price<Token, Token>
@@ -28,6 +31,7 @@ export default function RangeSelector({
currencyA?: Currency | null
currencyB?: Currency | null
feeAmount?: number
ticksAtLimit: { [bound in Bound]?: boolean | undefined }
}) {
const tokenA = (currencyA ?? undefined)?.wrapped
const tokenB = (currencyB ?? undefined)?.wrapped
@@ -37,31 +41,37 @@ export default function RangeSelector({
const rightPrice = isSorted ? priceUpper : priceLower?.invert()
return (
<RowBetween>
<StepCounter
value={leftPrice?.toSignificant(5) ?? ''}
onUserInput={onLeftRangeInput}
width="48%"
decrement={isSorted ? getDecrementLower : getIncrementUpper}
increment={isSorted ? getIncrementLower : getDecrementUpper}
feeAmount={feeAmount}
label={leftPrice ? `${currencyB?.symbol}` : '-'}
title={<Trans>Min Price</Trans>}
tokenA={currencyA?.symbol}
tokenB={currencyB?.symbol}
/>
<StepCounter
value={rightPrice?.toSignificant(5) ?? ''}
onUserInput={onRightRangeInput}
width="48%"
decrement={isSorted ? getDecrementUpper : getIncrementLower}
increment={isSorted ? getIncrementUpper : getDecrementLower}
feeAmount={feeAmount}
label={rightPrice ? `${currencyB?.symbol}` : '-'}
tokenA={currencyA?.symbol}
tokenB={currencyB?.symbol}
title={<Trans>Max Price</Trans>}
/>
</RowBetween>
<AutoColumn gap="md">
<RowBetween>
<StepCounter
value={ticksAtLimit[isSorted ? Bound.LOWER : Bound.UPPER] ? '0' : leftPrice?.toSignificant(5) ?? ''}
onUserInput={onLeftRangeInput}
width="48%"
decrement={isSorted ? getDecrementLower : getIncrementUpper}
increment={isSorted ? getIncrementLower : getDecrementUpper}
decrementDisabled={ticksAtLimit[isSorted ? Bound.LOWER : Bound.UPPER]}
incrementDisabled={ticksAtLimit[isSorted ? Bound.LOWER : Bound.UPPER]}
feeAmount={feeAmount}
label={leftPrice ? `${currencyB?.symbol}` : '-'}
title={<Trans>Min Price</Trans>}
tokenA={currencyA?.symbol}
tokenB={currencyB?.symbol}
/>
<StepCounter
value={ticksAtLimit[isSorted ? Bound.UPPER : Bound.LOWER] ? '∞' : rightPrice?.toSignificant(5) ?? ''}
onUserInput={onRightRangeInput}
width="48%"
decrement={isSorted ? getDecrementUpper : getIncrementLower}
increment={isSorted ? getIncrementUpper : getDecrementLower}
incrementDisabled={ticksAtLimit[isSorted ? Bound.UPPER : Bound.LOWER]}
decrementDisabled={ticksAtLimit[isSorted ? Bound.UPPER : Bound.LOWER]}
feeAmount={feeAmount}
label={rightPrice ? `${currencyB?.symbol}` : '-'}
tokenA={currencyA?.symbol}
tokenB={currencyB?.symbol}
title={<Trans>Max Price</Trans>}
/>
</RowBetween>
</AutoColumn>
)
}

View File

@@ -22,10 +22,10 @@ export default function RateToggle({
<div style={{ width: 'fit-content', display: 'flex', alignItems: 'center' }} onClick={handleRateToggle}>
<ToggleWrapper width="fit-content">
<ToggleElement isActive={isSorted} fontSize="12px">
<Trans>{isSorted ? currencyA.symbol : currencyB.symbol} price</Trans>
<Trans>{isSorted ? currencyA.symbol : currencyB.symbol}</Trans>
</ToggleElement>
<ToggleElement isActive={!isSorted} fontSize="12px">
<Trans>{isSorted ? currencyB.symbol : currencyA.symbol} price</Trans>
<Trans>{isSorted ? currencyB.symbol : currencyA.symbol}</Trans>
</ToggleElement>
</ToggleWrapper>
</div>

View File

@@ -10,6 +10,12 @@ import QuestionHelper from '../QuestionHelper'
import { AutoRow } from '../Row'
import CurrencyLogo from '../CurrencyLogo'
const MobileWrapper = styled(AutoColumn)`
${({ theme }) => theme.mediaWidth.upToSmall`
display: none;
`};
`
const BaseWrapper = styled.div<{ disable?: boolean }>`
border: 1px solid ${({ theme, disable }) => (disable ? 'transparent' : theme.bg3)};
border-radius: 10px;
@@ -39,7 +45,7 @@ export default function CommonBases({
const bases = typeof chainId !== 'undefined' ? COMMON_BASES[chainId] ?? [] : []
return bases.length > 0 ? (
<AutoColumn gap="md">
<MobileWrapper gap="md">
<AutoRow>
<Text fontWeight={500} fontSize={14}>
<Trans>Common bases</Trans>
@@ -63,6 +69,6 @@ export default function CommonBases({
)
})}
</AutoRow>
</AutoColumn>
</MobileWrapper>
) : null
}

View File

@@ -33,10 +33,11 @@ const WarningWrapper = styled(Card)<{ highWarning: boolean }>`
const AddressText = styled(TYPE.blue)`
font-size: 12px;
word-break: break-all;
${({ theme }) => theme.mediaWidth.upToSmall`
font-size: 10px;
`}
`}
`
interface ImportProps {

View File

@@ -1,36 +1,34 @@
import { memo, useCallback, useMemo, useRef, useState, useEffect } from 'react'
import { Settings, CheckCircle } from 'react-feather'
import { t, Trans } from '@lingui/macro'
import { TokenList } from '@uniswap/token-lists'
import Card from 'components/Card'
import { UNSUPPORTED_LIST_URLS } from 'constants/lists'
import { useListColor } from 'hooks/useColor'
import { useActiveWeb3React } from 'hooks/web3'
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { CheckCircle, Settings } from 'react-feather'
import ReactGA from 'react-ga'
import { useAppDispatch, useAppSelector } from 'state/hooks'
import { usePopper } from 'react-popper'
import { useAppDispatch, useAppSelector } from 'state/hooks'
import styled from 'styled-components/macro'
import { useFetchListCallback } from '../../hooks/useFetchListCallback'
import { useOnClickOutside } from '../../hooks/useOnClickOutside'
import { TokenList } from '@uniswap/token-lists'
import { t, Trans } from '@lingui/macro'
import useTheme from '../../hooks/useTheme'
import useToggle from '../../hooks/useToggle'
import { acceptListUpdate, removeList, disableList, enableList } from '../../state/lists/actions'
import { useIsListActive, useAllLists, useActiveListUrls } from '../../state/lists/hooks'
import { ExternalLink, LinkStyledButton, TYPE, IconWrapper } from '../../theme'
import { acceptListUpdate, disableList, enableList, removeList } from '../../state/lists/actions'
import { useActiveListUrls, useAllLists, useIsListActive } from '../../state/lists/hooks'
import { ExternalLink, IconWrapper, LinkStyledButton, TYPE } from '../../theme'
import listVersionLabel from '../../utils/listVersionLabel'
import { parseENSAddress } from '../../utils/parseENSAddress'
import uriToHttp from '../../utils/uriToHttp'
import { ButtonEmpty, ButtonPrimary } from '../Button'
import Column, { AutoColumn } from '../Column'
import ListLogo from '../ListLogo'
import Row, { RowFixed, RowBetween } from '../Row'
import { PaddedColumn, SearchInput, Separator, SeparatorDark } from './styleds'
import { useListColor } from 'hooks/useColor'
import useTheme from '../../hooks/useTheme'
import Row, { RowBetween, RowFixed } from '../Row'
import ListToggle from '../Toggle/ListToggle'
import Card from 'components/Card'
import { CurrencyModalView } from './CurrencySearchModal'
import { UNSUPPORTED_LIST_URLS } from 'constants/lists'
import { PaddedColumn, SearchInput, Separator, SeparatorDark } from './styleds'
const Wrapper = styled(Column)`
width: 100%;
height: 100%;
`
@@ -80,8 +78,9 @@ const StyledListUrlText = styled(TYPE.main)<{ active: boolean }>`
color: ${({ theme, active }) => (active ? theme.white : theme.text2)};
`
const RowWrapper = styled(Row)<{ bgColor: string; active: boolean }>`
const RowWrapper = styled(Row)<{ bgColor: string; active: boolean; hasActiveTokens: boolean }>`
background-color: ${({ bgColor, active, theme }) => (active ? bgColor ?? 'transparent' : theme.bg2)};
opacity: ${({ hasActiveTokens }) => (hasActiveTokens ? 1 : 0.4)};
transition: 200ms;
align-items: center;
padding: 1rem;
@@ -93,10 +92,18 @@ function listUrlRowHTMLId(listUrl: string) {
}
const ListRow = memo(function ListRow({ listUrl }: { listUrl: string }) {
const { chainId } = useActiveWeb3React()
const listsByUrl = useAppSelector((state) => state.lists.byUrl)
const dispatch = useAppDispatch()
const { current: list, pendingUpdate: pending } = listsByUrl[listUrl]
const activeTokensOnThisChain = useMemo(() => {
if (!list || !chainId) {
return 0
}
return list.tokens.reduce((acc, cur) => (cur.chainId === chainId ? acc + 1 : acc), 0)
}, [chainId, list])
const theme = useTheme()
const listColor = useListColor(list?.logoURI)
const isActive = useIsListActive(listUrl)
@@ -130,7 +137,7 @@ const ListRow = memo(function ListRow({ listUrl }: { listUrl: string }) {
action: 'Start Remove List',
label: listUrl,
})
if (window.prompt(`Please confirm you would like to remove this list by typing REMOVE`) === `REMOVE`) {
if (window.prompt(t`Please confirm you would like to remove this list by typing REMOVE`) === `REMOVE`) {
ReactGA.event({
category: 'Lists',
action: 'Confirm Remove List',
@@ -161,7 +168,13 @@ const ListRow = memo(function ListRow({ listUrl }: { listUrl: string }) {
if (!list) return null
return (
<RowWrapper active={isActive} bgColor={listColor} key={listUrl} id={listUrlRowHTMLId(listUrl)}>
<RowWrapper
active={isActive}
hasActiveTokens={activeTokensOnThisChain > 0}
bgColor={listColor}
key={listUrl}
id={listUrlRowHTMLId(listUrl)}
>
{list.logoURI ? (
<ListLogo size="40px" style={{ marginRight: '1rem' }} logoURI={list.logoURI} alt={`${list.name} list logo`} />
) : (
@@ -173,7 +186,7 @@ const ListRow = memo(function ListRow({ listUrl }: { listUrl: string }) {
</Row>
<RowFixed mt="4px">
<StyledListUrlText active={isActive} mr="6px">
<Trans>{list.tokens.length} tokens</Trans>
<Trans>{activeTokensOnThisChain} tokens</Trans>
</StyledListUrlText>
<StyledMenu ref={node as any}>
<ButtonEmpty onClick={toggle} ref={setReferenceElement} padding="0">
@@ -226,20 +239,29 @@ export function ManageLists({
setImportList: (list: TokenList) => void
setListUrl: (url: string) => void
}) {
const { chainId } = useActiveWeb3React()
const theme = useTheme()
const [listUrlInput, setListUrlInput] = useState<string>('')
const lists = useAllLists()
const tokenCountByListName = useMemo<Record<string, number>>(
() =>
Object.values(lists).reduce((acc, { current: list }) => {
if (!list) {
return acc
}
return {
...acc,
[list.name]: list.tokens.reduce((count: number, token) => (token.chainId === chainId ? count + 1 : count), 0),
}
}, {}),
[chainId, lists]
)
// sort by active but only if not visible
const activeListUrls = useActiveListUrls()
const [activeCopy, setActiveCopy] = useState<string[] | undefined>()
useEffect(() => {
if (!activeCopy && activeListUrls) {
setActiveCopy(activeListUrls)
}
}, [activeCopy, activeListUrls])
const handleInput = useCallback((e) => {
setListUrlInput(e.target.value)
@@ -258,30 +280,36 @@ export function ManageLists({
// only show loaded lists, hide unsupported lists
return Boolean(lists[listUrl].current) && !Boolean(UNSUPPORTED_LIST_URLS.includes(listUrl))
})
.sort((u1, u2) => {
const { current: l1 } = lists[u1]
const { current: l2 } = lists[u2]
.sort((listUrlA, listUrlB) => {
const { current: listA } = lists[listUrlA]
const { current: listB } = lists[listUrlB]
// first filter on active lists
if (activeCopy?.includes(u1) && !activeCopy?.includes(u2)) {
if (activeListUrls?.includes(listUrlA) && !activeListUrls?.includes(listUrlB)) {
return -1
}
if (!activeCopy?.includes(u1) && activeCopy?.includes(u2)) {
if (!activeListUrls?.includes(listUrlA) && activeListUrls?.includes(listUrlB)) {
return 1
}
if (l1 && l2) {
return l1.name.toLowerCase() < l2.name.toLowerCase()
if (listA && listB) {
if (tokenCountByListName[listA.name] > tokenCountByListName[listB.name]) {
return -1
}
if (tokenCountByListName[listA.name] < tokenCountByListName[listB.name]) {
return 1
}
return listA.name.toLowerCase() < listB.name.toLowerCase()
? -1
: l1.name.toLowerCase() === l2.name.toLowerCase()
: listA.name.toLowerCase() === listB.name.toLowerCase()
? 0
: 1
}
if (l1) return -1
if (l2) return 1
if (listA) return -1
if (listB) return 1
return 0
})
}, [lists, activeCopy])
}, [lists, activeListUrls, tokenCountByListName])
// temporary fetched list for import flow
const [tempList, setTempList] = useState<TokenList>()

View File

@@ -3,7 +3,7 @@ import { useContext, useRef, useState } from 'react'
import { Settings, X } from 'react-feather'
import ReactGA from 'react-ga'
import { Text } from 'rebass'
import styled, { ThemeContext } from 'styled-components'
import styled, { ThemeContext } from 'styled-components/macro'
import { useOnClickOutside } from '../../hooks/useOnClickOutside'
import { ApplicationModal } from '../../state/application/actions'
import { useModalOpen, useToggleSettingsMenu } from '../../state/application/hooks'
@@ -173,7 +173,7 @@ export default function SettingsTab({ placeholderSlippage }: { placeholderSlippa
</AutoColumn>
</ModalContentWrapper>
</Modal>
<StyledMenuButton onClick={toggle} id="open-settings-dialog-button">
<StyledMenuButton onClick={toggle} id="open-settings-dialog-button" aria-label={t`Transaction Settings`}>
<StyledMenuIcon />
{expertMode ? (
<EmojiWrapper>

View File

@@ -1,13 +1,10 @@
import { Trans } from '@lingui/macro'
import { useMemo } from 'react'
import ReactGA from 'react-ga'
import { useLocation } from 'react-router'
import styled from 'styled-components/macro'
import { DEFAULT_LOCALE, LOCALE_LABEL, SupportedLocale } from '../../constants/locales'
import { navigatorLocale, useActiveLocale } from '../../hooks/useActiveLocale'
import useParsedQueryString from '../../hooks/useParsedQueryString'
import { StyledInternalLink, TYPE } from '../../theme'
import { stringify } from 'qs'
import { useLocationLinkProps } from 'hooks/useLocationLinkProps'
const Container = styled(TYPE.small)`
opacity: 0.6;
@@ -17,46 +14,35 @@ const Container = styled(TYPE.small)`
margin-top: 1rem !important;
`
export function SwitchLocaleLink() {
const activeLocale = useActiveLocale()
const useTargetLocale = (activeLocale: SupportedLocale) => {
const browserLocale = useMemo(() => navigatorLocale(), [])
const location = useLocation()
const qs = useParsedQueryString()
if (browserLocale && (browserLocale !== DEFAULT_LOCALE || activeLocale !== DEFAULT_LOCALE)) {
let targetLocale: SupportedLocale
if (activeLocale === browserLocale) {
targetLocale = DEFAULT_LOCALE
return DEFAULT_LOCALE
} else {
targetLocale = browserLocale
return browserLocale
}
const target = {
...location,
search: stringify({ ...qs, lng: targetLocale }),
}
return (
<Container>
<Trans>
Uniswap available in:{' '}
{
<StyledInternalLink
onClick={() => {
ReactGA.event({
category: 'Localization',
action: 'Switch Locale',
label: `${activeLocale} -> ${targetLocale}`,
})
}}
to={target}
>
{LOCALE_LABEL[targetLocale]}
</StyledInternalLink>
}
</Trans>
</Container>
)
}
return null
}
export function SwitchLocaleLink() {
const activeLocale = useActiveLocale()
const targetLocale = useTargetLocale(activeLocale)
const { to, onClick } = useLocationLinkProps(targetLocale)
if (!targetLocale || !to) return null
return (
<Container>
<Trans>
Uniswap available in:{' '}
<StyledInternalLink onClick={onClick} to={to}>
{LOCALE_LABEL[targetLocale]}
</StyledInternalLink>
</Trans>
</Container>
)
}

View File

@@ -0,0 +1,130 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ResizableTextArea renders correctly 1`] = `
<DocumentFragment>
.c0 {
font-size: 12;
outline: none;
border: none;
-webkit-flex: 1 1 auto;
-ms-flex: 1 1 auto;
flex: 1 1 auto;
width: 0;
resize: none;
background-color: #F7F8FA;
-webkit-transition: color 300ms step-start;
transition: color 300ms step-start;
color: #000000;
overflow: hidden;
text-overflow: ellipsis;
font-weight: 500;
width: 100%;
line-height: 1.2;
padding: 0px;
-webkit-appearance: textfield;
}
.c0::-webkit-search-decoration {
-webkit-appearance: none;
}
.c0::-webkit-outer-spin-button,
.c0::-webkit-inner-spin-button {
-webkit-appearance: none;
}
.c0::-webkit-input-placeholder {
color: #C3C5CB;
}
.c0::-moz-placeholder {
color: #C3C5CB;
}
.c0:-ms-input-placeholder {
color: #C3C5CB;
}
.c0::placeholder {
color: #C3C5CB;
}
<textarea
autocapitalize="off"
autocomplete="off"
autocorrect="off"
class="c0 testing"
font-size="12"
placeholder="Test Placeholder"
spellcheck="false"
style="height: auto; min-height: 500px;"
>
My test input
</textarea>
</DocumentFragment>
`;
exports[`TextInput renders correctly 1`] = `
<DocumentFragment>
.c0 {
font-size: 12;
outline: none;
border: none;
-webkit-flex: 1 1 auto;
-ms-flex: 1 1 auto;
flex: 1 1 auto;
width: 0;
background-color: #F7F8FA;
-webkit-transition: color 300ms step-start;
transition: color 300ms step-start;
color: #000000;
overflow: hidden;
text-overflow: ellipsis;
font-weight: 500;
width: 100%;
padding: 0px;
-webkit-appearance: textfield;
}
.c0::-webkit-search-decoration {
-webkit-appearance: none;
}
.c0::-webkit-outer-spin-button,
.c0::-webkit-inner-spin-button {
-webkit-appearance: none;
}
.c0::-webkit-input-placeholder {
color: #C3C5CB;
}
.c0::-moz-placeholder {
color: #C3C5CB;
}
.c0:-ms-input-placeholder {
color: #C3C5CB;
}
.c0::placeholder {
color: #C3C5CB;
}
<div
class="testing"
>
<input
autocapitalize="off"
autocomplete="off"
autocorrect="off"
class="c0"
font-size="12"
placeholder="Test Placeholder"
spellcheck="false"
type="text"
value="My test input"
/>
</div>
</DocumentFragment>
`;

View File

@@ -0,0 +1,68 @@
import { TextInput, ResizingTextArea } from './'
import { render, screen, fireEvent } from 'test-utils'
describe('TextInput', () => {
it('renders correctly', () => {
const { asFragment } = render(
<TextInput
className="testing"
value="My test input"
onUserInput={() => null}
placeholder="Test Placeholder"
fontSize="12"
/>
)
expect(asFragment()).toMatchSnapshot()
})
it('calls the handler on user input', () => {
const onUserInputSpy = jest.fn()
render(
<TextInput
className="testing"
value=""
onUserInput={onUserInputSpy}
placeholder="Test Placeholder"
fontSize="12"
/>
)
fireEvent.change(screen.getByPlaceholderText('Test Placeholder'), { target: { value: 'New value' } })
expect(onUserInputSpy).toHaveBeenCalledWith('New value')
expect(onUserInputSpy).toHaveBeenCalledTimes(1)
})
})
describe('ResizableTextArea', () => {
it('renders correctly', () => {
const { asFragment } = render(
<ResizingTextArea
className="testing"
value="My test input"
onUserInput={() => null}
placeholder="Test Placeholder"
fontSize="12"
/>
)
expect(asFragment()).toMatchSnapshot()
})
it('calls the handler on user input', () => {
const onUserInputSpy = jest.fn()
render(
<ResizingTextArea
className="testing"
value=""
onUserInput={onUserInputSpy}
placeholder="Test Placeholder"
fontSize="12"
/>
)
fireEvent.change(screen.getByPlaceholderText('Test Placeholder'), { target: { value: 'New value' } })
expect(onUserInputSpy).toHaveBeenCalledWith('New value')
expect(onUserInputSpy).toHaveBeenCalledTimes(1)
})
})

View File

@@ -1,5 +1,5 @@
import React, { memo, useCallback, useRef } from 'react'
import styled from 'styled-components'
import styled from 'styled-components/macro'
const Input = styled.input<{ error?: boolean; fontSize?: string }>`
font-size: ${({ fontSize }) => fontSize || '1.25rem'};

View File

@@ -8,7 +8,7 @@ const ToggleElement = styled.span<{ isActive?: boolean; isOnSwitch?: boolean }>`
border-radius: 9px;
background: ${({ theme, isActive, isOnSwitch }) => (isActive ? (isOnSwitch ? theme.primary1 : theme.bg4) : 'none')};
color: ${({ theme, isActive }) => (isActive ? theme.white : theme.text2)};
font-size: 1rem;
font-size: 14px;
font-weight: ${({ isOnSwitch }) => (isOnSwitch ? '500' : '400')};
:hover {
user-select: ${({ isOnSwitch }) => (isOnSwitch ? 'none' : 'initial')};
@@ -20,9 +20,8 @@ const ToggleElement = styled.span<{ isActive?: boolean; isOnSwitch?: boolean }>`
const StyledToggle = styled.button<{ isActive?: boolean; activeElement?: boolean }>`
border-radius: 12px;
border: 2px solid;
border-color: ${({ theme, isActive }) => (isActive ? theme.primary1 : theme.bg3)};
background: ${({ theme }) => theme.bg1};
border: none;
background: ${({ theme }) => theme.bg0};
display: flex;
width: fit-content;
cursor: pointer;

View File

@@ -0,0 +1,70 @@
import useTheme from 'hooks/useTheme'
import styled, { keyframes } from 'styled-components/macro'
const Wrapper = styled.div`
height: 90px;
width: 90px;
`
const dash = keyframes`
0% {
stroke-dashoffset: 1000;
}
100% {
stroke-dashoffset: 0;
}
`
const dashCheck = keyframes`
0% {
stroke-dashoffset: -100;
}
100% {
stroke-dashoffset: 900;
}
`
const Circle = styled.circle`
stroke-dasharray: 1000;
stroke-dashoffset: 0;
-webkit-animation: ${dash} 0.9s ease-in-out;
animation: ${dash} 0.9s ease-in-out;
`
const PolyLine = styled.polyline`
stroke-dasharray: 1000;
stroke-dashoffset: 0;
stroke-dashoffset: -100;
-webkit-animation: ${dashCheck} 0.9s 0.35s ease-in-out forwards;
animation: ${dashCheck} 0.9s 0.35s ease-in-out forwards;
`
export default function AnimatedConfirmation() {
const theme = useTheme()
return (
<Wrapper className="w4rAnimated_checkmark">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 130.2 130.2">
<Circle
className="path circle"
fill="none"
stroke={theme.green1}
strokeWidth="6"
strokeMiterlimit="10"
cx="65.1"
cy="65.1"
r="62.1"
/>
<PolyLine
className="path check"
fill="none"
stroke={theme.green1}
strokeWidth="6"
strokeLinecap="round"
strokeMiterlimit="10"
points="100.2,40.2 51.5,88.8 29.8,67.5 "
/>
</svg>
</Wrapper>
)
}

View File

@@ -1,13 +1,13 @@
import { Currency } from '@uniswap/sdk-core'
import { ReactNode, useContext } from 'react'
import styled, { ThemeContext } from 'styled-components'
import styled, { ThemeContext } from 'styled-components/macro'
import { getExplorerLink, ExplorerDataType } from '../../utils/getExplorerLink'
import Modal from '../Modal'
import { ExternalLink } from '../../theme'
import { Text } from 'rebass'
import { CloseIcon, CustomLightSpinner } from '../../theme/components'
import { RowBetween, RowFixed } from '../Row'
import { AlertTriangle, ArrowUpCircle, CheckCircle } from 'react-feather'
import { AlertCircle, AlertTriangle, ArrowUpCircle, CheckCircle } from 'react-feather'
import { ButtonPrimary, ButtonLight } from '../Button'
import { AutoColumn, ColumnCenter } from '../Column'
import Circle from '../../assets/images/blue-loader.svg'
@@ -15,6 +15,10 @@ import MetaMaskLogo from '../../assets/images/metamask.png'
import { useActiveWeb3React } from '../../hooks/web3'
import useAddTokenToMetamask from 'hooks/useAddTokenToMetamask'
import { Trans } from '@lingui/macro'
import { CHAIN_INFO, L2_CHAIN_IDS, SupportedL2ChainId } from 'constants/chains'
import { useIsTransactionConfirmed, useTransaction } from 'state/transactions/hooks'
import Badge from 'components/Badge'
import AnimatedConfirmation from './AnimatedConfirmation'
const Wrapper = styled.div`
width: 100%;
@@ -30,7 +34,7 @@ const BottomSection = styled(Section)`
`
const ConfirmedIcon = styled(ColumnCenter)<{ inline?: boolean }>`
padding: ${({ inline }) => (inline ? '20px 0' : '60px 0;')};
padding: ${({ inline }) => (inline ? '20px 0' : '32px 0;')};
`
const StyledLogo = styled.img`
@@ -64,12 +68,10 @@ function ConfirmationPendingContent({
<Text fontWeight={500} fontSize={20} textAlign="center">
<Trans>Waiting For Confirmation</Trans>
</Text>
<AutoColumn gap="12px" justify={'center'}>
<Text fontWeight={600} fontSize={14} color="" textAlign="center">
{pendingText}
</Text>
</AutoColumn>
<Text fontSize={12} color="#565A69" textAlign="center" marginBottom={12}>
<Text fontWeight={400} fontSize={16} textAlign="center">
{pendingText}
</Text>
<Text fontWeight={500} fontSize={14} color="#565A69" textAlign="center" marginBottom="12px">
<Trans>Confirm this transaction in your wallet</Trans>
</Text>
</AutoColumn>
@@ -77,7 +79,6 @@ function ConfirmationPendingContent({
</Wrapper>
)
}
function TransactionSubmittedContent({
onDismiss,
chainId,
@@ -206,6 +207,106 @@ export function TransactionErrorContent({ message, onDismiss }: { message: React
)
}
function L2Content({
onDismiss,
chainId,
hash,
pendingText,
inline,
}: {
onDismiss: () => void
hash: string | undefined
chainId: number
currencyToAdd?: Currency | undefined
pendingText: ReactNode
inline?: boolean // not in modal
}) {
const theme = useContext(ThemeContext)
const transaction = useTransaction(hash)
const confirmed = useIsTransactionConfirmed(hash)
const transactionSuccess = transaction?.receipt?.status === 1
// convert unix time difference to seconds
const secondsToConfirm = transaction?.confirmedTime
? (transaction.confirmedTime - transaction.addedTime) / 1000
: undefined
const info = CHAIN_INFO[chainId as SupportedL2ChainId]
return (
<Wrapper>
<Section inline={inline}>
{!inline && (
<RowBetween mb="16px">
<Badge>
<RowFixed>
<StyledLogo src={info.logoUrl} style={{ margin: '0 8px 0 0' }} />
{info.label}
</RowFixed>
</Badge>
<CloseIcon onClick={onDismiss} />
</RowBetween>
)}
<ConfirmedIcon inline={inline}>
{confirmed ? (
transactionSuccess ? (
// <CheckCircle strokeWidth={1} size={inline ? '40px' : '90px'} color={theme.green1} />
<AnimatedConfirmation />
) : (
<AlertCircle strokeWidth={1} size={inline ? '40px' : '90px'} color={theme.red1} />
)
) : (
<CustomLightSpinner src={Circle} alt="loader" size={inline ? '40px' : '90px'} />
)}
</ConfirmedIcon>
<AutoColumn gap="12px" justify={'center'}>
<Text fontWeight={500} fontSize={20} textAlign="center">
{!hash ? (
<Trans>Confirm transaction in wallet</Trans>
) : !confirmed ? (
<Trans>Transaction Submitted</Trans>
) : transactionSuccess ? (
<Trans>Success</Trans>
) : (
<Trans>Error</Trans>
)}
</Text>
<Text fontWeight={400} fontSize={16} textAlign="center">
{transaction?.summary ?? pendingText ?? ''}
</Text>
{chainId && hash ? (
<ExternalLink href={getExplorerLink(chainId, hash, ExplorerDataType.TRANSACTION)}>
<Text fontWeight={500} fontSize={14} color={theme.primary1}>
<Trans>View on Explorer</Trans>
</Text>
</ExternalLink>
) : (
<div style={{ height: '17px' }}></div>
)}
<Text color={theme.text3} style={{ margin: '20px 0 0 0' }} fontSize={'14px'}>
{!secondsToConfirm ? (
<div style={{ height: '24px' }}></div>
) : (
<div>
<Trans>Transaction completed in </Trans>
<span style={{ fontWeight: 500, marginLeft: '4px', color: theme.text1 }}>
{secondsToConfirm} seconds 🎉
</span>
</div>
)}
</Text>
<ButtonPrimary onClick={onDismiss} style={{ margin: '4px 0 0 0' }}>
<Text fontWeight={500} fontSize={20}>
{inline ? <Trans>Return</Trans> : <Trans>Close</Trans>}
</Text>
</ButtonPrimary>
</AutoColumn>
</Section>
</Wrapper>
)
}
interface ConfirmationModalProps {
isOpen: boolean
onDismiss: () => void
@@ -227,12 +328,16 @@ export default function TransactionConfirmationModal({
}: ConfirmationModalProps) {
const { chainId } = useActiveWeb3React()
const isL2 = Boolean(chainId && L2_CHAIN_IDS.includes(chainId))
if (!chainId) return null
// confirmation screen
return (
<Modal isOpen={isOpen} onDismiss={onDismiss} maxHeight={90}>
{attemptingTxn ? (
{isL2 && (hash || attemptingTxn) ? (
<L2Content chainId={chainId} hash={hash} onDismiss={onDismiss} pendingText={pendingText} />
) : attemptingTxn ? (
<ConfirmationPendingContent onDismiss={onDismiss} pendingText={pendingText} />
) : hash ? (
<TransactionSubmittedContent

View File

@@ -1,7 +1,7 @@
import { t, Trans } from '@lingui/macro'
import { useState, useContext } from 'react'
import { Percent } from '@uniswap/sdk-core'
import styled, { ThemeContext } from 'styled-components'
import styled, { ThemeContext } from 'styled-components/macro'
import QuestionHelper from '../QuestionHelper'
import { TYPE } from '../../theme'
@@ -10,6 +10,8 @@ import { RowBetween, RowFixed } from '../Row'
import { DEFAULT_DEADLINE_FROM_NOW } from 'constants/misc'
import { darken } from 'polished'
import { useSetUserSlippageTolerance, useUserSlippageTolerance, useUserTransactionTTL } from 'state/user/hooks'
import { L2_CHAIN_IDS } from 'constants/chains'
import { useActiveWeb3React } from 'hooks/web3'
enum SlippageError {
InvalidInput = 'InvalidInput',
@@ -92,6 +94,7 @@ interface TransactionSettingsProps {
}
export default function TransactionSettings({ placeholderSlippage }: TransactionSettingsProps) {
const { chainId } = useActiveWeb3React()
const theme = useContext(ThemeContext)
const userSlippageTolerance = useUserSlippageTolerance()
@@ -151,6 +154,8 @@ export default function TransactionSettings({ placeholderSlippage }: Transaction
}
}
const showCustomDeadlineRow = Boolean(chainId && !L2_CHAIN_IDS.includes(chainId))
return (
<AutoColumn gap="md">
<AutoColumn gap="sm">
@@ -221,37 +226,41 @@ export default function TransactionSettings({ placeholderSlippage }: Transaction
) : null}
</AutoColumn>
<AutoColumn gap="sm">
<RowFixed>
<TYPE.black fontSize={14} fontWeight={400} color={theme.text2}>
<Trans>Transaction deadline</Trans>
</TYPE.black>
<QuestionHelper text={t`Your transaction will revert if it is pending for more than this period of time.`} />
</RowFixed>
<RowFixed>
<OptionCustom style={{ width: '80px' }} warning={!!deadlineError} tabIndex={-1}>
<Input
placeholder={(DEFAULT_DEADLINE_FROM_NOW / 60).toString()}
value={
deadlineInput.length > 0
? deadlineInput
: deadline === DEFAULT_DEADLINE_FROM_NOW
? ''
: (deadline / 60).toString()
}
onChange={(e) => parseCustomDeadline(e.target.value)}
onBlur={() => {
setDeadlineInput('')
setDeadlineError(false)
}}
color={deadlineError ? 'red' : ''}
{showCustomDeadlineRow && (
<AutoColumn gap="sm">
<RowFixed>
<TYPE.black fontSize={14} fontWeight={400} color={theme.text2}>
<Trans>Transaction deadline</Trans>
</TYPE.black>
<QuestionHelper
text={t`Your transaction will revert if it is pending for more than this period of time.`}
/>
</OptionCustom>
<TYPE.body style={{ paddingLeft: '8px' }} fontSize={14}>
<Trans>minutes</Trans>
</TYPE.body>
</RowFixed>
</AutoColumn>
</RowFixed>
<RowFixed>
<OptionCustom style={{ width: '80px' }} warning={!!deadlineError} tabIndex={-1}>
<Input
placeholder={(DEFAULT_DEADLINE_FROM_NOW / 60).toString()}
value={
deadlineInput.length > 0
? deadlineInput
: deadline === DEFAULT_DEADLINE_FROM_NOW
? ''
: (deadline / 60).toString()
}
onChange={(e) => parseCustomDeadline(e.target.value)}
onBlur={() => {
setDeadlineInput('')
setDeadlineError(false)
}}
color={deadlineError ? 'red' : ''}
/>
</OptionCustom>
<TYPE.body style={{ paddingLeft: '8px' }} fontSize={14}>
<Trans>minutes</Trans>
</TYPE.body>
</RowFixed>
</AutoColumn>
)}
</AutoColumn>
)
}

View File

@@ -1,10 +1,10 @@
import { AbstractConnector } from '@web3-react/abstract-connector'
import { UnsupportedChainIdError, useWeb3React } from '@web3-react/core'
import { darken, lighten } from 'polished'
import { darken } from 'polished'
import { useMemo } from 'react'
import { Activity } from 'react-feather'
import { t, Trans } from '@lingui/macro'
import styled, { css } from 'styled-components'
import styled, { css } from 'styled-components/macro'
import CoinbaseWalletIcon from '../../assets/images/coinbaseWalletIcon.svg'
import FortmaticIcon from '../../assets/images/fortmaticIcon.png'
import PortisIcon from '../../assets/images/portisIcon.png'
@@ -61,6 +61,7 @@ const Web3StatusError = styled(Web3StatusGeneric)`
const Web3StatusConnect = styled(Web3StatusGeneric)<{ faded?: boolean }>`
background-color: ${({ theme }) => theme.primary4};
border: none;
color: ${({ theme }) => theme.primaryText1};
font-weight: 500;
@@ -86,13 +87,13 @@ const Web3StatusConnect = styled(Web3StatusGeneric)<{ faded?: boolean }>`
`
const Web3StatusConnected = styled(Web3StatusGeneric)<{ pending?: boolean }>`
background-color: ${({ pending, theme }) => (pending ? theme.primary1 : theme.bg1)};
border: 1px solid ${({ pending, theme }) => (pending ? theme.primary1 : theme.bg2)};
background-color: ${({ pending, theme }) => (pending ? theme.primary1 : theme.bg0)};
border: 1px solid ${({ pending, theme }) => (pending ? theme.primary1 : theme.bg1)};
color: ${({ pending, theme }) => (pending ? theme.white : theme.text1)};
font-weight: 500;
:hover,
:focus {
background-color: ${({ pending, theme }) => (pending ? darken(0.05, theme.primary1) : lighten(0.05, theme.bg1))};
border: 1px solid ${({ theme }) => darken(0.05, theme.bg3)};
:focus {
border: 1px solid ${({ pending, theme }) => (pending ? darken(0.1, theme.primary1) : darken(0.1, theme.bg2))};

View File

@@ -134,7 +134,7 @@ export default function PoolCard({ stakingInfo }: { stakingInfo: StakingInfo })
{valueOfTotalStakedAmountInUSDC ? (
<Trans>${valueOfTotalStakedAmountInUSDC.toFixed(0, { groupSeparator: ',' })}</Trans>
) : (
<Trans>${valueOfTotalStakedAmountInWETH?.toSignificant(4, { groupSeparator: ',' }) ?? '-'} ETH</Trans>
<Trans>{valueOfTotalStakedAmountInWETH?.toSignificant(4, { groupSeparator: ',' }) ?? '-'} ETH</Trans>
)}
</TYPE.white>
</RowBetween>

View File

@@ -3,7 +3,7 @@ import { Percent, Currency, TradeType } from '@uniswap/sdk-core'
import { Trade as V2Trade } from '@uniswap/v2-sdk'
import { Trade as V3Trade } from '@uniswap/v3-sdk'
import { useContext, useMemo } from 'react'
import { ThemeContext } from 'styled-components'
import { ThemeContext } from 'styled-components/macro'
import { TYPE } from '../../theme'
import { computeRealizedLPFeePercent } from '../../utils/prices'
import { AutoColumn } from '../Column'

View File

@@ -4,7 +4,7 @@ import { Trade as V3Trade } from '@uniswap/v3-sdk'
import { useContext, useState } from 'react'
import { ArrowDown, AlertTriangle } from 'react-feather'
import { Text } from 'rebass'
import styled, { ThemeContext } from 'styled-components'
import styled, { ThemeContext } from 'styled-components/macro'
import { useUSDCValue } from '../../hooks/useUSDCPrice'
import { TYPE } from '../../theme'
import { ButtonPrimary } from '../Button'

View File

@@ -4,7 +4,7 @@ import { Trade as V3Trade, FeeAmount } from '@uniswap/v3-sdk'
import { Fragment, memo, useContext } from 'react'
import { ChevronRight } from 'react-feather'
import { Flex } from 'rebass'
import { ThemeContext } from 'styled-components'
import { ThemeContext } from 'styled-components/macro'
import { TYPE } from '../../theme'
import { unwrappedToken } from 'utils/unwrappedToken'

View File

@@ -2,7 +2,7 @@ import { useCallback } from 'react'
import { Price, Currency } from '@uniswap/sdk-core'
import { useContext } from 'react'
import { Text } from 'rebass'
import styled, { ThemeContext } from 'styled-components'
import styled, { ThemeContext } from 'styled-components/macro'
interface TradePriceProps {
price: Price<Currency, Currency>

View File

@@ -2,7 +2,7 @@ import { transparentize } from 'polished'
import { ReactNode } from 'react'
import { AlertTriangle } from 'react-feather'
import styled, { css } from 'styled-components'
import styled, { css } from 'styled-components/macro'
import { Text } from 'rebass'
import { AutoColumn } from '../Column'

View File

@@ -4,7 +4,7 @@ import { getExplorerLink, ExplorerDataType } from '../../utils/getExplorerLink'
import Modal from '../Modal'
import { AutoColumn, ColumnCenter } from '../Column'
import styled, { ThemeContext } from 'styled-components'
import styled, { ThemeContext } from 'styled-components/macro'
import { RowBetween } from '../Row'
import { TYPE, CustomLightSpinner } from '../../theme'
import { X, ArrowUpCircle } from 'react-feather'

View File

@@ -1,50 +1,35 @@
import { Web3Provider } from '@ethersproject/providers'
import { SafeAppConnector } from '@gnosis.pm/safe-apps-web3-react'
import { InjectedConnector } from '@web3-react/injected-connector'
import { PortisConnector } from '@web3-react/portis-connector'
import { WalletConnectConnector } from '@web3-react/walletconnect-connector'
import { WalletLinkConnector } from '@web3-react/walletlink-connector'
import { PortisConnector } from '@web3-react/portis-connector'
import { SupportedChainId } from '../constants/chains'
import UNISWAP_LOGO_URL from '../assets/svg/logo.svg'
import { ALL_SUPPORTED_CHAIN_IDS, SupportedChainId } from '../constants/chains'
import getLibrary from '../utils/getLibrary'
import { FortmaticConnector } from './Fortmatic'
import { NetworkConnector } from './NetworkConnector'
import UNISWAP_LOGO_URL from '../assets/svg/logo.svg'
const INFURA_KEY = process.env.REACT_APP_INFURA_KEY
const FORMATIC_KEY = process.env.REACT_APP_FORTMATIC_KEY
const PORTIS_ID = process.env.REACT_APP_PORTIS_ID
const WALLETCONNECT_BRIDGE_URL = process.env.REACT_APP_WALLETCONNECT_BRIDGE_URL
if (typeof INFURA_KEY === 'undefined') {
throw new Error(`REACT_APP_INFURA_KEY must be a defined environment variable`)
}
const NETWORK_URLS: {
[chainId in SupportedChainId]: string
} = {
const NETWORK_URLS: { [key in SupportedChainId]: string } = {
[SupportedChainId.MAINNET]: `https://mainnet.infura.io/v3/${INFURA_KEY}`,
[SupportedChainId.RINKEBY]: `https://rinkeby.infura.io/v3/${INFURA_KEY}`,
[SupportedChainId.ROPSTEN]: `https://ropsten.infura.io/v3/${INFURA_KEY}`,
[SupportedChainId.GOERLI]: `https://goerli.infura.io/v3/${INFURA_KEY}`,
[SupportedChainId.KOVAN]: `https://kovan.infura.io/v3/${INFURA_KEY}`,
[SupportedChainId.ARBITRUM_ONE]: `https://arb1.arbitrum.io/rpc`,
[SupportedChainId.ARBITRUM_RINKEBY]: `https://rinkeby.arbitrum.io/rpc`,
[SupportedChainId.OPTIMISM]: `https://mainnet.optimism.io`,
[SupportedChainId.OPTIMISTIC_KOVAN]: `https://kovan.optimism.io`,
[SupportedChainId.OPTIMISM]: `https://optimism-mainnet.infura.io/v3/${INFURA_KEY}`,
[SupportedChainId.OPTIMISTIC_KOVAN]: `https://optimism-kovan.infura.io/v3/${INFURA_KEY}`,
[SupportedChainId.ARBITRUM_ONE]: `https://arbitrum-mainnet.infura.io/v3/${INFURA_KEY}`,
[SupportedChainId.ARBITRUM_RINKEBY]: `https://arbitrum-rinkeby.infura.io/v3/${INFURA_KEY}`,
}
const SUPPORTED_CHAIN_IDS: SupportedChainId[] = [
SupportedChainId.MAINNET,
SupportedChainId.KOVAN,
SupportedChainId.GOERLI,
SupportedChainId.RINKEBY,
SupportedChainId.ROPSTEN,
SupportedChainId.ARBITRUM_ONE,
SupportedChainId.ARBITRUM_RINKEBY,
SupportedChainId.OPTIMISM,
SupportedChainId.OPTIMISTIC_KOVAN,
]
export const network = new NetworkConnector({
urls: NETWORK_URLS,
defaultChainId: 1,
@@ -56,15 +41,15 @@ export function getNetworkLibrary(): Web3Provider {
}
export const injected = new InjectedConnector({
supportedChainIds: SUPPORTED_CHAIN_IDS,
supportedChainIds: ALL_SUPPORTED_CHAIN_IDS,
})
export const gnosisSafe = new SafeAppConnector()
export const walletconnect = new WalletConnectConnector({
supportedChainIds: SUPPORTED_CHAIN_IDS,
supportedChainIds: ALL_SUPPORTED_CHAIN_IDS,
rpc: NETWORK_URLS,
bridge: WALLETCONNECT_BRIDGE_URL,
qrcode: true,
pollingInterval: 15000,
})
// mainnet only
@@ -81,7 +66,7 @@ export const portis = new PortisConnector({
// mainnet only
export const walletlink = new WalletLinkConnector({
url: NETWORK_URLS[1],
url: NETWORK_URLS[SupportedChainId.MAINNET],
appName: 'Uniswap',
appLogoUrl: UNISWAP_LOGO_URL,
})

View File

@@ -7,9 +7,10 @@ type AddressMap = { [chainId: number]: string }
export const UNI_ADDRESS: AddressMap = constructSameAddressMap('0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984')
export const MULTICALL_ADDRESS: AddressMap = {
...constructSameAddressMap('0x1F98415757620B543A52E61c46B32eB19261F984'),
[SupportedChainId.ARBITRUM_RINKEBY]: '0xa501c031958F579dB7676fF1CE78AD305794d579',
...constructSameAddressMap('0x1F98415757620B543A52E61c46B32eB19261F984', [SupportedChainId.OPTIMISTIC_KOVAN]),
[SupportedChainId.OPTIMISM]: '0x90f872b3d8f33f305e0250db6A2761B354f7710A',
[SupportedChainId.ARBITRUM_ONE]: '0xadF885960B47eA2CD9B55E6DAc6B42b7Cb2806dB',
[SupportedChainId.ARBITRUM_RINKEBY]: '0xa501c031958F579dB7676fF1CE78AD305794d579',
}
export const V2_FACTORY_ADDRESSES: AddressMap = constructSameAddressMap(V2_FACTORY_ADDRESS)
export const V2_ROUTER_ADDRESS: AddressMap = constructSameAddressMap('0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D')
@@ -36,16 +37,25 @@ export const ARGENT_WALLET_DETECTOR_ADDRESS: AddressMap = {
[SupportedChainId.MAINNET]: '0xeca4B0bDBf7c55E9b7925919d03CbF8Dc82537E8',
}
export const V3_CORE_FACTORY_ADDRESSES: AddressMap = constructSameAddressMap(V3_FACTORY_ADDRESS, [
SupportedChainId.OPTIMISM,
SupportedChainId.OPTIMISTIC_KOVAN,
SupportedChainId.ARBITRUM_ONE,
SupportedChainId.ARBITRUM_RINKEBY,
])
export const QUOTER_ADDRESSES: AddressMap = constructSameAddressMap('0xb27308f9F90D607463bb33eA1BeBb41C27CE5AB6', [
SupportedChainId.OPTIMISM,
SupportedChainId.OPTIMISTIC_KOVAN,
SupportedChainId.ARBITRUM_ONE,
SupportedChainId.ARBITRUM_RINKEBY,
])
export const NONFUNGIBLE_POSITION_MANAGER_ADDRESSES: AddressMap = constructSameAddressMap(
'0xC36442b4a4522E871399CD717aBDD847Ab11FE88',
[SupportedChainId.ARBITRUM_ONE, SupportedChainId.ARBITRUM_RINKEBY]
[
SupportedChainId.OPTIMISM,
SupportedChainId.OPTIMISTIC_KOVAN,
SupportedChainId.ARBITRUM_ONE,
SupportedChainId.ARBITRUM_RINKEBY,
]
)
export const ENS_REGISTRAR_ADDRESSES: AddressMap = {
[SupportedChainId.MAINNET]: '0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e',
@@ -57,6 +67,8 @@ export const SOCKS_CONTROLLER_ADDRESSES: AddressMap = {
[SupportedChainId.MAINNET]: '0x65770b5283117639760beA3F867b69b3697a91dd',
}
export const SWAP_ROUTER_ADDRESSES: AddressMap = constructSameAddressMap('0xE592427A0AEce92De3Edee1F18E0157C05861564', [
SupportedChainId.OPTIMISM,
SupportedChainId.OPTIMISTIC_KOVAN,
SupportedChainId.ARBITRUM_ONE,
SupportedChainId.ARBITRUM_RINKEBY,
])

View File

@@ -0,0 +1,30 @@
import { ALL_SUPPORTED_CHAIN_IDS, SupportedChainId } from './chains'
describe('chains', () => {
describe('ALL_SUPPORTED_CHAIN_IDS', () => {
it('contains all the values in the SupportedChainId enum', () => {
Object.values(SupportedChainId).forEach((chainId) => {
if (typeof chainId === 'number') expect(ALL_SUPPORTED_CHAIN_IDS.includes(chainId as number)).toBeTruthy()
})
})
it('contains no duplicates', () => {
const set = new Set<number>()
ALL_SUPPORTED_CHAIN_IDS.forEach((chainId) => {
expect(set.has(chainId)).toEqual(false)
set.add(chainId)
})
})
it('all values are in the SupportedChainId mapping', () => {
ALL_SUPPORTED_CHAIN_IDS.forEach((chainId) => {
// takes advantage of the reverse mapping
expect(SupportedChainId[chainId]).toBeTruthy()
})
})
it('all values are numeric', () => {
expect(ALL_SUPPORTED_CHAIN_IDS.every((chainId) => typeof chainId === 'number')).toBeTruthy()
})
})
})

View File

@@ -7,42 +7,122 @@ export enum SupportedChainId {
RINKEBY = 4,
GOERLI = 5,
KOVAN = 42,
ARBITRUM_ONE = 42161,
ARBITRUM_RINKEBY = 421611,
OPTIMISM = 10,
OPTIMISTIC_KOVAN = 69,
}
export const L2_CHAIN_IDS = [
export const ALL_SUPPORTED_CHAIN_IDS: SupportedChainId[] = [
SupportedChainId.MAINNET,
SupportedChainId.ROPSTEN,
SupportedChainId.RINKEBY,
SupportedChainId.GOERLI,
SupportedChainId.KOVAN,
SupportedChainId.ARBITRUM_ONE,
SupportedChainId.ARBITRUM_RINKEBY,
SupportedChainId.OPTIMISM,
SupportedChainId.OPTIMISTIC_KOVAN,
]
export const L2_INFO: Record<number, { bridge: string; docs: string; explorer: string; logoUrl: string }> = {
export const L1_CHAIN_IDS = [
SupportedChainId.MAINNET,
SupportedChainId.ROPSTEN,
SupportedChainId.RINKEBY,
SupportedChainId.GOERLI,
SupportedChainId.KOVAN,
] as const
export type SupportedL1ChainId = typeof L1_CHAIN_IDS[number]
export const L2_CHAIN_IDS = [
SupportedChainId.ARBITRUM_ONE,
SupportedChainId.ARBITRUM_RINKEBY,
SupportedChainId.OPTIMISM,
SupportedChainId.OPTIMISTIC_KOVAN,
] as const
export type SupportedL2ChainId = typeof L2_CHAIN_IDS[number]
interface L1ChainInfo {
readonly docs: string
readonly explorer: string
readonly infoLink: string
readonly label: string
}
export interface L2ChainInfo extends L1ChainInfo {
readonly bridge: string
readonly logoUrl: string
}
type ChainInfo = { readonly [chainId: number]: L1ChainInfo | L2ChainInfo } & {
readonly [chainId in SupportedL2ChainId]: L2ChainInfo
} &
{ readonly [chainId in SupportedL1ChainId]: L1ChainInfo }
export const CHAIN_INFO: ChainInfo = {
[SupportedChainId.ARBITRUM_ONE]: {
bridge: 'https://bridge.arbitrum.io/',
docs: 'https://offchainlabs.com/',
explorer: 'https://explorer.arbitrum.io/',
infoLink: 'https://info.uniswap.org/#/arbitrum',
label: 'Arbitrum',
logoUrl: arbitrumLogoUrl,
},
[SupportedChainId.ARBITRUM_RINKEBY]: {
bridge: 'https://bridge.arbitrum.io/',
docs: 'https://offchainlabs.com/',
explorer: 'https://explorer.arbitrum.io/',
infoLink: 'https://info.uniswap.org/#/arbitrum/',
label: 'Arbitrum Rinkeby',
logoUrl: arbitrumLogoUrl,
},
[SupportedChainId.MAINNET]: {
docs: 'https://docs.uniswap.org/',
explorer: 'https://etherscan.io/',
infoLink: 'https://info.uniswap.org/#/',
label: 'Mainnet',
},
[SupportedChainId.RINKEBY]: {
docs: 'https://docs.uniswap.org/',
explorer: 'https://rinkeby.etherscan.io/',
infoLink: 'https://info.uniswap.org/#/',
label: 'Rinkeby',
},
[SupportedChainId.ROPSTEN]: {
docs: 'https://docs.uniswap.org/',
explorer: 'https://ropsten.etherscan.io/',
infoLink: 'https://info.uniswap.org/#/',
label: 'Ropsten',
},
[SupportedChainId.KOVAN]: {
docs: 'https://docs.uniswap.org/',
explorer: 'https://kovan.etherscan.io/',
infoLink: 'https://info.uniswap.org/#/',
label: 'Kovan',
},
[SupportedChainId.GOERLI]: {
docs: 'https://docs.uniswap.org/',
explorer: 'https://goerli.etherscan.io/',
infoLink: 'https://info.uniswap.org/#/',
label: 'Görli',
},
[SupportedChainId.OPTIMISM]: {
bridge: 'https://gateway.optimism.io/',
docs: 'https://optimism.io/',
explorer: 'https://optimistic.etherscan.io/',
infoLink: 'https://info.uniswap.org/#/optimism/',
label: 'Optimism',
logoUrl: optimismLogoUrl,
},
[SupportedChainId.ARBITRUM_ONE]: {
bridge: 'https://bridge.arbitrum.io/',
explorer: 'https://explorer.arbitrum.io/',
docs: 'https://offchainlabs.com/',
logoUrl: arbitrumLogoUrl,
[SupportedChainId.OPTIMISTIC_KOVAN]: {
bridge: 'https://gateway.optimism.io/',
docs: 'https://optimism.io/',
explorer: 'https://optimistic.etherscan.io/',
infoLink: 'https://info.uniswap.org/#/optimism',
label: 'Optimistic Kovan',
logoUrl: optimismLogoUrl,
},
}
export const NETWORK_LABELS: { [chainId in SupportedChainId | number]: string } = {
[SupportedChainId.MAINNET]: 'Mainnet',
[SupportedChainId.RINKEBY]: 'Rinkeby',
[SupportedChainId.ROPSTEN]: 'Ropsten',
[SupportedChainId.GOERLI]: 'Görli',
[SupportedChainId.KOVAN]: 'Kovan',
[SupportedChainId.ARBITRUM_ONE]: 'Arbitrum',
[SupportedChainId.ARBITRUM_RINKEBY]: 'Arbitrum Testnet',
[SupportedChainId.OPTIMISM]: 'Optimism',
[SupportedChainId.OPTIMISTIC_KOVAN]: 'Optimism Testnet',
}

View File

@@ -1,32 +1,33 @@
// used to mark unsupported tokens, these are hosted lists of unsupported tokens
import { IS_ON_APP_URL } from './misc'
const COMPOUND_LIST = 'https://raw.githubusercontent.com/compound-finance/token-list/master/compound.tokenlist.json'
const UMA_LIST = 'https://umaproject.org/uma.tokenlist.json'
const AAVE_LIST = 'tokenlist.aave.eth'
const WRAPPED_LIST = 'wrapped.tokensoft.eth'
const SET_LIST = 'https://raw.githubusercontent.com/SetProtocol/uniswap-tokenlist/main/set.tokenlist.json'
const ROLL_LIST = 'https://app.tryroll.com/tokens.json'
const COINGECKO_LIST = 'https://tokens.coingecko.com/uniswap/all.json'
const BA_LIST = 'https://raw.githubusercontent.com/The-Blockchain-Association/sec-notice-list/master/ba-sec-list.json'
const CMC_ALL_LIST = 'defi.cmc.eth'
const CMC_STABLECOIN = 'stablecoin.cmc.eth'
const KLEROS_LIST = 't2crtokens.eth'
const COINGECKO_LIST = 'https://tokens.coingecko.com/uniswap/all.json'
const COMPOUND_LIST = 'https://raw.githubusercontent.com/compound-finance/token-list/master/compound.tokenlist.json'
const GEMINI_LIST = 'https://www.gemini.com/uniswap/manifest.json'
const BA_LIST = 'https://raw.githubusercontent.com/The-Blockchain-Association/sec-notice-list/master/ba-sec-list.json'
const KLEROS_LIST = 't2crtokens.eth'
export const OPTIMISM_LIST = 'https://static.optimism.io/optimism.tokenlist.json'
const ROLL_LIST = 'https://app.tryroll.com/tokens.json'
const SET_LIST = 'https://raw.githubusercontent.com/SetProtocol/uniswap-tokenlist/main/set.tokenlist.json'
const WRAPPED_LIST = 'wrapped.tokensoft.eth'
export const UNSUPPORTED_LIST_URLS: string[] = [BA_LIST]
// only load blocked list if on app url
export const UNSUPPORTED_LIST_URLS: string[] = IS_ON_APP_URL ? [BA_LIST] : []
// lower index == higher priority for token import
export const DEFAULT_LIST_OF_LISTS: string[] = [
COMPOUND_LIST,
AAVE_LIST,
UMA_LIST,
CMC_ALL_LIST,
CMC_STABLECOIN,
WRAPPED_LIST,
SET_LIST,
ROLL_LIST,
COINGECKO_LIST,
CMC_ALL_LIST,
CMC_STABLECOIN,
KLEROS_LIST,
OPTIMISM_LIST,
GEMINI_LIST,
...UNSUPPORTED_LIST_URLS, // need to load unsupported tokens as well
]

View File

@@ -1,4 +1,6 @@
export const SUPPORTED_LOCALES = [
// order as they appear in the language dropdown
'en-US',
'af-ZA',
'ar-SA',
'ca-ES',
@@ -6,7 +8,6 @@ export const SUPPORTED_LOCALES = [
'da-DK',
'de-DE',
'el-GR',
'en-US',
'es-ES',
'fi-FI',
'fr-FR',
@@ -65,6 +66,6 @@ export const LOCALE_LABEL: { [locale in SupportedLocale]: string } = {
'tr-TR': 'Türkçe',
'uk-UA': 'Український',
'vi-VN': 'Tiếng Việt',
'zh-CN': '中文 ( 中国 )',
'zh-TW': '中文 ( 台灣 )',
'zh-CN': '简体中文',
'zh-TW': '繁体中文',
}

View File

@@ -7,6 +7,7 @@ export const NetworkContextName = 'NETWORK'
// 30 minutes, denominated in seconds
export const DEFAULT_DEADLINE_FROM_NOW = 60 * 30
export const L2_DEADLINE_FROM_NOW = 60 * 5
// used for rewards deadlines
export const BIG_INT_SECONDS_IN_WEEK = JSBI.BigInt(60 * 60 * 24 * 7)
@@ -30,3 +31,5 @@ export const BETTER_TRADE_LESS_HOPS_THRESHOLD = new Percent(JSBI.BigInt(50), BIP
export const ZERO_PERCENT = new Percent('0')
export const ONE_HUNDRED_PERCENT = new Percent('1')
export const IS_ON_APP_URL = window && window.location.hostname === 'app.uniswap.org'

View File

@@ -0,0 +1,27 @@
export const BRAVO_PROPOSAL_DESCRIPTION = `# Upgrade Governance Contract to Compound's Governor Bravo
## Previous Discussion
[Temperature Check](https://gov.uniswap.org/t/temperature-check-upgrade-governance-contract-to-governor-bravo/13610) | [Snapshot](https://snapshot.org/#/uniswap/proposal/QmScNLeajiF2hQh1z9DYqTFKGgrRhHwrHisV4ynmDEQwxH) [Consensus Check](https://gov.uniswap.org/t/consensus-check-upgrade-governance-contract-to-governor-bravo/13707) | [Snapshot](https://snapshot.org/#/uniswap/proposal/QmWbgzHJ8nK2TDaj6LF6BxAMPahy97dMmbbU5kRBw1QkXt)
## TL;DR:
Upgrade Uniswap's current governance contract from Governor Alpha to Governor Bravo to improve governance upgradability and protocol safety. [On-Chain Proposal]()
## Summary and Motivation:
*Co-written with [Getty Hill](https://twitter.com/getty_hill) (@Getty), [Eddy Lee ](https://twitter.com/yesimeddy) (@elee), [Yuan Han Li ](https://twitter.com/yuan_han_li) (@yuan-han-li), [John Wang ](https://twitter.com/j0hnwang) (@johnwang), and [Ali Khambati ](https://twitter.com/alikhambati1) (@alikhambati)* Governor Alpha, the current governance contract used, was a great start, but in light of Compound's and other protocols upgrade to Governor Bravo, Uniswap should migrate given Bravo's additional safety benefits and upgradability.
1. **Native upgradeability:** Under Governor Alpha, changes to governance parameters require deploying a new contract and completely migrating to a new address. This is particularly damaging to governance participation as it introduces downtime and asynchronicity. Many governance tools and custodians use factory contracts which point to a specific contract address, meaning parties must manually upgrade their infrastructure every time governance parameters are changed under Governor Alpha. This includes tools for creating autonomous proposals like [fish.vote ](https://www.fish.vote/); front-ends such as [Tally ](https://www.withtally.com/), [Sybil](https://sybil.org/#/delegates/uniswap), and [Boardroom ](https://app.boardroom.info/) which aggregate and display governance results for various protocols; and professional custodians which are used by large token holders, delegates, and community members. Enabling a static contract address that has proxy upgradability patterns is paramount for maximizing participation, and maintaining an open and secure governance process.
2. **Voting reason string:** Governor Bravo gives voters the opportunity to add free-form comments (text strings) along with their on-chain votes. Not only does this increase the transparency around the rationale people have behind their votes, but it also facilitates more in-depth and nuanced discussion.
3. **New abstain' voting option:** Governor Bravo enables voters to formally abstain rather than forcing them to choose between voting yes/no or not voting at all. This will give voters more flexibility and also increase transparency into delegate behavior.
4. **Proposal numbers won't reset:** Under Governor Alpha, any upgrades to the contract resets the proposal number schema. Notice that [Proposal 0.4'](https://app.uniswap.org/#/vote/0/4) (which resulted in deployment and migration to a new Governor Alpha contract due to modifying the proposal submission threshold parameter) caused the following on-chain proposal from @HarvardLawBFI to be numbered [Proposal 1.1' ](https://app.uniswap.org/#/vote/1/1). Under Governor Bravo, this would not be an issue and proposal numbers would be continuous because the contract would be maintained at a single address.
5. **Proposal Cancellation:** Bravo allows user-directed cancellations enabling erroneous proposals to be canceled if need be (rather than forcing people to vote no/abstain).
6. **Review Period:** Governor Bravo allows governance to include a review/analysis period. Currently, Compound uses a 13140 block (~2-day) review period that allows holders to review the proposal. This means that users will have 2-days to prepare for voting (e.g., remove UNI from Aave), unlike Governor Alpha which instantly snapshots users' voting power. A review period substantially improves the accessibility and safety of the governance process.
## Implementation:
After speaking with the OpenZeppelin team about their reimplementation of Governor Bravo, we think using Compound's Governor Bravo contract instead makes the most sense at this stage. Although Open Zeppelin's Governor contract takes a simpler approach that can include all Bravo functionality, it does not do so by default. The contract uses similar logic, but the code is new. It has gone through an internal audit and review process but still needs to go through an external audit. For the safety and simplicity of Uniswap, we think using Compound's Governor Bravo contract is the best decision for the time being. Compound's Governor contract has already undergone an [audit](https://blog.openzeppelin.com/compound-governor-bravo-audit/) by OpenZeppelin and has been widely used. We can always change/upgrade to Open Zeppelin Governor contract at a future date if their contract introduces new features/functionality the community is interested in. More details on OpenZeppelin's code can be found here: [Github](https://github.com/OpenZeppelin/openzeppelin-contracts/tree/master/contracts/governance) [Docs](https://docs.openzeppelin.com/contracts/4.x/api/governance) [More info](https://openzeppelin.notion.site/Comparing-Compound-Governor-to-OpenZeppelin-Governor-2-10d0fdcf61ba476fae492b295822ee13) As mentioned in the previous Consensus Check, we have deployed the contract on the Ropsten test net for the community to review our code: [Governor Bravo Delegator](https://ropsten.etherscan.io/address/0x15df15caad12adaa03949014ba5cc49a84803d0f#code) [Governor Bravo Delegate](https://ropsten.etherscan.io/address/0xD8bf60dfC5115F6cB99bb50502346E7b863800f1#code) [Github for contracts](https://github.com/gettty/uniswap-gov) *NB: The \`_initiate\` function has been slightly modified to take an initial proposal number as an input rather than pulling it from Governor Alpha. This way Uniswap can resume proper proposal numbering.*
## Resource links:
1. [Governor Bravo Development - Protocol Development - Compound Community Forum ](https://www.comp.xyz/t/governor-bravo-development/942)
2. [Understanding Governor Bravo. A review of key changes versus | by monetsupply | Tally | Jul, 2021 | Medium ](https://medium.com/tally-blog/understanding-governor-bravo-69b06f1875da)
3. [Compound | Proposal Detail #42 ](https://compound.finance/governance/proposals/42)
4. [Compound | Proposal Detail #43](https://compound.finance/governance/proposals/43)
`

View File

@@ -1 +1,2 @@
export const UNISWAP_GRANTS_START_BLOCK = 11473815
export const BRAVO_START_BLOCK = 13059344

View File

@@ -8,17 +8,16 @@ import {
FEI,
FRAX,
FXS,
MIR,
renBTC,
TRIBE,
UMA,
UNI,
USDC,
USDT,
UST,
WBTC,
ETH2X_FLI,
WETH9_EXTENDED,
DAI_OPTIMISM,
USDT_OPTIMISM,
WBTC_OPTIMISM,
} from './tokens'
type ChainTokenList = {
@@ -29,48 +28,19 @@ type ChainCurrencyList = {
readonly [chainId: number]: Currency[]
}
// List of all mirror's assets addresses.
// Last pulled from : https://whitelist.mirror.finance/eth/tokenlists.json
// TODO: Generate this programmatically ?
const mAssetsAdditionalBases: { [tokenAddress: string]: Token[] } = {
[UST.address]: [MIR],
[MIR.address]: [UST],
'0xd36932143F6eBDEDD872D5Fb0651f4B72Fd15a84': [MIR, UST], // mAAPL
'0x59A921Db27Dd6d4d974745B7FfC5c33932653442': [MIR, UST], // mGOOGL
'0x21cA39943E91d704678F5D00b6616650F066fD63': [MIR, UST], // mTSLA
'0xC8d674114bac90148d11D3C1d33C61835a0F9DCD': [MIR, UST], // mNFLX
'0x13B02c8dE71680e71F0820c996E4bE43c2F57d15': [MIR, UST], // mQQQ
'0xEdb0414627E6f1e3F082DE65cD4F9C693D78CCA9': [MIR, UST], // mTWTR
'0x41BbEDd7286dAab5910a1f15d12CBda839852BD7': [MIR, UST], // mMSFT
'0x0cae9e4d663793c2a2A0b211c1Cf4bBca2B9cAa7': [MIR, UST], // mAMZN
'0x56aA298a19C93c6801FDde870fA63EF75Cc0aF72': [MIR, UST], // mBABA
'0x1d350417d9787E000cc1b95d70E9536DcD91F373': [MIR, UST], // mIAU
'0x9d1555d8cB3C846Bb4f7D5B1B1080872c3166676': [MIR, UST], // mSLV
'0x31c63146a635EB7465e5853020b39713AC356991': [MIR, UST], // mUSO
'0xf72FCd9DCF0190923Fadd44811E240Ef4533fc86': [MIR, UST], // mVIXY
}
const WETH_ONLY: ChainTokenList = {
[SupportedChainId.MAINNET]: [WETH9_EXTENDED[SupportedChainId.MAINNET]],
[SupportedChainId.ROPSTEN]: [WETH9_EXTENDED[SupportedChainId.ROPSTEN]],
[SupportedChainId.RINKEBY]: [WETH9_EXTENDED[SupportedChainId.RINKEBY]],
[SupportedChainId.GOERLI]: [WETH9_EXTENDED[SupportedChainId.GOERLI]],
[SupportedChainId.KOVAN]: [WETH9_EXTENDED[SupportedChainId.KOVAN]],
[SupportedChainId.ARBITRUM_ONE]: [WETH9_EXTENDED[SupportedChainId.ARBITRUM_ONE]],
}
const WETH_ONLY: ChainTokenList = Object.fromEntries(
Object.entries(WETH9_EXTENDED).map(([key, value]) => [key, [value]])
)
// used to construct intermediary pairs for trading
export const BASES_TO_CHECK_TRADES_AGAINST: ChainTokenList = {
...WETH_ONLY,
[1]: [...WETH_ONLY[1], DAI, USDC, USDT, WBTC],
[SupportedChainId.MAINNET]: [...WETH_ONLY[SupportedChainId.MAINNET], DAI, USDC, USDT, WBTC],
[SupportedChainId.OPTIMISM]: [...WETH_ONLY[SupportedChainId.OPTIMISM], DAI_OPTIMISM, USDT_OPTIMISM, WBTC_OPTIMISM],
}
export const ADDITIONAL_BASES: { [chainId: number]: { [tokenAddress: string]: Token[] } } = {
[1]: {
...mAssetsAdditionalBases,
[SupportedChainId.MAINNET]: {
'0xF16E4d813f4DcfDe4c5b44f305c908742De84eF0': [ETH2X_FLI],
'0xA948E86885e12Fb09AfEF8C52142EBDbDf73cD18': [UNI[1]],
'0x561a4717537ff4AF5c687328c0f7E90a319705C0': [UNI[1]],
'0xE0360A9e2cdd7d03B9408c7D3001E017BAc2EcD5': [UNI[1]],
'0xa6e3454fec677772dd771788a079355e43910638': [UMA],
'0xB46F57e7Ce3a284d74b70447Ef9352B5E5Df8963': [UMA],
[FEI.address]: [TRIBE],
[TRIBE.address]: [FEI],
[FRAX.address]: [FXS],
@@ -84,8 +54,8 @@ export const ADDITIONAL_BASES: { [chainId: number]: { [tokenAddress: string]: To
* tokens.
*/
export const CUSTOM_BASES: { [chainId: number]: { [tokenAddress: string]: Token[] } } = {
[1]: {
[AMPL.address]: [DAI, WETH9_EXTENDED[1]],
[SupportedChainId.MAINNET]: {
[AMPL.address]: [DAI, WETH9_EXTENDED[SupportedChainId.MAINNET]],
},
}
@@ -93,27 +63,52 @@ export const CUSTOM_BASES: { [chainId: number]: { [tokenAddress: string]: Token[
* Shows up in the currency select for swap and add liquidity
*/
export const COMMON_BASES: ChainCurrencyList = {
[1]: [ExtendedEther.onChain(1), DAI, USDC, USDT, WBTC, WETH9_EXTENDED[1]],
[3]: [ExtendedEther.onChain(3), WETH9_EXTENDED[3]],
[4]: [ExtendedEther.onChain(4), WETH9_EXTENDED[4]],
[5]: [ExtendedEther.onChain(5), WETH9_EXTENDED[5]],
[42]: [ExtendedEther.onChain(42), WETH9_EXTENDED[42]],
[SupportedChainId.MAINNET]: [
ExtendedEther.onChain(SupportedChainId.MAINNET),
DAI,
USDC,
USDT,
WBTC,
WETH9_EXTENDED[SupportedChainId.MAINNET],
],
[SupportedChainId.ROPSTEN]: [
ExtendedEther.onChain(SupportedChainId.ROPSTEN),
WETH9_EXTENDED[SupportedChainId.ROPSTEN],
],
[SupportedChainId.RINKEBY]: [
ExtendedEther.onChain(SupportedChainId.RINKEBY),
WETH9_EXTENDED[SupportedChainId.RINKEBY],
],
[SupportedChainId.GOERLI]: [ExtendedEther.onChain(SupportedChainId.GOERLI), WETH9_EXTENDED[SupportedChainId.GOERLI]],
[SupportedChainId.KOVAN]: [ExtendedEther.onChain(SupportedChainId.KOVAN), WETH9_EXTENDED[SupportedChainId.KOVAN]],
[SupportedChainId.ARBITRUM_ONE]: [
ExtendedEther.onChain(SupportedChainId.ARBITRUM_ONE),
WETH9_EXTENDED[SupportedChainId.ARBITRUM_ONE],
],
[SupportedChainId.ARBITRUM_RINKEBY]: [
ExtendedEther.onChain(SupportedChainId.ARBITRUM_RINKEBY),
WETH9_EXTENDED[SupportedChainId.ARBITRUM_RINKEBY],
],
[SupportedChainId.OPTIMISM]: [ExtendedEther.onChain(SupportedChainId.OPTIMISM)],
[SupportedChainId.OPTIMISTIC_KOVAN]: [ExtendedEther.onChain(SupportedChainId.OPTIMISTIC_KOVAN)],
}
// used to construct the list of all pairs we consider by default in the frontend
export const BASES_TO_TRACK_LIQUIDITY_FOR: ChainTokenList = {
...WETH_ONLY,
[1]: [...WETH_ONLY[1], DAI, USDC, USDT, WBTC],
[SupportedChainId.MAINNET]: [...WETH_ONLY[SupportedChainId.MAINNET], DAI, USDC, USDT, WBTC],
}
export const PINNED_PAIRS: { readonly [chainId: number]: [Token, Token][] } = {
[1]: [
[SupportedChainId.MAINNET]: [
[
new Token(1, '0x5d3a536E4D6DbD6114cc1Ead35777bAB948E3643', 8, 'cDAI', 'Compound Dai'),
new Token(1, '0x39AA39c021dfbaE8faC545936693aC917d5E7563', 8, 'cUSDC', 'Compound USD Coin'),
new Token(SupportedChainId.MAINNET, '0x5d3a536E4D6DbD6114cc1Ead35777bAB948E3643', 8, 'cDAI', 'Compound Dai'),
new Token(
SupportedChainId.MAINNET,
'0x39AA39c021dfbaE8faC545936693aC917d5E7563',
8,
'cUSDC',
'Compound USD Coin'
),
],
[USDC, USDT],
[DAI, USDT],

View File

@@ -0,0 +1,22 @@
{
"name": "Broken Token List",
"timestamp": "2021-01-05T20:47:02.923Z",
"version": {
"major": 1,
"minor": 0,
"patch": 0
},
"tags": {},
"logoURI": "ipfs://QmNa8mQkrNKp1WEEeGjFezDmDeodkWRevGFN8JCV7b4Xir",
"keywords": ["uniswap", "broken"],
"tokens": [
{
"name": "UNI HODL",
"address": "0x4bf5dc91E2555449293D7824028Eb8Fe5879B689",
"symbol": "UniH",
"decimals": 18,
"chainId": 1,
"logoURI": ""
}
]
}

View File

@@ -1,30 +0,0 @@
{
"name": "Unsupported Tokens",
"timestamp": "2021-01-05T20:47:02.923Z",
"version": {
"major": 1,
"minor": 0,
"patch": 0
},
"tags": {},
"logoURI": "ipfs://QmNa8mQkrNKp1WEEeGjFezDmDeodkWRevGFN8JCV7b4Xir",
"keywords": ["uniswap", "unsupported"],
"tokens": [
{
"name": "Gold Tether",
"address": "0x4922a015c4407F87432B179bb209e125432E4a2A",
"symbol": "XAUt",
"decimals": 6,
"chainId": 1,
"logoURI": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0x4922a015c4407F87432B179bb209e125432E4a2A/logo.png"
},
{
"name": "Grump Cat",
"address": "0x93B2FfF814FCaEFFB01406e80B4Ecd89Ca6A021b",
"symbol": "GRUMPY",
"decimals": 9,
"chainId": 1,
"logoURI": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0x4922a015c4407F87432B179bb209e125432E4a2A/logo.png"
}
]
}

File diff suppressed because it is too large Load Diff

View File

@@ -30,6 +30,13 @@ export const USDC_ARBITRUM = new Token(
'USDC',
'USD//C'
)
export const DAI_OPTIMISM = new Token(
SupportedChainId.OPTIMISM,
'0xDA10009cBd5D07dd0CeCc66161FC93D7c9000da1',
18,
'DAI',
'Dai stable coin'
)
export const USDT = new Token(
SupportedChainId.MAINNET,
'0xdAC17F958D2ee523a2206206994597C13D831ec7',
@@ -37,6 +44,13 @@ export const USDT = new Token(
'USDT',
'Tether USD'
)
export const USDT_OPTIMISM = new Token(
SupportedChainId.OPTIMISM,
'0x94b008aA00579c1307B0EF2c499aD98a8ce58e58',
6,
'USDT',
'Tether USD'
)
export const WBTC = new Token(
SupportedChainId.MAINNET,
'0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599',
@@ -44,6 +58,13 @@ export const WBTC = new Token(
'WBTC',
'Wrapped BTC'
)
export const WBTC_OPTIMISM = new Token(
SupportedChainId.OPTIMISM,
'0x68f180fcCe6836688e9084f035309E29Bf0A2095',
8,
'WBTC',
'Wrapped BTC'
)
export const FEI = new Token(
SupportedChainId.MAINNET,
'0x956F47F50A910163D8BF957Cf5846D573E7f87CA',
@@ -79,13 +100,6 @@ export const renBTC = new Token(
'renBTC',
'renBTC'
)
export const UMA = new Token(
SupportedChainId.MAINNET,
'0x04Fa0d235C4abf4BcF4787aF4CF447DE572eF828',
18,
'UMA',
'UMA Voting Token v1'
)
export const ETH2X_FLI = new Token(
SupportedChainId.MAINNET,
'0xAa6E8127831c9DE45ae56bB1b0d4D4Da6e5665BD',
@@ -93,21 +107,6 @@ export const ETH2X_FLI = new Token(
'ETH2x-FLI',
'ETH 2x Flexible Leverage Index'
)
// Mirror Protocol compat.
export const UST = new Token(
SupportedChainId.MAINNET,
'0xa47c8bf37f92abed4a126bda807a7b7498661acd',
18,
'UST',
'Wrapped UST'
)
export const MIR = new Token(
SupportedChainId.MAINNET,
'0x09a3ecafa817268f77be1283176b946c4ff2e608',
18,
'MIR',
'Wrapped MIR'
)
export const UNI: { [chainId: number]: Token } = {
[SupportedChainId.MAINNET]: new Token(SupportedChainId.MAINNET, UNI_ADDRESS[1], 18, 'UNI', 'Uniswap'),
[SupportedChainId.RINKEBY]: new Token(SupportedChainId.RINKEBY, UNI_ADDRESS[4], 18, 'UNI', 'Uniswap'),
@@ -118,20 +117,6 @@ export const UNI: { [chainId: number]: Token } = {
export const WETH9_EXTENDED: { [chainId: number]: Token } = {
...WETH9,
[SupportedChainId.ARBITRUM_ONE]: new Token(
SupportedChainId.ARBITRUM_ONE,
'0x82aF49447D8a07e3bd95BD0d56f35241523fBab1',
18,
'WETH',
'Wrapped Ether'
),
[SupportedChainId.ARBITRUM_RINKEBY]: new Token(
SupportedChainId.ARBITRUM_RINKEBY,
'0xB47e6A5f8b33b3F17603C83a0535A9dcD7E32681',
18,
'WETH',
'Wrapped Ether'
),
[SupportedChainId.OPTIMISM]: new Token(
SupportedChainId.OPTIMISM,
'0x4200000000000000000000000000000000000006',
@@ -146,6 +131,20 @@ export const WETH9_EXTENDED: { [chainId: number]: Token } = {
'WETH',
'Wrapped Ether'
),
[SupportedChainId.ARBITRUM_ONE]: new Token(
SupportedChainId.ARBITRUM_ONE,
'0x82aF49447D8a07e3bd95BD0d56f35241523fBab1',
18,
'WETH',
'Wrapped Ether'
),
[SupportedChainId.ARBITRUM_RINKEBY]: new Token(
SupportedChainId.ARBITRUM_RINKEBY,
'0xB47e6A5f8b33b3F17603C83a0535A9dcD7E32681',
18,
'WETH',
'Wrapped Ether'
),
}
export class ExtendedEther extends Ether {

View File

@@ -5,6 +5,18 @@ import { useUserSingleHopOnly } from '../state/user/hooks'
import { useActiveWeb3React } from './web3'
import { useV3SwapPools } from './useV3SwapPools'
/**
* Returns true if poolA is equivalent to poolB
* @param poolA one of the two pools
* @param poolB the other pool
*/
function poolEquals(poolA: Pool, poolB: Pool): boolean {
return (
poolA === poolB ||
(poolA.token0.equals(poolB.token0) && poolA.token1.equals(poolB.token1) && poolA.fee === poolB.fee)
)
}
function computeAllRoutes(
currencyIn: Currency,
currencyOut: Currency,
@@ -20,7 +32,7 @@ function computeAllRoutes(
if (!tokenIn || !tokenOut) throw new Error('Missing tokenIn/tokenOut')
for (const pool of pools) {
if (currentPath.indexOf(pool) !== -1 || !pool.involvesToken(tokenIn)) continue
if (!pool.involvesToken(tokenIn) || currentPath.find((pathPool) => poolEquals(pool, pathPool))) continue
const outputToken = pool.token0.equals(tokenIn) ? pool.token1 : pool.token0
if (outputToken.equals(tokenOut)) {

View File

@@ -23,7 +23,7 @@ export function useApproveCallback(
amountToApprove?: CurrencyAmount<Currency>,
spender?: string
): [ApprovalState, () => Promise<void>] {
const { account } = useActiveWeb3React()
const { account, chainId } = useActiveWeb3React()
const token = amountToApprove?.currency?.isToken ? amountToApprove.currency : undefined
const currentAllowance = useTokenAllowance(token, account ?? undefined, spender)
const pendingApproval = useHasPendingApproval(token?.address, spender)
@@ -51,6 +51,11 @@ export function useApproveCallback(
console.error('approve was called unnecessarily')
return
}
if (!chainId) {
console.error('no chainId')
return
}
if (!token) {
console.error('no token')
return
@@ -80,7 +85,7 @@ export function useApproveCallback(
return tokenContract
.approve(spender, useExact ? amountToApprove.quotient.toString() : MaxUint256, {
gasLimit: calculateGasMargin(estimatedGas),
gasLimit: calculateGasMargin(chainId, estimatedGas),
})
.then((response: TransactionResponse) => {
addTransaction(response, {
@@ -92,7 +97,7 @@ export function useApproveCallback(
console.debug('Failed to approve token', error)
throw error
})
}, [approvalState, token, tokenContract, amountToApprove, spender, addTransaction])
}, [approvalState, token, tokenContract, amountToApprove, spender, addTransaction, chainId])
return [approvalState, approve]
}

View File

@@ -1,10 +1,12 @@
import { Currency, CurrencyAmount, TradeType } from '@uniswap/sdk-core'
import { encodeRouteToPath, Route, Trade } from '@uniswap/v3-sdk'
import { SupportedChainId } from 'constants/chains'
import { BigNumber } from 'ethers'
import { useMemo } from 'react'
import { useSingleContractMultipleData } from '../state/multicall/hooks'
import { useAllV3Routes } from './useAllV3Routes'
import { useV3Quoter } from './useContract'
import { useActiveWeb3React } from './web3'
export enum V3TradeState {
LOADING,
@@ -14,6 +16,13 @@ export enum V3TradeState {
SYNCING,
}
const QUOTE_GAS_OVERRIDES: { [chainId: number]: number } = {
[SupportedChainId.OPTIMISM]: 6_000_000,
[SupportedChainId.OPTIMISTIC_KOVAN]: 6_000_000,
}
const DEFAULT_GAS_QUOTE = 2_000_000
/**
* Returns the best v3 trade for a desired exact input swap
* @param amountIn the amount to swap in
@@ -23,6 +32,7 @@ export function useBestV3TradeExactIn(
amountIn?: CurrencyAmount<Currency>,
currencyOut?: Currency
): { state: V3TradeState; trade: Trade<Currency, Currency, TradeType.EXACT_INPUT> | null } {
const { chainId } = useActiveWeb3React()
const quoter = useV3Quoter()
const { routes, loading: routesLoading } = useAllV3Routes(amountIn?.currency, currencyOut)
@@ -33,10 +43,17 @@ export function useBestV3TradeExactIn(
])
}, [amountIn, routes])
const quotesResults = useSingleContractMultipleData(quoter, 'quoteExactInput', quoteExactInInputs)
const quotesResults = useSingleContractMultipleData(quoter, 'quoteExactInput', quoteExactInInputs, {
gasRequired: chainId ? QUOTE_GAS_OVERRIDES[chainId] ?? DEFAULT_GAS_QUOTE : undefined,
})
return useMemo(() => {
if (!amountIn || !currencyOut) {
if (
!amountIn ||
!currencyOut ||
// skip when tokens are the same
amountIn.currency.equals(currencyOut)
) {
return {
state: V3TradeState.INVALID,
trade: null,
@@ -104,6 +121,7 @@ export function useBestV3TradeExactOut(
currencyIn?: Currency,
amountOut?: CurrencyAmount<Currency>
): { state: V3TradeState; trade: Trade<Currency, Currency, TradeType.EXACT_OUTPUT> | null } {
const { chainId } = useActiveWeb3React()
const quoter = useV3Quoter()
const { routes, loading: routesLoading } = useAllV3Routes(currencyIn, amountOut?.currency)
@@ -114,10 +132,18 @@ export function useBestV3TradeExactOut(
])
}, [amountOut, routes])
const quotesResults = useSingleContractMultipleData(quoter, 'quoteExactOutput', quoteExactOutInputs)
const quotesResults = useSingleContractMultipleData(quoter, 'quoteExactOutput', quoteExactOutInputs, {
gasRequired: chainId ? QUOTE_GAS_OVERRIDES[chainId] ?? DEFAULT_GAS_QUOTE : undefined,
})
return useMemo(() => {
if (!amountOut || !currencyIn || quotesResults.some(({ valid }) => !valid)) {
if (
!amountOut ||
!currencyIn ||
quotesResults.some(({ valid }) => !valid) ||
// skip when tokens are the same
amountOut.currency.equals(currencyIn)
) {
return {
state: V3TradeState.INVALID,
trade: null,

View File

@@ -1,5 +1,5 @@
import { FeeAmount } from '@uniswap/v3-sdk'
import { Token } from '@uniswap/sdk-core'
import { Token, Currency } from '@uniswap/sdk-core'
import { useFeeTierDistributionQuery } from 'state/data/enhanced'
import { skipToken } from '@reduxjs/toolkit/query/react'
import { reduce } from 'lodash'
@@ -8,6 +8,7 @@ import ReactGA from 'react-ga'
import { useMemo } from 'react'
import { FeeTierDistributionQuery } from 'state/data/generated'
import ms from 'ms.macro'
import { PoolState, usePool } from './usePools'
// maximum number of blocks past which we consider the data stale
const MAX_DATA_BLOCK_AGE = 10
@@ -25,14 +26,26 @@ interface FeeTierDistribution {
}
}
export function useFeeTierDistribution(token0: Token | undefined, token1: Token | undefined): FeeTierDistribution {
const { isFetching, isLoading, isUninitialized, isError, distributions } = usePoolTVL(token0, token1)
export function useFeeTierDistribution(
currencyA: Currency | undefined,
currencyB: Currency | undefined
): FeeTierDistribution {
const { isFetching, isLoading, isUninitialized, isError, distributions } = usePoolTVL(
currencyA?.wrapped,
currencyB?.wrapped
)
// fetch all pool states to determine pool state
const [poolStateLow] = usePool(currencyA, currencyB, FeeAmount.LOW)
const [poolStateMedium] = usePool(currencyA, currencyB, FeeAmount.MEDIUM)
const [poolStateHigh] = usePool(currencyA, currencyB, FeeAmount.HIGH)
return useMemo(() => {
if (isLoading || isFetching || isUninitialized || isError || !distributions) {
return {
isLoading: isLoading || isFetching || !isUninitialized,
isError,
distributions,
}
}
@@ -42,13 +55,18 @@ export function useFeeTierDistribution(token0: Token | undefined, token1: Token
.reduce((a: FeeAmount, b: FeeAmount) => ((distributions[a] ?? 0) > (distributions[b] ?? 0) ? a : b), -1)
const percentages =
!isLoading && !isError && distributions
!isLoading &&
!isError &&
distributions &&
poolStateLow !== PoolState.LOADING &&
poolStateMedium !== PoolState.LOADING &&
poolStateHigh !== PoolState.LOADING
? {
[FeeAmount.LOW]: distributions[FeeAmount.LOW] ? (distributions[FeeAmount.LOW] ?? 0) * 100 : undefined,
[FeeAmount.MEDIUM]: distributions[FeeAmount.MEDIUM]
? (distributions[FeeAmount.MEDIUM] ?? 0) * 100
: undefined,
[FeeAmount.HIGH]: distributions[FeeAmount.HIGH] ? (distributions[FeeAmount.HIGH] ?? 0) * 100 : undefined,
[FeeAmount.LOW]: poolStateLow === PoolState.EXISTS ? (distributions[FeeAmount.LOW] ?? 0) * 100 : undefined,
[FeeAmount.MEDIUM]:
poolStateMedium === PoolState.EXISTS ? (distributions[FeeAmount.MEDIUM] ?? 0) * 100 : undefined,
[FeeAmount.HIGH]:
poolStateHigh === PoolState.EXISTS ? (distributions[FeeAmount.HIGH] ?? 0) * 100 : undefined,
}
: undefined
@@ -58,7 +76,7 @@ export function useFeeTierDistribution(token0: Token | undefined, token1: Token
distributions: percentages,
largestUsageFeeTier: largestUsageFeeTier === -1 ? undefined : largestUsageFeeTier,
}
}, [isLoading, isFetching, isUninitialized, isError, distributions])
}, [isLoading, isFetching, isUninitialized, isError, distributions, poolStateLow, poolStateMedium, poolStateHigh])
}
function usePoolTVL(token0: Token | undefined, token1: Token | undefined) {
@@ -67,7 +85,7 @@ function usePoolTVL(token0: Token | undefined, token1: Token | undefined) {
const { isLoading, isFetching, isUninitialized, isError, data } = useFeeTierDistributionQuery(
token0 && token1 ? { token0: token0.address.toLowerCase(), token1: token1.address.toLowerCase() } : skipToken,
{
pollingInterval: ms`1m`,
pollingInterval: ms`2m`,
}
)

View File

@@ -0,0 +1,23 @@
import { FeeAmount, nearestUsableTick, TickMath, TICK_SPACINGS } from '@uniswap/v3-sdk'
import { useMemo } from 'react'
import { Bound } from 'state/mint/v3/actions'
export default function useIsTickAtLimit(
feeAmount: FeeAmount | undefined,
tickLower: number | undefined,
tickUpper: number | undefined
) {
return useMemo(
() => ({
[Bound.LOWER]:
feeAmount && tickLower
? tickLower === nearestUsableTick(TickMath.MIN_TICK, TICK_SPACINGS[feeAmount as FeeAmount])
: undefined,
[Bound.UPPER]:
feeAmount && tickUpper
? tickUpper === nearestUsableTick(TickMath.MAX_TICK, TICK_SPACINGS[feeAmount as FeeAmount])
: undefined,
}),
[feeAmount, tickLower, tickUpper]
)
}

View File

@@ -0,0 +1,37 @@
import ReactGA from 'react-ga'
import { stringify } from 'qs'
import useParsedQueryString from 'hooks/useParsedQueryString'
import { useLocation } from 'react-router-dom'
import { LocationDescriptor } from 'history'
import { SupportedLocale } from 'constants/locales'
import { useActiveLocale } from './useActiveLocale'
import { useMemo } from 'react'
export function useLocationLinkProps(locale: SupportedLocale | null): {
to?: LocationDescriptor
onClick?: () => void
} {
const location = useLocation()
const qs = useParsedQueryString()
const activeLocale = useActiveLocale()
return useMemo(
() =>
!locale
? {}
: {
to: {
...location,
search: stringify({ ...qs, lng: locale }),
},
onClick: () => {
ReactGA.event({
category: 'Localization',
action: 'Switch Locale',
label: `${activeLocale} -> ${locale}`,
})
},
},
[location, qs, activeLocale, locale]
)
}

View File

@@ -2,12 +2,11 @@ import { Currency } from '@uniswap/sdk-core'
import { FeeAmount, Pool, tickToPrice, TICK_SPACINGS } from '@uniswap/v3-sdk'
import JSBI from 'jsbi'
import { PoolState, usePool } from './usePools'
import { useEffect, useMemo, useState } from 'react'
import { useMemo } from 'react'
import computeSurroundingTicks from 'utils/computeSurroundingTicks'
import { useAllV3TicksQuery } from 'state/data/enhanced'
import { skipToken } from '@reduxjs/toolkit/query/react'
import ms from 'ms.macro'
import cloneDeep from 'lodash/cloneDeep'
import { AllV3TicksQuery } from 'state/data/generated'
const PRICE_FIXED_DIGITS = 8
@@ -33,7 +32,7 @@ export function useAllV3Ticks(
currencyA && currencyB && feeAmount ? Pool.getAddress(currencyA?.wrapped, currencyB?.wrapped, feeAmount) : undefined
//TODO(judo): determine if pagination is necessary for this query
const { isLoading, isError, data } = useAllV3TicksQuery(
const { isLoading, isError, error, isUninitialized, data } = useAllV3TicksQuery(
poolAddress ? { poolAddress: poolAddress?.toLowerCase(), skip: 0 } : skipToken,
{
pollingInterval: ms`2m`,
@@ -42,8 +41,10 @@ export function useAllV3Ticks(
return {
isLoading,
isUninitialized,
isError,
ticks: data?.ticks,
error,
ticks: data?.ticks as AllV3TicksQuery['ticks'],
}
}
@@ -53,63 +54,82 @@ export function usePoolActiveLiquidity(
feeAmount: FeeAmount | undefined
): {
isLoading: boolean
isUninitialized: boolean
isError: boolean
error: any
activeTick: number | undefined
data: TickProcessed[]
data: TickProcessed[] | undefined
} {
const [ticksProcessed, setTicksProcessed] = useState<TickProcessed[]>([])
const pool = usePool(currencyA, currencyB, feeAmount)
const { isLoading, isError, ticks } = useAllV3Ticks(currencyA, currencyB, feeAmount)
// Find nearest valid tick for pool in case tick is not initialized.
const activeTick = useMemo(() => getActiveTick(pool[1]?.tickCurrent, feeAmount), [pool, feeAmount])
useEffect(() => {
if (!currencyA || !currencyB || !activeTick || pool[0] !== PoolState.EXISTS || !ticks || ticks.length === 0) {
setTicksProcessed([])
return
const { isLoading, isUninitialized, isError, error, ticks } = useAllV3Ticks(currencyA, currencyB, feeAmount)
return useMemo(() => {
if (
!currencyA ||
!currencyB ||
activeTick === undefined ||
pool[0] !== PoolState.EXISTS ||
!ticks ||
ticks.length === 0 ||
isLoading ||
isUninitialized
) {
return {
isLoading: isLoading || pool[0] === PoolState.LOADING,
isUninitialized,
isError,
error,
activeTick,
data: undefined,
}
}
const token0 = currencyA?.wrapped
const token1 = currencyB?.wrapped
const sortedTickData = cloneDeep(ticks as AllV3TicksQuery['ticks'])
sortedTickData.sort((a, b) => a.tickIdx - b.tickIdx)
// find where the active tick would be to partition the array
// if the active tick is initialized, the pivot will be an element
// if not, take the previous tick as pivot
const pivot = sortedTickData.findIndex(({ tickIdx }) => tickIdx > activeTick) - 1
const pivot = ticks.findIndex(({ tickIdx }) => tickIdx > activeTick) - 1
if (pivot < 0) {
// consider setting a local error
console.error('TickData pivot not found')
return
return {
isLoading,
isUninitialized,
isError,
error,
activeTick,
data: undefined,
}
}
const activeTickProcessed: TickProcessed = {
liquidityActive: JSBI.BigInt(pool[1]?.liquidity ?? 0),
tickIdx: activeTick,
liquidityNet:
sortedTickData[pivot].tickIdx === activeTick ? JSBI.BigInt(sortedTickData[pivot].liquidityNet) : JSBI.BigInt(0),
Number(ticks[pivot].tickIdx) === activeTick ? JSBI.BigInt(ticks[pivot].liquidityNet) : JSBI.BigInt(0),
price0: tickToPrice(token0, token1, activeTick).toFixed(PRICE_FIXED_DIGITS),
}
const subsequentTicks = computeSurroundingTicks(token0, token1, activeTickProcessed, sortedTickData, pivot, true)
const subsequentTicks = computeSurroundingTicks(token0, token1, activeTickProcessed, ticks, pivot, true)
const previousTicks = computeSurroundingTicks(token0, token1, activeTickProcessed, sortedTickData, pivot, false)
const previousTicks = computeSurroundingTicks(token0, token1, activeTickProcessed, ticks, pivot, false)
const newTicksProcessed = previousTicks.concat(activeTickProcessed).concat(subsequentTicks)
const ticksProcessed = previousTicks.concat(activeTickProcessed).concat(subsequentTicks)
setTicksProcessed(newTicksProcessed)
}, [currencyA, currencyB, activeTick, pool, ticks])
return {
isLoading: isLoading || pool[0] === PoolState.LOADING,
isError: isError || pool[0] === PoolState.INVALID,
activeTick,
data: ticksProcessed,
}
return {
isLoading,
isUninitialized,
isError: isError,
error,
activeTick,
data: ticksProcessed,
}
}, [currencyA, currencyB, activeTick, pool, ticks, isLoading, isUninitialized, isError, error])
}

View File

@@ -33,7 +33,10 @@ export function usePositionTokenURI(tokenId: TokenId | undefined): UsePositionTo
() => [tokenId instanceof BigNumber ? tokenId.toHexString() : tokenId?.toString(16)],
[tokenId]
)
const { result, error, loading, valid } = useSingleCallResult(contract, 'tokenURI', inputs, NEVER_RELOAD, 3_000_000)
const { result, error, loading, valid } = useSingleCallResult(contract, 'tokenURI', inputs, {
...NEVER_RELOAD,
gasRequired: 3_000_000,
})
return useMemo(() => {
if (error || !valid || !tokenId) {

30
src/hooks/useRouter.ts Normal file
View File

@@ -0,0 +1,30 @@
import { skipToken } from '@reduxjs/toolkit/query/react'
import { Currency, CurrencyAmount } from '@uniswap/sdk-core'
import ms from 'ms.macro'
import { useBlockNumber } from 'state/application/hooks'
import { useGetQuoteQuery } from 'state/routing/slice'
import { useActiveWeb3React } from './web3'
export function useRouterTradeExactIn(amountIn?: CurrencyAmount<Currency>, currencyOut?: Currency) {
const { account } = useActiveWeb3React()
const blockNumber = useBlockNumber()
const { isLoading, isError, data } = useGetQuoteQuery(
amountIn && currencyOut && account && blockNumber
? {
tokenInAddress: amountIn.currency.wrapped.address,
tokenInChainId: amountIn.currency.chainId,
tokenOutAddress: currencyOut.wrapped.address,
tokenOutChainId: currencyOut.chainId,
amount: amountIn.quotient.toString(),
type: 'exactIn',
}
: skipToken,
{ pollingInterval: ms`10s` }
)
// todo(judo): validate block number for freshness
return !isLoading && !isError ? data?.routeString : undefined
}

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