Compare commits

...

27 Commits

Author SHA1 Message Date
Noah Zinsmeister
e667615449 support proposed new FoT-capable methods (#866)
* support new FoT-capable methods

short-circuit modal footer rendering if no trade

improve clarity of tx modal flow

* update router address + ABI
2020-06-05 18:40:02 -04:00
CryptoCatVC
4ab61faeae Create he.json (#864) 2020-06-05 18:30:45 -04:00
Ian Lapham
0004db3d4a Style updates, Approve UI updates (#841)
improvements(approvals): match approve flow to add/remove, update UI styles
2020-06-05 13:20:47 -04:00
Moody Salem
c133c472be fix(search): fix searching tokens to be more tolerant, and sort the exact matched symbols first 2020-06-05 11:20:04 -04:00
Noah Zinsmeister
0019ccdf51 add ren* tokens 2020-06-04 20:58:14 -04:00
Moody Salem
5a1a469f35 perf(multicall): remove the validation that caused app to feel sluggish 2020-06-03 23:19:19 -04:00
Moody Salem
4c28f34803 remove vercel.json to put it in another repo 2020-06-03 22:35:56 -04:00
Moody Salem
104be830fc perf(multicall): use single call to get token information (#855)
* use single call to get token information

* delete the bytes32 overload

* console log statement

* add a bunch of tests to actions.ts for multicall

* fix to work with bytes32 symbols/names

* only include name/symbol

* enforce lowercase calldata
2020-06-03 22:15:26 -04:00
Moody Salem
24c70791cd lint error 2020-06-03 20:23:01 -04:00
Moody Salem
216fdea290 fix(instrumentation): event for migration v1->v2 2020-06-03 20:22:28 -04:00
Moody Salem
40e4ce2ed3 fix(style): add missing padding 2020-06-03 17:23:21 -04:00
Moody Salem
b2508fc6f2 Add missing capture group 2020-06-03 14:52:38 -04:00
Moody Salem
f73b37287f Remove the invalid redirect 2020-06-03 14:48:25 -04:00
Moody Salem
c09eb738c3 Add vercel.json for redirects in vercel deployment 2020-06-03 14:46:02 -04:00
Moody Salem
6de3a6ec28 Avoid duplicate popups for mined transactions 2020-06-03 14:30:07 -04:00
Moody Salem
c1d35cc8b3 feat(migrate): adds the migrate flow to the uniswap exchange site
* links and page

* print all the details of the liquidity

* show working approve/migrate buttons

* testnet v1 factory addresses

* split code up into two pages

* getting closer to styled

* compute min amount out of eth and token

* compute min amount out of eth and token

* add a back button to the list page

* Improve empty states

* Improve the state management

* change the display of the migrate page to be more similar to original

* style fix, pending transaction hook fix

* add forwarding to netlify.toml

* handle case where v2 spot price does not exist because pair does not exist

* make ternaries more accurate

* handle first liquidity provider situation

* Style tweaks for migrate

* merge

* Address feedback
- show pool token amount
- show success when migration complete
- show price warning only if price difference is large

Co-authored-by: Callil Capuozzo <callil.capuozzo@gmail.com>
2020-06-03 14:07:01 -04:00
Moody Salem
f279b2bea2 fix(misc): migrate link and gradient cut off 2020-06-03 13:36:31 -04:00
Noah Zinsmeister
6ffbf756f8 memoize common bases (#853)
* memoize common bases

* improve memoization
2020-06-02 15:43:10 -04:00
Moody Salem
10837d7ba1 fix(scrolling): too much scrolling 2020-06-02 12:28:35 -04:00
Noah Zinsmeister
2d6eddf9d4 introduce batched usePairs (#851)
centralize all lists of bases

pin some pairs
2020-06-01 16:55:31 -04:00
Moody Salem
aadf43efc3 chore(v1): add ipfs links for v1 to the v1 release 2020-06-01 11:20:05 -04:00
Noah Zinsmeister
227f729ecd add AST and EBASE 2020-06-01 10:53:40 -04:00
Moody Salem
a5b15e37f6 fix(application state): fix block number updates after changing networks 2020-05-31 15:33:31 -04:00
Moody Salem
2408b2966e fix(misc): fix styling on the having trouble link 2020-05-31 00:24:03 -04:00
Moody Salem
dc391d1bea fix(misc): fix the blinking ethereum logo on ios safari and temporarily remove the overflow settings to fix double scrolling situation 2020-05-31 00:15:17 -04:00
Moody Salem
e2d0514344 chore(cleanup): separate the components for 3 kinds of links we use 2020-05-30 23:33:16 -04:00
Moody Salem
98d25dd2af perf(cleanup): remove the material ui dependency for the input range 2020-05-30 21:22:59 -04:00
83 changed files with 1965 additions and 1242 deletions

View File

@@ -50,3 +50,8 @@ The frontend will not work on other networks.
**Please open all pull requests against the `v2` branch.**
CI checks will run against all PRs.
## Accessing Uniswap V1 interface
The Uniswap V1 interface for mainnet and testnets is accessible via IPFS gateways linked
from the [v1.0.0 release](https://github.com/Uniswap/uniswap-frontend/releases/tag/v1.0.0).

View File

@@ -1,4 +1,4 @@
import { TEST_ADDRESS_NEVER_USE } from '../support/commands'
import { TEST_ADDRESS_NEVER_USE_SHORTENED } from '../support/commands'
describe('Landing Page', () => {
beforeEach(() => cy.visit('/'))
@@ -22,6 +22,6 @@ describe('Landing Page', () => {
it('is connected', () => {
cy.get('#web3-status-connected').click()
cy.get('#web3-account-identifier-row').contains(TEST_ADDRESS_NEVER_USE)
cy.get('#web3-account-identifier-row').contains(TEST_ADDRESS_NEVER_USE_SHORTENED)
})
})

View File

@@ -1,5 +1,7 @@
export const TEST_ADDRESS_NEVER_USE: string
export const TEST_ADDRESS_NEVER_USE_SHORTENED: string
// declare namespace Cypress {
// // eslint-disable-next-line @typescript-eslint/class-name-casing
// interface cy {

View File

@@ -14,6 +14,8 @@ const PRIVATE_KEY_TEST_NEVER_USE = '0xad20c82497421e9784f18460ad2fe84f73569068e9
// address of the above key
export const TEST_ADDRESS_NEVER_USE = '0x0fF2D1eFd7A57B7562b2bf27F3f37899dB27F4a5'
export const TEST_ADDRESS_NEVER_USE_SHORTENED = '0x0fF2...F4a5'
class CustomizedBridge extends _Eip1193Bridge {
async sendAsync(...args) {
console.debug('sendAsync called', ...args)

View File

@@ -7,6 +7,13 @@
conditions = {Country=["BY","CU","IR","IQ","CI","LR","KP","SD","SY","ZW"]}
headers = {Link="<https://uniswap.exchange>"}
# forward migrate
[[redirects]]
from = "https://migrate.uniswap.exchange/*"
to = "https://uniswap.exchange/migrate/v1"
status = 301
force = true
# forward v2 subdomain to apex
[[redirects]]
from = "https://v2.uniswap.exchange/*"

View File

@@ -13,7 +13,6 @@
"@ethersproject/strings": "^5.0.0-beta.136",
"@ethersproject/units": "^5.0.0-beta.132",
"@ethersproject/wallet": "^5.0.0-beta.141",
"@material-ui/core": "^4.9.5",
"@popperjs/core": "^2.4.0",
"@reach/dialog": "^0.10.3",
"@reach/portal": "^0.10.3",
@@ -34,7 +33,7 @@
"@typescript-eslint/parser": "^2.31.0",
"@uniswap/sdk": "^2.0.5",
"@uniswap/v2-core": "1.0.0",
"@uniswap/v2-periphery": "1.0.0-beta.0",
"@uniswap/v2-periphery": "^1.1.0-beta.0",
"@web3-react/core": "^6.0.9",
"@web3-react/fortmatic-connector": "^6.0.9",
"@web3-react/injected-connector": "^6.0.7",

75
public/locales/he.json Normal file
View File

@@ -0,0 +1,75 @@
{
"noWallet": "לא נמצא ארנק",
"wrongNetwork": "נבחרה רשת לא נכונה",
"switchNetwork": "{{ correctNetwork }} יש צורך לשנות את הרשת ל",
"installWeb3MobileBrowser": "יש צורך בארנק ווב3.0, תתקין מטאמאסק או ארנק דומה",
"installMetamask": " Metamask יש צורך להתקין תוסף מטאמאסק לדפדפן, חפשו בגוגל ",
"disconnected": "מנותק",
"swap": "המרה",
"send": "שליחה",
"pool": "להפקיד",
"betaWarning": "הפרויקט נמצא בשלב בטא, השתמשו באחריות",
"input": "מוכר",
"output": "אקבל",
"estimated": "הערכה",
"balance": "בארנק שלי {{ balanceInput }}",
"unlock": "שחרור נעילת ארנק",
"pending": "ממתין לאישור",
"selectToken": "בחרו את הטוקן להמרה",
"searchOrPaste": "הכניסו שם או כתובת של טוקן לחיפוש",
"noExchange": "לא מתאפשרת המרה",
"exchangeRate": "שער המרה",
"enterValueCont": "כדי להמשיך {{ missingCurrencyValue }} הזינו ",
"selectTokenCont": "בחרו טוקן כדי להמשיך",
"noLiquidity": "אין נזילות",
"unlockTokenCont": "יש צורך לאשר את הטוקן למסחר",
"transactionDetails": "פרטי הטרנזקציה",
"hideDetails": "הסתר פרטים נוספים",
"youAreSelling": "למכירה",
"orTransFail": "או שהטרנזקציה תיכשל",
"youWillReceive": "תוצר המרה מינימלי",
"youAreBuying": "קונה",
"itWillCost": "זה יעלה",
"insufficientBalance": "אין בחשבון מספיק מטבעות",
"inputNotValid": "קלט לא תקין",
"differentToken": "יש צורך בטוקנים שונים",
"noRecipient": "לא הוכנסה כתובת ארנק יעד",
"invalidRecipient": "לא הוכנסה כתובת תקינה",
"recipientAddress": "כתובת יעד",
"youAreSending": "כמות לשליחה",
"willReceive": "יתקבל לכל הפחות",
"to": "אל",
"addLiquidity": "להוספת נזילות למאגר",
"deposit": "הפקדה",
"currentPoolSize": "גודל מאגר הנזילות הכולל",
"yourPoolShare": "חלקך במאגר הנזילות",
"noZero": "אפס אינו ערך תקין",
"mustBeETH": "ETH חייב להופיע באחד מהצדדים",
"enterCurrencyOrLabelCont": "כדי להמשיך {{ inputCurrency }} או {{ label }} הכנס",
"youAreAdding": "מתווספים למאגר",
"and": "וגם",
"intoPool": "לתוך הנזילות",
"outPool": "מתוך",
"youWillMint": "יונפקו לכם",
"liquidityTokens": "טוקנים של נזילות",
"totalSupplyIs": "חלקך במאגר הנזילות",
"youAreSettingExRate": "שער ההמרה יקבע על ידך",
"totalSupplyIs0": "אין לך טוקנים של נזילות",
"tokenWorth": "שווי כל טוקן נזילות הינו",
"firstLiquidity": "את\ה הראשון\ה שמזרים נזילות למאגר",
"initialExchangeRate": "ושל האית'ר הינן בערך שווה {{ label }} תוודאו שההפקדה של הטוקן",
"removeLiquidity": "הוצאה של נזילות",
"poolTokens": "טוקנים של מאגר הנזילות",
"enterLabelCont": "כדי להמשיך {{ label }} הכנס ",
"youAreRemoving": "יוסרו",
"youWillRemove": "יוסרו",
"createExchange": "ליצירת זוג מסחר",
"invalidTokenAddress": "כתובת טוקן לא נכונה",
"exchangeExists": "{{ label }} כבר קיים זוג המרה עבור",
"invalidSymbol": "תו שגוי",
"invalidDecimals": "ספרות עשרוניות שגויות",
"tokenAddress": "כתובת הטוקן",
"label": "שם",
"decimals": "ספרות עשרויות",
"enterTokenCont": "הכניסו כתובת טוקן כדי להמשיך"
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

View File

@@ -1,9 +0,0 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<rect width="20" height="20" fill="url(#pattern0)"/>
<defs>
<pattern id="pattern0" patternContentUnits="objectBoundingBox" width="1" height="1">
<use xlink:href="#image0" transform="scale(0.0078125)"/>
</pattern>
<image id="image0" width="128" height="128" xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAYAAADDPmHLAAAAAXNSR0IArs4c6QAADd9JREFUeNrtXdtzE+cV38kweWpm+tI8JZP8EZ0pL+UhferkfyAzFqEpadq0hbRNUlsYQ2ycBEgCgQQIgVxMbC6BAHbEJZg7dW4OpeCWwDTNDOyuLFuybMsXfT2/tWVkWavd1e5K58g6M9+Q8cTS+ju//b5z/R1Nq0F5Ppr46cpm8xcroubyFU3m+oYmY39Do97d0KRfoH/76d/b9K9JP89Yy/pv62f9s/9PN34Hv4vPwGfhM7W68JNVUfWTp9fqv440Ge2RRqOXFHiPFKhCWfTZ+A58F74T313XQIUlGlUP0lv5RCRqtkQa9YukjMnQFO6w8N3WM9Cz4JnwbHUNhSQrovGlkSZ9K216vFoKdwGIOJ4Rz1rXWADyTDTxeCSqv0wbe5Or0kuA4SaeHX9DXZMepSFq/Jw28FBDo5GVpviFy8hafwv9TXXNOh3zzfov6Qjtka90u1NB78HfWNd0gUTWmr+yLPgaVfwCIMCToL+5/sZH44+Qa9W5WBRfxK3sxB4sRlduSSRqrKYjMbVolX//WkhhL7Ani+O4j+rL6Ai8ttgVX+RauIa9qfG3Xm+tDcs+RI+B9qjmToNVLfFHKWJ2vq5gt6eBfr5mbIPIWuNJK9lSV6zXZWLvJB/5D1AApK1+5PsOIrVhL8UlbMiy7agrMKArgfZSTKKpoc14iHLpMUkb/OZHQ6pxa5x5zID2lPaWefIm9TC5M32SlP/MOl0Zg1PqWG9aUY6fu6vY99yG1M94GnsticfozhqQdryeOJ9W09NZ9cU/Rq2TQIBdMIC9ZvfmS1T+39+Kq6mp7BwATl8dVX/caIoAAfaczZ0v7djPrYE7EwqSAwDW/u6UlMhhX9VtAlim0gy+3Np9KKlykg8ArPXvDApJJhmxqnkHlp8v1NV77hVDJUembQHQfSFtGYeCXMTKxwlmgjwy/erevlGVL4UAwHqna1hS+VlbFcK7MiN8698dVNls1hEAWC+8bsqJGFYqbIzEjtTY/oqorn64O6kKxQ4AB0+NIF8vJncA3YSf0hWc1YOFX0zsAIDVvichKosYaip5Jp8vU/l/ftVUY+NZzwCIXRpVz24wBOUN9NbQKnkkZ/a+vD6u7KQUALDeP5KUVlSyLISjX24Z16Z9Q6qUOAEA6+U344KuAuNaoFcBoWqNVOX/pllXOiV7/ALg6NkR9XRUVC3BmsBKtyVX70JxTuIGAFhvfDgkKDagpwIpKyPLskuq8l98I64mJ7OBAeD0lVH1fJshySvo8t2xI7mS5vr3GeVG3AIA6+MTKVF74KsDSXK7FkK5bsULALCatw9KMgh7y27UlKp8+O1DqenQAIAiEhiXYiKg5TSkSu7SxV3tRbwCAGt7p6Rkkd7jze2jXnapysfxDIWGDQCs1a+ZcvbGCz/BDDmDyEZLdefHCeVVygXAgZicZBF06rK+L/G41JDvh8eSqhwpFwBYbbsTYkLEruhqwGsjUfko5hwdm644AGKX0mrVehmxAejWTdh3QCIArvSPqXLFDwCwdh9OSrkGbjqFfZdKVP6rlLP3I34BgPXiFhnJopIUduC4k6b8ldTNc9ecrDoAjpwZsSqOBBjKW+0bOhmTMNqtw6dHlF8JAgBYSDsLuAbiRUvJQXUqTfl/3WyqiYksGwCcogDU71sNAdeA+UQR699skQaA7/6dUUFIUADAgivK3xswW4qlfS9KUv62/cMqKAkSAFjRbbyTRdD1/FJvoj2vJuu217WqxVCDw1NsAXD8XNoyThnbAZPzqO7BfS/p7f/8YloFKUEDAGtrB2+DEDrPj/23S1F+07a452RPNQBwhlrNUYrO+BRoF1n4ceuHiUCVjzax67cy6pOeVOAg6Pw8xdgOyCsUCXXMSoAL9flBSYbcR/QKwHWDsta+PahadyXU3qNJq54gKBBs2MnUICSdW8rHMCQJyod/PTI67VvxQ8lpdfGbsQVKBgByC/wAOw8Mqx6yNfwCAPbKb1t47qk1CAsTsSQA4MLXY74U/6M+qc5SW/gZG0XlAyC31lFxyVtkzKG03A8Idh7kWT0E3dP9bz7FXfk4msuVgTsZetud3+RiAMhfm/YlrDu9XBAgaskwIrgc+f8NrN0VSrDg7fUiE9QL8PUNut8vuz/CnQCQWxupAOSDz5IWoZQXACBnwa16CLrXZocqsgVAVyzlWvGgfbn07ZirN75cAMzZCUQ0sYuO9pgHO+G195lVD5HuWVf/riGGjvGMs89/15hUvV/a3+9hACC3mncMqm1kJxxzYSecujxq8ROxqhbmnAP45sZ4ScX/578ZdSYgl61cAOSvzWQnHIiVthP2HU3yygk0YF4uU/7eYjJJxI7f3oT/ng40aBMEAHKr/b2ElRW0sxMY8RL3a7NDk9nx95qJ+ckexACufDcWaJAmLADk1gayE3YfGlYnL80HKxteYtK9xpHwCdm0nNyLT6lzX83E1sNQfJgAmIsnUGBp+/4hdbz3PhCY8BKb2uz4dHb8vbf/NxG60isFgDmDkQJLW4hj4OCpFA9eYtI9OwAcOzcyF5+v5KoEAObZCXvIaPxgiAEAmF0BSKGCzq3WAfA3KiNn0GFssjQCc02ex3rTNQcAXHG/40I7N2sE9nMOA2+hYxIBFOkAQJ0g7nxm4WDLDbwggd17z6dJsQAA5zDLcTSke5wA3XL6AOIWf68UALxEJFVcawFmVze7ZJBT2hRH6Cs7E6rnAt9IICJ9bhpE/lBtxjErGcQsHbyRwqiXKaPnxL6BaCF6A05f5QOAKC14MU73fO5KA41dVesBmsz1aAlbzu1ogvWPmj3k0J1cJRhWHx1PVR0AuJ5WNjuWYltkk4hzcIgEWgUhHEvCYP3nqn+RE8Cb7vQ7OHaPfjFScQDArXvWBUEE3FqEuPFdqHHg4A1YJWFci0L/sokYP8bvF4HeuJ2xegKcBkKg6OLk5fABALfODWsorrL8knPQz3NpILWKQjmXhReSPaKBA/kBpw3EG7mLsnBhAQCFKk7E0Whfe/fAwmdYt2OQiwt4T0RjCEq4CyVNqWHc+06EDPC/u2KpwAAAo81pmhieCYwlsUsLvRSMqmPZGMK5NQxvkh6fsi31dlNnhzp/sHuWCwDYF25KuWAPgCnEjm5+JSN20XmtYdybQ3FsIkVsJ6gAdoofYPNhedsVlNi5dX9qd3brYAugAsiWZfwqvwlk85pDJbSHoya/lIAWHlY2TgynDiPU5TkBAEaoU4s3XFR0ATvFIl7fy6saeEF7uASCCLyFbujf0fq1y0Unzks0AubTvON6LnxLP3cCERbKvbpdRCM7GNLLLyCIkEIRg+M4lXbXH/g9VRS1OMwABqgQeUT/HlxMN6FZXDUHTrqLNyBczXHiWFGKGCkkUYikeWn9Rk8hgOMUVnbyKJDDf++wt4wko+pfZ5IoSTRxqA/wIpgXCHewHNoW+Pub93mvSeBKJ29LEyeJKBIWfbERsE4Cd9JLDB5eQDlVSchhcJ0wZksUKY0qFj53uRyB/yRGkFJzAJHR6yizLhGJHqcrhy1V7GxQ6KYUEMCd88MLhJxBfn0eijd2dA77yiqilV0sWbREuviv/jXuizgCXgVoYXLegB/lc+r7K5suXtrACIRo/XIGBsEShiCUU55AxMAIiSNjQNrghzpuMdDFux4ZI3Vo1Gdn01UDAJM+v9LLy9AoiWPjVkTL5xD0AwDkKLgPjvI8Nk7q4MgXCqqIwgYAcv4i6OHLGRwpdXSsl5GxfgHAprrHbeHHYhke7ZVTsBwAoORMwl74Gh49Wy/YKQ0ASOfei0+FBgBUH6+UMDuYdKf5FQodPkJGREoaCHA8T05lAwcAij/WCBgZC51Bd1oQEokaqyVeBU5VROUAgB3Xn23Uz1itBSWUPlxCxsQ1aQCwqohuZQIDwMcnUjL+btIVdKYFKRRHXiZxpjAyc2AQ9QsAq7pHxIhYIwtdaWEIfXCrxKsAJBN+AYD0s4yjX2/VwpKZq0A/LxEEpaqInADw9idCXD7STeBHf6Gsaok/ypFb0E8VUSkAHDrFt7qnkPAJutEqIZG1xpMS7QFUAWWKVBHZAQCnRtW5/Nze+6QTrZJC6cU2iVfB3iJVRHYAABOJDG/HaNMqLXTXPEC+ZodEEGBYlBMA9h5JSvH3O6ALrRqC8mLimYlJA0BhFVEhAPhX98xx/MRsS7wrJQ1txkMUeOiTBoK2vCqiQgCA7kVAsKcPe69xkGeiqYfJEBmQBgK0bBcCAJ1HAoy+Aey5xkkiLYnHpIEgV0WUA4CE6h7sMfZa4yhApbTrAH37YBxBdQ+neT52xz67N7+YTSDNMEQfH1i8uBt8bO58N96BVBeRsav3oCZJrDiBFSySFzFkFeGjPayanx9c2Fhe7oBDbL/i4d0wE0hSs4jVyupVLLFTwSthyUw9Qf1KcCjmaA09pVvVK4GqVSSWl1WijCu0Sh6OpwGhfY3EauMwqnexFzX91pcsOW/UuxbxXd8VWOm26GuBulcktqH5adfy3bFTkycCNTFK60r22qVbdqPmYhL0ss+QVNSCx2AFcw557s+vywxdzQxnkbxUs5W1o2d3TctSF0eDcSk47jiTWeLZ8IyOVGx18ZdoAtUp+G5BelxNlnN8t/UM9Cx4JnEJm5oIMxPtObjvMQDB8iTCHH9Dn21Z8PRd+M4FlOt14SEYhoSJWJFG8ylrLiINSIQFPkuJ3z87MNvE+HRrWf9t/ax/5lQhTwS/Q7+Lz8BnzQ1YqjH5P29N0rBVv2N5AAAAAElFTkSuQmCC"/>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 5.2 KiB

View File

@@ -2,25 +2,25 @@ import React from 'react'
import styled from 'styled-components'
import useCopyClipboard from '../../hooks/useCopyClipboard'
import { Link } from '../../theme'
import { LinkStyledButton } from '../../theme'
import { CheckCircle, Copy } from 'react-feather'
const CopyIcon = styled(Link)`
color: ${({ theme }) => theme.text4};
const CopyIcon = styled(LinkStyledButton)`
color: ${({ theme }) => theme.text3};
flex-shrink: 0;
display: flex;
margin-right: 1rem;
margin-left: 0.5rem;
text-decoration: none;
font-size: 0.825rem;
:hover,
:active,
:focus {
text-decoration: none;
color: ${({ theme }) => theme.text3};
color: ${({ theme }) => theme.text2};
}
`
const TransactionStatusText = styled.span`
margin-left: 0.25rem;
font-size: 0.825rem;
${({ theme }) => theme.flexRowNoWrap};
align-items: center;
`
@@ -30,7 +30,6 @@ export default function CopyHelper(props: { toCopy: string; children?: React.Rea
return (
<CopyIcon onClick={() => setCopied(props.toCopy)}>
{props.children}
{isCopied ? (
<TransactionStatusText>
<CheckCircle size={'16'} />
@@ -41,6 +40,7 @@ export default function CopyHelper(props: { toCopy: string; children?: React.Rea
<Copy size={'16'} />
</TransactionStatusText>
)}
{isCopied ? '' : props.children}
</CopyIcon>
)
}

View File

@@ -1,55 +1,39 @@
import React from 'react'
import styled from 'styled-components'
import { Check, Triangle } from 'react-feather'
import { CheckCircle, Triangle, ExternalLink as LinkIcon } from 'react-feather'
import { useActiveWeb3React } from '../../hooks'
import { getEtherscanLink } from '../../utils'
import { Link, Spinner } from '../../theme'
import Circle from '../../assets/images/circle.svg'
import { transparentize } from 'polished'
import { ExternalLink } from '../../theme'
import { useAllTransactions } from '../../state/transactions/hooks'
import { RowFixed } from '../Row'
import Loader from '../Loader'
const TransactionWrapper = styled.div`
margin-top: 0.75rem;
`
const TransactionWrapper = styled.div``
const TransactionStatusText = styled.div`
margin-right: 0.5rem;
`
const TransactionState = styled(Link)<{ pending: boolean; success?: boolean }>`
display: flex;
justify-content: space-between;
text-decoration: none !important;
border-radius: 0.5rem;
padding: 0.5rem 0.75rem;
font-weight: 500;
font-size: 0.75rem;
border: 1px solid;
color: ${({ pending, success, theme }) => (pending ? theme.primary1 : success ? theme.green1 : theme.red1)};
border-color: ${({ pending, success, theme }) =>
pending
? transparentize(0.75, theme.primary1)
: success
? transparentize(0.75, theme.green1)
: transparentize(0.75, theme.red1)};
align-items: center;
:hover {
border-color: ${({ pending, success, theme }) =>
pending
? transparentize(0, theme.primary1)
: success
? transparentize(0, theme.green1)
: transparentize(0, theme.red1)};
text-decoration: underline;
}
`
const IconWrapper = styled.div`
flex-shrink: 0;
const TransactionState = styled(ExternalLink)<{ pending: boolean; success?: boolean }>`
display: flex;
justify-content: space-between;
align-items: center;
text-decoration: none !important;
border-radius: 0.5rem;
padding: 0.25rem 0rem;
font-weight: 500;
font-size: 0.825rem;
color: ${({ theme }) => theme.primary1};
`
const IconWrapper = styled.div<{ pending: boolean; success?: boolean }>`
color: ${({ pending, success, theme }) => (pending ? theme.primary1 : success ? theme.green1 : theme.red1)};
`
export default function Transaction({ hash }: { hash: string }) {
@@ -65,9 +49,12 @@ export default function Transaction({ hash }: { hash: string }) {
return (
<TransactionWrapper>
<TransactionState href={getEtherscanLink(chainId, hash, 'transaction')} pending={pending} success={success}>
<TransactionStatusText>{summary ? summary : hash}</TransactionStatusText>
<IconWrapper>
{pending ? <Spinner src={Circle} /> : success ? <Check size="16" /> : <Triangle size="16" />}
<RowFixed>
<TransactionStatusText>{summary ? summary : hash}</TransactionStatusText>
<LinkIcon size={16} />
</RowFixed>
<IconWrapper pending={pending} success={success}>
{pending ? <Loader /> : success ? <CheckCircle size="16" /> : <Triangle size="16" />}
</IconWrapper>
</TransactionState>
</TransactionWrapper>

View File

@@ -2,9 +2,9 @@ import React, { useCallback, useContext } from 'react'
import { useDispatch } from 'react-redux'
import styled, { ThemeContext } from 'styled-components'
import { useActiveWeb3React } from '../../hooks'
import { isMobile } from 'react-device-detect'
import { AppDispatch } from '../../state'
import { clearAllTransactions } from '../../state/transactions/actions'
import { shortenAddress } from '../../utils'
import { AutoRow } from '../Row'
import Copy from './Copy'
import Transaction from './Transaction'
@@ -18,10 +18,9 @@ import WalletConnectIcon from '../../assets/images/walletConnectIcon.svg'
import FortmaticIcon from '../../assets/images/fortmaticIcon.png'
import PortisIcon from '../../assets/images/portisIcon.png'
import Identicon from '../Identicon'
import { ButtonEmpty } from '../Button'
import { Link, TYPE } from '../../theme'
import { ButtonSecondary } from '../Button'
import { ExternalLink as LinkIcon } from 'react-feather'
import { ExternalLink, LinkStyledButton, TYPE } from '../../theme'
const HeaderRow = styled.div`
${({ theme }) => theme.flexRowNoWrap};
@@ -55,31 +54,31 @@ const UpperSection = styled.div`
const InfoCard = styled.div`
padding: 1rem;
background-color: ${({ theme }) => theme.bg2};
border: 1px solid ${({ theme }) => theme.bg3};
border-radius: 20px;
position: relative;
display: grid;
grid-row-gap: 12px;
margin-bottom: 20px;
`
const AccountGroupingRow = styled.div`
${({ theme }) => theme.flexRowNoWrap};
justify-content: space-between;
align-items: center;
font-weight: 500;
font-weight: 400;
color: ${({ theme }) => theme.text1};
div {
${({ theme }) => theme.flexRowNoWrap}
align-items: center;
}
&:first-of-type {
margin-bottom: 8px;
}
`
const AccountSection = styled.div`
background-color: ${({ theme }) => theme.bg1};
padding: 0rem 1rem;
${({ theme }) => theme.mediaWidth.upToMedium`padding: 0rem 1rem 1rem 1rem;`};
${({ theme }) => theme.mediaWidth.upToMedium`padding: 0rem 1rem 1.5rem 1rem;`};
`
const YourAccount = styled.div`
@@ -94,28 +93,6 @@ const YourAccount = styled.div`
}
`
const GreenCircle = styled.div`
${({ theme }) => theme.flexRowNoWrap}
justify-content: center;
align-items: center;
&:first-child {
height: 8px;
width: 8px;
margin-left: 12px;
margin-right: 2px;
background-color: ${({ theme }) => theme.green1};
border-radius: 50%;
}
`
const CircleWrapper = styled.div`
color: ${({ theme }) => theme.green1};
display: flex;
justify-content: center;
align-items: center;
`
const LowerSection = styled.div`
${({ theme }) => theme.flexColumnNoWrap}
padding: 1.5rem;
@@ -132,13 +109,14 @@ const LowerSection = styled.div`
}
`
const AccountControl = styled.div<{ hasENS: boolean; isENS: boolean }>`
${({ theme }) => theme.flexRowNoWrap};
align-items: center;
const AccountControl = styled.div`
display: flex;
justify-content: space-between;
min-width: 0;
width: 100%;
font-weight: ${({ hasENS, isENS }) => (hasENS ? (isENS ? 500 : 400) : 500)};
font-size: ${({ hasENS, isENS }) => (hasENS ? (isENS ? '1rem' : '0.8rem') : '1rem')};
font-weight: 500;
font-size: 1.25rem;
a:hover {
text-decoration: underline;
@@ -146,22 +124,22 @@ const AccountControl = styled.div<{ hasENS: boolean; isENS: boolean }>`
p {
min-width: 0;
margin: 0.5rem 0;
margin: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
`
const ConnectButtonRow = styled.div`
${({ theme }) => theme.flexRowNoWrap}
align-items: center;
justify-content: center;
margin: 10px 0;
`
const StyledLink = styled(Link)<{ hasENS: boolean; isENS: boolean }>`
color: ${({ hasENS, isENS, theme }) => (hasENS ? (isENS ? theme.primary1 : theme.text3) : theme.primary1)};
const AddressLink = styled(ExternalLink)<{ hasENS: boolean; isENS: boolean }>`
font-size: 0.825rem;
color: ${({ theme }) => theme.text3};
margin-left: 1rem;
font-size: 0.825rem;
display: flex;
:hover {
color: ${({ theme }) => theme.text2};
}
`
const CloseIcon = styled.div`
@@ -181,14 +159,17 @@ const CloseColor = styled(Close)`
`
const WalletName = styled.div`
padding-left: 0.5rem;
width: initial;
font-size: 0.825rem;
font-weight: 500;
color: ${({ theme }) => theme.text3};
`
const IconWrapper = styled.div<{ size?: number }>`
${({ theme }) => theme.flexColumnNoWrap};
align-items: center;
justify-content: center;
margin-right: 8px;
& > img,
span {
height: ${({ size }) => (size ? size + 'px' : '32px')};
@@ -203,10 +184,12 @@ const TransactionListWrapper = styled.div`
${({ theme }) => theme.flexColumnNoWrap};
`
const WalletAction = styled.div`
color: ${({ theme }) => theme.text4};
margin-left: 16px;
const WalletAction = styled(ButtonSecondary)`
width: fit-content;
font-weight: 400;
margin-left: 8px;
font-size: 0.825rem;
padding: 4px 6px;
:hover {
cursor: pointer;
text-decoration: underline;
@@ -255,39 +238,39 @@ export default function AccountDetails({
SUPPORTED_WALLETS[k].connector === connector && (connector !== injected || isMetaMask === (k === 'METAMASK'))
)
.map(k => SUPPORTED_WALLETS[k].name)[0]
return <WalletName>{name}</WalletName>
return <WalletName>Connected with {name}</WalletName>
}
function getStatusIcon() {
if (connector === injected) {
return (
<IconWrapper size={16}>
<Identicon /> {formatConnectorName()}
<Identicon />
</IconWrapper>
)
} else if (connector === walletconnect) {
return (
<IconWrapper size={16}>
<img src={WalletConnectIcon} alt={''} /> {formatConnectorName()}
<img src={WalletConnectIcon} alt={''} />
</IconWrapper>
)
} else if (connector === walletlink) {
return (
<IconWrapper size={16}>
<img src={CoinbaseWalletIcon} alt={''} /> {formatConnectorName()}
<img src={CoinbaseWalletIcon} alt={''} />
</IconWrapper>
)
} else if (connector === fortmatic) {
return (
<IconWrapper size={16}>
<img src={FortmaticIcon} alt={''} /> {formatConnectorName()}
<img src={FortmaticIcon} alt={''} />
</IconWrapper>
)
} else if (connector === portis) {
return (
<>
<IconWrapper size={16}>
<img src={PortisIcon} alt={''} /> {formatConnectorName()}
<img src={PortisIcon} alt={''} />
<MainWalletAction
onClick={() => {
portis.portis.showPortis()
@@ -320,10 +303,11 @@ export default function AccountDetails({
<YourAccount>
<InfoCard>
<AccountGroupingRow>
{getStatusIcon()}
{formatConnectorName()}
<div>
{connector !== injected && connector !== walletlink && (
<WalletAction
style={{ fontSize: '.825rem', fontWeight: 400, marginRight: '8px' }}
onClick={() => {
;(connector as any).close()
}}
@@ -331,71 +315,84 @@ export default function AccountDetails({
Disconnect
</WalletAction>
)}
<CircleWrapper>
<GreenCircle>
<div />
</GreenCircle>
</CircleWrapper>
<WalletAction
style={{ fontSize: '.825rem', fontWeight: 400 }}
onClick={() => {
openOptions()
}}
>
Change
</WalletAction>
</div>
</AccountGroupingRow>
<AccountGroupingRow id="web3-account-identifier-row">
{ENSName ? (
<>
<AccountControl hasENS={!!ENSName} isENS={true}>
<p>{ENSName}</p> <Copy toCopy={account} />
</AccountControl>
</>
) : (
<>
<AccountControl hasENS={!!ENSName} isENS={false}>
<p>{account}</p> <Copy toCopy={account} />
</AccountControl>
</>
)}
<AccountControl>
{ENSName ? (
<>
<div>
{getStatusIcon()}
<p> {ENSName}</p>
</div>
</>
) : (
<>
<div>
{getStatusIcon()}
<p> {shortenAddress(account)}</p>
</div>
</>
)}
</AccountControl>
</AccountGroupingRow>
<AccountGroupingRow>
{ENSName ? (
<>
<AccountControl hasENS={!!ENSName} isENS={false}>
<StyledLink hasENS={!!ENSName} isENS={true} href={getEtherscanLink(chainId, ENSName, 'address')}>
View on Etherscan
</StyledLink>
<AccountControl>
<div>
<Copy toCopy={account}>
<span style={{ marginLeft: '4px' }}>Copy Address</span>
</Copy>
<AddressLink
hasENS={!!ENSName}
isENS={true}
href={getEtherscanLink(chainId, ENSName, 'address')}
>
<LinkIcon size={16} />
<span style={{ marginLeft: '4px' }}>View on Etherscan</span>
</AddressLink>
</div>
</AccountControl>
</>
) : (
<>
<AccountControl hasENS={!!ENSName} isENS={false}>
<StyledLink hasENS={!!ENSName} isENS={false} href={getEtherscanLink(chainId, account, 'address')}>
View on Etherscan
</StyledLink>
<AccountControl>
<div>
<Copy toCopy={account}>
<span style={{ marginLeft: '4px' }}>Copy Address</span>
</Copy>
<AddressLink
hasENS={!!ENSName}
isENS={false}
href={getEtherscanLink(chainId, account, 'address')}
>
<LinkIcon size={16} />
<span style={{ marginLeft: '4px' }}>View on Etherscan</span>
</AddressLink>
</div>
</AccountControl>
</>
)}
{/* {formatConnectorName()} */}
</AccountGroupingRow>
</InfoCard>
</YourAccount>
{!(isMobile && (window.web3 || window.ethereum)) && (
<ConnectButtonRow>
<ButtonEmpty
style={{ fontWeight: 400 }}
padding={'12px'}
width={'260px'}
onClick={() => {
openOptions()
}}
>
Connect to a different wallet
</ButtonEmpty>
</ConnectButtonRow>
)}
</AccountSection>
</UpperSection>
{!!pendingTransactions.length || !!confirmedTransactions.length ? (
<LowerSection>
<AutoRow style={{ justifyContent: 'space-between' }}>
<AutoRow mb={'1rem'} style={{ justifyContent: 'space-between' }}>
<TYPE.body>Recent Transactions</TYPE.body>
<Link onClick={clearAllTransactionsCallback}>(clear all)</Link>
<LinkStyledButton onClick={clearAllTransactionsCallback}>(clear all)</LinkStyledButton>
</AutoRow>
{renderTransactions(pendingTransactions)}
{renderTransactions(confirmedTransactions)}

View File

@@ -4,7 +4,7 @@ import useDebounce from '../../hooks/useDebounce'
import { isAddress } from '../../utils'
import { useActiveWeb3React } from '../../hooks'
import { Link, TYPE } from '../../theme'
import { ExternalLink, TYPE } from '../../theme'
import { AutoColumn } from '../Column'
import { RowBetween } from '../Row'
import { getEtherscanLink } from '../../utils'
@@ -160,12 +160,12 @@ export default function AddressInputPanel({
Recipient
</TYPE.black>
{data.address && (
<Link
<ExternalLink
href={getEtherscanLink(chainId, data.name || data.address, 'address')}
style={{ fontSize: '14px' }}
>
(View on Etherscan)
</Link>
</ExternalLink>
)}
</RowBetween>
<Input

View File

@@ -6,7 +6,12 @@ import { RowBetween } from '../Row'
import { ChevronDown } from 'react-feather'
import { Button as RebassButton, ButtonProps } from 'rebass/styled-components'
const Base = styled(RebassButton)<{ padding?: string; width?: string; borderRadius?: string }>`
const Base = styled(RebassButton)<{
padding?: string
width?: string
borderRadius?: string
altDisbaledStyle?: boolean
}>`
padding: ${({ padding }) => (padding ? padding : '18px')};
width: ${({ width }) => (width ? width : '100%')};
font-weight: 500;
@@ -45,10 +50,12 @@ export const ButtonPrimary = styled(Base)`
background-color: ${({ theme }) => darken(0.1, theme.primary1)};
}
&:disabled {
background-color: ${({ theme }) => theme.bg3};
color: ${({ theme }) => theme.text3}
background-color: ${({ theme, altDisbaledStyle }) => (altDisbaledStyle ? theme.primary1 : theme.bg3)};
color: ${({ theme, altDisbaledStyle }) => (altDisbaledStyle ? 'white' : theme.text3)}
cursor: auto;
box-shadow: none;
border: 1px solid transparent;;
outline: none;
}
`
@@ -68,6 +75,16 @@ export const ButtonLight = styled(Base)`
box-shadow: 0 0 0 1pt ${({ theme, disabled }) => !disabled && darken(0.05, theme.primary5)};
background-color: ${({ theme, disabled }) => !disabled && darken(0.05, theme.primary5)};
}
:disabled {
opacity: 0.4;
:hover {
cursor: auto;
background-color: ${({ theme }) => theme.primary5};
box-shadow: none;
border: 1px solid transparent;
outline: none;
}
}
`
export const ButtonGray = styled(Base)`

View File

@@ -1,19 +1,17 @@
import React, { useCallback, useContext } from 'react'
import React, { useContext } from 'react'
import styled, { ThemeContext } from 'styled-components'
import { RouteComponentProps, withRouter } from 'react-router-dom'
import Modal from '../Modal'
import Loader from '../Loader'
import { Link } from '../../theme'
import { ExternalLink } from '../../theme'
import { Text } from 'rebass'
import { CloseIcon } from '../../theme/components'
import { CloseIcon, Spinner } from '../../theme/components'
import { RowBetween } from '../Row'
import { ArrowUpCircle } from 'react-feather'
import { ButtonPrimary } from '../Button'
import { AutoColumn, ColumnCenter } from '../Column'
import Circle from '../../assets/images/blue-loader.svg'
import { useActiveWeb3React } from '../../hooks'
import { getEtherscanLink } from '../../utils'
import { useActiveWeb3React } from '../../hooks'
const Wrapper = styled.div`
width: 100%;
@@ -32,56 +30,41 @@ const ConfirmedIcon = styled(ColumnCenter)`
padding: 60px 0;
`
interface ConfirmationModalProps extends RouteComponentProps<{}> {
const CustomLightSpinner = styled(Spinner)<{ size: string }>`
height: ${({ size }) => size};
width: ${({ size }) => size};
`
interface ConfirmationModalProps {
isOpen: boolean
onDismiss: () => void
hash: string
topContent: () => React.ReactChild
bottomContent: () => React.ReactChild
attemptingTxn: boolean
pendingConfirmation: boolean
pendingText: string
title?: string
}
function ConfirmationModal({
history,
export default function ConfirmationModal({
isOpen,
onDismiss,
hash,
topContent,
bottomContent,
attemptingTxn,
pendingConfirmation,
hash,
pendingText,
title = ''
}: ConfirmationModalProps) {
const { chainId } = useActiveWeb3React()
const theme = useContext(ThemeContext)
const dismissAndReturn = useCallback(() => {
if (history.location.pathname.match('/add') || history.location.pathname.match('/remove')) {
history.push('/pool')
}
onDismiss()
}, [onDismiss, history])
const transactionBroadcast = !!hash
return (
<Modal isOpen={isOpen} onDismiss={onDismiss} maxHeight={90}>
{!attemptingTxn ? (
<Wrapper>
<Section>
<RowBetween>
<Text fontWeight={500} fontSize={20}>
{title}
</Text>
<CloseIcon onClick={onDismiss} />
</RowBetween>
{topContent()}
</Section>
<BottomSection gap="12px">{bottomContent()}</BottomSection>
</Wrapper>
) : (
// waiting for user to confirm/reject tx _or_ showing info on a tx that has been broadcast
if (attemptingTxn || transactionBroadcast) {
return (
<Modal isOpen={isOpen} onDismiss={onDismiss} maxHeight={90}>
<Wrapper>
<Section>
<RowBetween>
@@ -89,37 +72,36 @@ function ConfirmationModal({
<CloseIcon onClick={onDismiss} />
</RowBetween>
<ConfirmedIcon>
{pendingConfirmation ? (
<Loader size="90px" />
) : (
{transactionBroadcast ? (
<ArrowUpCircle strokeWidth={0.5} size={90} color={theme.primary1} />
) : (
<CustomLightSpinner src={Circle} alt="loader" size={'90px'} />
)}
</ConfirmedIcon>
<AutoColumn gap="12px" justify={'center'}>
<Text fontWeight={500} fontSize={20}>
{!pendingConfirmation ? 'Transaction Submitted' : 'Waiting For Confirmation'}
{transactionBroadcast ? 'Transaction Submitted' : 'Waiting For Confirmation'}
</Text>
<AutoColumn gap="12px" justify={'center'}>
<Text fontWeight={600} fontSize={14} color="" textAlign="center">
{pendingText}
</Text>
</AutoColumn>
{!pendingConfirmation && (
{transactionBroadcast ? (
<>
<Link href={getEtherscanLink(chainId, hash, 'transaction')}>
<ExternalLink href={getEtherscanLink(chainId, hash, 'transaction')}>
<Text fontWeight={500} fontSize={14} color={theme.primary1}>
View on Etherscan
</Text>
</Link>
<ButtonPrimary onClick={dismissAndReturn} style={{ margin: '20px 0 0 0' }}>
</ExternalLink>
<ButtonPrimary onClick={onDismiss} style={{ margin: '20px 0 0 0' }}>
<Text fontWeight={500} fontSize={20}>
Close
</Text>
</ButtonPrimary>
</>
)}
{pendingConfirmation && (
) : (
<Text fontSize={12} color="#565A69" textAlign="center">
Confirm this transaction in your wallet
</Text>
@@ -127,9 +109,25 @@ function ConfirmationModal({
</AutoColumn>
</Section>
</Wrapper>
)}
</Modal>
)
}
// confirmation screen
return (
<Modal isOpen={isOpen} onDismiss={onDismiss} maxHeight={90}>
<Wrapper>
<Section>
<RowBetween>
<Text fontWeight={500} fontSize={20}>
{title}
</Text>
<CloseIcon onClick={onDismiss} />
</RowBetween>
{topContent()}
</Section>
<BottomSection gap="12px">{bottomContent()}</BottomSection>
</Wrapper>
</Modal>
)
}
export default withRouter(ConfirmationModal)

View File

@@ -49,7 +49,6 @@ const LabelRow = styled.div`
font-size: 0.75rem;
line-height: 1rem;
padding: 0.75rem 1rem 0 1rem;
height: 20px;
span:hover {
cursor: pointer;
color: ${({ theme }) => darken(0.2, theme.text2)};

View File

@@ -8,7 +8,7 @@ import Row from '../Row'
import Menu from '../Menu'
import Web3Status from '../Web3Status'
import { Link } from '../../theme'
import { ExternalLink, StyledInternalLink } from '../../theme'
import { Text } from 'rebass'
import { WETH, ChainId } from '@uniswap/sdk'
import { isMobile } from 'react-device-detect'
@@ -138,7 +138,8 @@ const VersionLabel = styled.span<{ isV2?: boolean }>`
const VersionToggle = styled.a`
border-radius: 16px;
border: 1px solid ${({ theme }) => theme.primary1};
background: ${({ theme }) => theme.primary5};
border: 1px solid ${({ theme }) => theme.primary4};
color: ${({ theme }) => theme.primary1};
display: flex;
width: fit-content;
@@ -159,13 +160,13 @@ export default function Header() {
<HeaderFrame>
<MigrateBanner>
Uniswap V2 is live! Read the&nbsp;
<Link href="https://uniswap.org/blog/launch-uniswap-v2/">
<ExternalLink href="https://uniswap.org/blog/launch-uniswap-v2/">
<b>blog post </b>
</Link>
</ExternalLink>
&nbsp;or&nbsp;
<Link href="https://migrate.uniswap.exchange/">
<StyledInternalLink to="/migrate/v1">
<b>migrate your liquidity </b>
</Link>
</StyledInternalLink>
.
</MigrateBanner>
<RowBetween padding="1rem">
@@ -204,7 +205,7 @@ export default function Header() {
</TestnetWrapper>
<AccountElement active={!!account} style={{ pointerEvents: 'auto' }}>
{account && userEthBalance ? (
<Text style={{ flexShrink: 0 }} px="0.5rem" fontWeight={500}>
<Text style={{ flexShrink: 0 }} pl="0.75rem" pr="0.5rem" fontWeight={500}>
{userEthBalance?.toSignificant(4)} ETH
</Text>
) : null}

View File

@@ -1,15 +1,38 @@
import React from 'react'
import styled from 'styled-components'
import styled, { keyframes } from 'styled-components'
import { Spinner } from '../../theme'
import Circle from '../../assets/images/blue-loader.svg'
const SpinnerWrapper = styled(Spinner)<{ size: string }>`
height: ${({ size }) => size};
width: ${({ size }) => size};
const rotate = keyframes`
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
`
export default function Loader({ size }: { size: string }) {
return <SpinnerWrapper src={Circle} alt="loader" size={size} />
const StyledSVG = styled.svg<{ size: string; stroke?: string }>`
animation: 2s ${rotate} linear infinite;
height: ${({ size }) => size};
width: ${({ size }) => size};
path {
stroke: ${({ stroke, theme }) => stroke ?? theme.primary1};
}
`
/**
* Takes in custom size and stroke for circle color, default to primary color as fill,
* need ...rest for layered styles on top
*/
export default function Loader({ size = '16px', stroke = null, ...rest }: { size?: string; stroke?: string }) {
return (
<StyledSVG viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" size={size} stroke={stroke} {...rest}>
<path
d="M12 2C6.47715 2 2 6.47715 2 12C2 17.5228 6.47715 22 12 22C17.5228 22 22 17.5228 22 12C22 9.27455 20.9097 6.80375 19.1414 5"
strokeWidth="2.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</StyledSVG>
)
}

View File

@@ -4,7 +4,7 @@ import styled from 'styled-components'
import { ReactComponent as MenuIcon } from '../../assets/images/menu.svg'
import useToggle from '../../hooks/useToggle'
import { Link } from '../../theme'
import { ExternalLink } from '../../theme'
const StyledMenuIcon = styled(MenuIcon)`
path {
@@ -63,7 +63,7 @@ const MenuFlyout = styled.span`
z-index: 100;
`
const MenuItem = styled(Link)`
const MenuItem = styled(ExternalLink)`
flex: 1;
padding: 0.5rem 0.5rem;
color: ${({ theme }) => theme.text2};

View File

@@ -42,8 +42,10 @@ const StyledDialogOverlay = styled(({ mobile, ...rest }) => <AnimatedDialogOverl
// destructure to not pass custom props to Dialog DOM element
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const StyledDialogContent = styled(({ minHeight, maxHeight, mobile, isOpen, ...rest }) => (
<DialogContent aria-label="content" {...rest} />
))`
<DialogContent {...rest} />
)).attrs({
'aria-label': 'dialog'
})`
&[data-reach-dialog-content] {
margin: 0 0 2rem 0;
border: 1px solid ${({ theme }) => theme.bg1};
@@ -170,7 +172,7 @@ export default function Modal({
hidden={true}
minHeight={minHeight}
maxHeight={maxHeight}
mobile={isMobile}
mobile={isMobile ?? undefined}
>
<HiddenCloseButton onClick={onDismiss} />
{children}
@@ -189,13 +191,7 @@ export default function Modal({
{transitions.map(
({ item, key, props }) =>
item && (
<StyledDialogOverlay
key={key}
style={props}
onDismiss={onDismiss}
initialFocusRef={initialFocusRef}
mobile={false}
>
<StyledDialogOverlay key={key} style={props} onDismiss={onDismiss} initialFocusRef={initialFocusRef}>
<StyledDialogContent hidden={true} minHeight={minHeight} maxHeight={maxHeight} isOpen={isOpen}>
<HiddenCloseButton onClick={onDismiss} />
{children}

View File

@@ -1,11 +1,12 @@
import React, { useContext } from 'react'
import { ChainId, Pair, Token } from '@uniswap/sdk'
import React, { useContext, useMemo } from 'react'
import styled, { ThemeContext } from 'styled-components'
import { useMediaLayout } from 'use-media'
import { X } from 'react-feather'
import { PopupContent } from '../../state/application/actions'
import { useActivePopups, useRemovePopup } from '../../state/application/hooks'
import { Link } from '../../theme'
import { ExternalLink } from '../../theme'
import { AutoColumn } from '../Column'
import DoubleTokenLogo from '../DoubleLogo'
import Row from '../Row'
@@ -71,6 +72,40 @@ const Popup = styled.div`
`}
`
function PoolPopup({
token0,
token1
}: {
token0: { address?: string; symbol?: string }
token1: { address?: string; symbol?: string }
}) {
const pairAddress: string | null = useMemo(() => {
if (!token0 || !token1) return null
// just mock it out
return Pair.getAddress(
new Token(ChainId.MAINNET, token0.address, 18),
new Token(ChainId.MAINNET, token1.address, 18)
)
}, [token0, token1])
return (
<AutoColumn gap={'10px'}>
<Text fontSize={20} fontWeight={500}>
Pool Imported
</Text>
<Row>
<DoubleTokenLogo a0={token0?.address ?? ''} a1={token1?.address ?? ''} margin={true} />
<Text fontSize={16} fontWeight={500}>
UNI {token0?.symbol} / {token1?.symbol}
</Text>
</Row>
{pairAddress ? (
<ExternalLink href={`https://uniswap.info/pair/${pairAddress}`}>View on Uniswap Info.</ExternalLink>
) : null}
</AutoColumn>
)
}
function PopupItem({ content, popKey }: { content: PopupContent; popKey: string }) {
if ('txn' in content) {
const {
@@ -81,20 +116,8 @@ function PopupItem({ content, popKey }: { content: PopupContent; popKey: string
const {
poolAdded: { token0, token1 }
} = content
return (
<AutoColumn gap={'10px'}>
<Text fontSize={20} fontWeight={500}>
Pool Imported
</Text>
<Row>
<DoubleTokenLogo a0={token0?.address ?? ''} a1={token1?.address ?? ''} margin={true} />
<Text fontSize={16} fontWeight={500}>
UNI {token0?.symbol} / {token1?.symbol}
</Text>
</Row>
<Link>View on Uniswap Info.</Link>
</AutoColumn>
)
return <PoolPopup token0={token0} token1={token1} />
}
}

View File

@@ -12,7 +12,7 @@ import Card, { GreyCard } from '../Card'
import TokenLogo from '../TokenLogo'
import DoubleLogo from '../DoubleLogo'
import { Text } from 'rebass'
import { Link } from '../../theme/components'
import { ExternalLink } from '../../theme/components'
import { AutoColumn } from '../Column'
import { ChevronDown, ChevronUp } from 'react-feather'
import { ButtonSecondary } from '../Button'
@@ -204,7 +204,9 @@ function PositionCard({ pair, history, border, minimal = false }: PositionCardPr
)}
<AutoRow justify="center" marginTop={'10px'}>
<Link href={`https://uniswap.info/pair/${pair?.liquidityToken.address}`}>View pool information </Link>
<ExternalLink href={`https://uniswap.info/pair/${pair?.liquidityToken.address}`}>
View pool information
</ExternalLink>
</AutoRow>
<RowBetween marginTop="10px">
<ButtonSecondary

View File

@@ -1,6 +1,8 @@
import React from 'react'
import { Text } from 'rebass'
import { COMMON_BASES } from '../../constants'
import { Token } from '@uniswap/sdk'
import { SUGGESTED_BASES } from '../../constants'
import { AutoColumn } from '../Column'
import QuestionHelper from '../QuestionHelper'
import { AutoRow } from '../Row'
@@ -25,7 +27,7 @@ export default function CommonBases({
<QuestionHelper text="These tokens are commonly used in pairs." />
</AutoRow>
<AutoRow gap="10px">
{COMMON_BASES[chainId]?.map(token => {
{(SUGGESTED_BASES[chainId] ?? []).map((token: Token) => {
return (
<BaseWrapper
gap="6px"

View File

@@ -4,16 +4,16 @@ import { useTranslation } from 'react-i18next'
import { FixedSizeList } from 'react-window'
import { Text } from 'rebass'
import { ThemeContext } from 'styled-components'
import Circle from '../../assets/images/circle.svg'
import { ALL_TOKENS } from '../../constants/tokens'
import { useActiveWeb3React } from '../../hooks'
import { Link as StyledLink, TYPE } from '../../theme'
import { LinkStyledButton, TYPE } from '../../theme'
import { isAddress } from '../../utils'
import { ButtonSecondary } from '../Button'
import Column, { AutoColumn } from '../Column'
import { RowFixed } from '../Row'
import TokenLogo from '../TokenLogo'
import { FadedSpan, GreySpan, MenuItem, SpinnerWrapper, ModalInfo } from './styleds'
import { FadedSpan, GreySpan, MenuItem, ModalInfo } from './styleds'
import Loader from '../Loader'
function isDefaultToken(tokenAddress: string, chainId?: number): boolean {
const address = isAddress(tokenAddress)
@@ -28,7 +28,8 @@ export default function TokenList({
otherToken,
showSendWithSwap,
onRemoveAddedToken,
otherSelectedText
otherSelectedText,
hideRemove
}: {
tokens: Token[]
selectedToken: string
@@ -38,6 +39,7 @@ export default function TokenList({
otherToken: string
showSendWithSwap?: boolean
otherSelectedText: string
hideRemove?: boolean
}) {
const { t } = useTranslation()
const { account, chainId } = useActiveWeb3React()
@@ -80,15 +82,16 @@ export default function TokenList({
</Text>
<FadedSpan>
<TYPE.main fontWeight={500}>{customAdded && 'Added by user'}</TYPE.main>
{customAdded && (
<div
{customAdded && !hideRemove && (
<LinkStyledButton
onClick={event => {
event.stopPropagation()
onRemoveAddedToken(chainId, address)
}}
style={{ marginLeft: '4px', fontWeight: 400 }}
>
<StyledLink style={{ marginLeft: '4px', fontWeight: 400 }}>(Remove)</StyledLink>
</div>
(Remove)
</LinkStyledButton>
)}
</FadedSpan>
</Column>
@@ -109,7 +112,7 @@ export default function TokenList({
)}
</Text>
) : account ? (
<SpinnerWrapper src={Circle} alt="loader" />
<Loader />
) : (
'-'
)}

View File

@@ -10,12 +10,22 @@ export function filterTokens(tokens: Token[], search: string): Token[] {
return tokens.filter(token => token.address === searchingAddress)
}
const lowerSearchParts = searchingAddress ? [] : search.toLowerCase().split(/\s+/)
const lowerSearchParts = search
.toLowerCase()
.split(/\s+/)
.filter(s => s.length > 0)
if (lowerSearchParts.length === 0) {
return tokens
}
const matchesSearch = (s: string): boolean => {
const sParts = s.toLowerCase().split(/\s+/)
const sParts = s
.toLowerCase()
.split(/\s+/)
.filter(s => s.length > 0)
return lowerSearchParts.every(p => p.length === 0 || sParts.some(sp => sp.startsWith(p)))
return lowerSearchParts.every(p => p.length === 0 || sParts.some(sp => sp.startsWith(p) || sp.endsWith(p)))
}
return tokens.filter(token => {

View File

@@ -10,7 +10,7 @@ import { useActiveWeb3React } from '../../hooks'
import { useAllTokens, useTokenByAddressAndAutomaticallyAdd } from '../../hooks/Tokens'
import { useAllDummyPairs, useRemoveUserAddedToken } from '../../state/user/hooks'
import { useAllTokenBalancesTreatingWETHasETH, useTokenBalances } from '../../state/wallet/hooks'
import { CloseIcon, Link as StyledLink } from '../../theme/components'
import { CloseIcon, LinkStyledButton, StyledInternalLink } from '../../theme/components'
import { isAddress } from '../../utils'
import Column from '../Column'
import Modal from '../Modal'
@@ -20,7 +20,7 @@ import Tooltip from '../Tooltip'
import CommonBases from './CommonBases'
import { filterPairs, filterTokens } from './filtering'
import PairList from './PairList'
import { balanceComparator, useTokenComparator } from './sorting'
import { useTokenComparator, pairComparator } from './sorting'
import { PaddedColumn, SearchInput } from './styleds'
import TokenList from './TokenList'
import SortButton from './SortButton'
@@ -74,15 +74,26 @@ function SearchModal({
const tokenComparator = useTokenComparator(invertSearchOrder)
const sortedTokens: Token[] = useMemo(() => {
if (!isTokenView) return []
return Object.values(allTokens).sort(tokenComparator)
}, [allTokens, isTokenView, tokenComparator])
const filteredTokens: Token[] = useMemo(() => {
if (!isTokenView) return []
return filterTokens(sortedTokens, searchQuery)
}, [isTokenView, sortedTokens, searchQuery])
return filterTokens(Object.values(allTokens), searchQuery)
}, [isTokenView, allTokens, searchQuery])
const filteredSortedTokens: Token[] = useMemo(() => {
if (!isTokenView) return []
const sorted = filteredTokens.sort(tokenComparator)
const symbolMatch = searchQuery
.toLowerCase()
.split(/\s+/)
.filter(s => s.length > 0)
if (symbolMatch.length > 1) return sorted
return [
// sort any exact symbol matches first
...sorted.filter(token => token.symbol.toLowerCase() === symbolMatch[0]),
...sorted.filter(token => token.symbol.toLowerCase() !== symbolMatch[0])
]
}, [filteredTokens, isTokenView, searchQuery, tokenComparator])
function _onTokenSelect(address: string) {
onTokenSelect(address)
@@ -105,11 +116,9 @@ function SearchModal({
const sortedPairList = useMemo(() => {
if (isTokenView) return []
return allPairs.sort((a, b): number => {
// sort by balance
const balanceA = allPairBalances[a.liquidityToken.address]
const balanceB = allPairBalances[b.liquidityToken.address]
return balanceComparator(balanceA, balanceB)
return pairComparator(a, b, balanceA, balanceB)
})
}, [isTokenView, allPairs, allPairBalances])
@@ -159,7 +168,7 @@ function SearchModal({
placement="bottom"
>
<SearchInput
type={'text'}
type="text"
id="token-search-input"
placeholder={t('tokenSearchPlaceholder')}
value={searchQuery}
@@ -183,7 +192,7 @@ function SearchModal({
<div style={{ width: '100%', height: '1px', backgroundColor: theme.bg2 }} />
{isTokenView ? (
<TokenList
tokens={filteredTokens}
tokens={filteredSortedTokens}
allTokenBalances={allTokenBalances}
onRemoveAddedToken={removeTokenByAddress}
onTokenSelect={_onTokenSelect}
@@ -191,6 +200,7 @@ function SearchModal({
otherToken={otherSelectedTokenAddress}
selectedToken={hiddenToken}
showSendWithSwap={showSendWithSwap}
hideRemove={Boolean(isAddress(searchQuery))}
/>
) : (
<PairList
@@ -206,19 +216,13 @@ function SearchModal({
<AutoRow justify={'center'}>
<div>
{isTokenView ? (
<Text fontWeight={500} color={theme.text2} fontSize={14}>
<StyledLink onClick={openTooltip}>Having trouble finding a token?</StyledLink>
</Text>
<LinkStyledButton style={{ fontWeight: 500, color: theme.text2, fontSize: 16 }} onClick={openTooltip}>
Having trouble finding a token?
</LinkStyledButton>
) : (
<Text fontWeight={500}>
{!isMobile && "Don't see a pool? "}
<StyledLink
onClick={() => {
history.push('/find')
}}
>
{!isMobile ? 'Import it.' : 'Import pool.'}
</StyledLink>
<StyledInternalLink to="/find">{!isMobile ? 'Import it.' : 'Import pool.'}</StyledInternalLink>
</Text>
)}
</div>

View File

@@ -1,10 +1,11 @@
import { Token, TokenAmount, WETH } from '@uniswap/sdk'
import { Token, TokenAmount, WETH, Pair } from '@uniswap/sdk'
import { useMemo } from 'react'
import { useActiveWeb3React } from '../../hooks'
import { useAllTokenBalancesTreatingWETHasETH } from '../../state/wallet/hooks'
import { DUMMY_PAIRS_TO_PIN } from '../../constants'
// compare two token amounts with highest one coming first
export function balanceComparator(balanceA?: TokenAmount, balanceB?: TokenAmount) {
function balanceComparator(balanceA?: TokenAmount, balanceB?: TokenAmount) {
if (balanceA && balanceB) {
return balanceA.greaterThan(balanceB) ? -1 : balanceA.equalTo(balanceB) ? 0 : 1
} else if (balanceA && balanceA.greaterThan('0')) {
@@ -15,6 +16,26 @@ export function balanceComparator(balanceA?: TokenAmount, balanceB?: TokenAmount
return 0
}
// compare two pairs, favoring "pinned" pairs, and falling back to balances
export function pairComparator(pairA: Pair, pairB: Pair, balanceA?: TokenAmount, balanceB?: TokenAmount) {
const aShouldBePinned =
DUMMY_PAIRS_TO_PIN[pairA?.token0?.chainId]?.some(
dummyPairToPin => dummyPairToPin.liquidityToken.address === pairA?.liquidityToken?.address
) ?? false
const bShouldBePinned =
DUMMY_PAIRS_TO_PIN[pairB?.token0?.chainId]?.some(
dummyPairToPin => dummyPairToPin.liquidityToken.address === pairB?.liquidityToken?.address
) ?? false
if (aShouldBePinned && !bShouldBePinned) {
return -1
} else if (!aShouldBePinned && bShouldBePinned) {
return 1
} else {
return balanceComparator(balanceA, balanceB)
}
}
function getTokenComparator(
weth: Token | undefined,
balances: { [tokenAddress: string]: TokenAmount }

View File

@@ -1,5 +1,4 @@
import styled from 'styled-components'
import { Spinner } from '../../theme'
import { AutoColumn } from '../Column'
import { AutoRow, RowBetween, RowFixed } from '../Row'
@@ -23,12 +22,6 @@ export const GreySpan = styled.span`
font-weight: 400;
`
export const SpinnerWrapper = styled(Spinner)`
margin: 0 0.25rem 0 0.25rem;
color: ${({ theme }) => theme.text4};
opacity: 0.6;
`
export const Input = styled.input`
position: relative;
display: flex;

View File

@@ -1,52 +1,103 @@
import React from 'react'
import Slider from '@material-ui/core/Slider'
import { withStyles } from '@material-ui/core/styles'
import React, { useCallback } from 'react'
import styled from 'styled-components'
const StyledSlider = withStyles({
root: {
width: '90%',
color: '#565A69',
height: 4,
marginLeft: '15px',
marginRight: '15px',
padding: '15px 0'
},
thumb: {
height: 28,
width: 28,
backgroundColor: '#565A69',
marginTop: -14,
marginLeft: -14,
'&:focus,&:hover,&$active': {
boxShadow:
'0px 0px 1px rgba(0, 0, 0, 0.04), 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.04)',
// Reset on touch devices, it doesn't add specificity
'@media (hover: none)': {}
}
},
active: {},
track: {
height: 4
},
rail: {
height: 2,
opacity: 0.5,
backgroundColor: '#C3C5CB'
},
mark: {
backgroundColor: '#C3C5CB',
height: 12,
width: 2,
marginTop: -4
},
markActive: {
opacity: 1,
backgroundColor: 'currentColor',
height: 12,
width: 2,
marginTop: -4
const StyledRangeInput = styled.input<{ value: number }>`
-webkit-appearance: none; /* Hides the slider so that custom slider can be made */
width: 100%; /* Specific width is required for Firefox. */
background: transparent; /* Otherwise white in Chrome */
cursor: pointer;
&:focus {
outline: none;
}
})(Slider)
&::-moz-focus-outer {
border: 0;
}
&::-webkit-slider-thumb {
-webkit-appearance: none;
height: 28px;
width: 28px;
background-color: #565a69;
border-radius: 100%;
border: none;
transform: translateY(-50%);
color: ${({ theme }) => theme.bg1};
&:hover,
&:focus {
box-shadow: 0px 0px 1px rgba(0, 0, 0, 0.1), 0px 4px 8px rgba(0, 0, 0, 0.08), 0px 16px 24px rgba(0, 0, 0, 0.06),
0px 24px 32px rgba(0, 0, 0, 0.04);
}
}
&::-moz-range-thumb {
height: 28px;
width: 28px;
background-color: #565a69;
border-radius: 100%;
border: none;
color: ${({ theme }) => theme.bg1};
&:hover,
&:focus {
box-shadow: 0px 0px 1px rgba(0, 0, 0, 0.1), 0px 4px 8px rgba(0, 0, 0, 0.08), 0px 16px 24px rgba(0, 0, 0, 0.06),
0px 24px 32px rgba(0, 0, 0, 0.04);
}
}
&::-ms-thumb {
height: 28px;
width: 28px;
background-color: #565a69;
border-radius: 100%;
color: ${({ theme }) => theme.bg1};
&:hover,
&:focus {
box-shadow: 0px 0px 1px rgba(0, 0, 0, 0.1), 0px 4px 8px rgba(0, 0, 0, 0.08), 0px 16px 24px rgba(0, 0, 0, 0.06),
0px 24px 32px rgba(0, 0, 0, 0.04);
}
}
&::-webkit-slider-runnable-track {
background: linear-gradient(
90deg,
${({ theme }) => theme.bg5},
${({ theme }) => theme.bg5} ${({ value }) => value}%,
${({ theme }) => theme.bg3} ${({ value }) => value}%,
${({ theme }) => theme.bg3}
);
height: 2px;
}
&::-moz-range-track {
background: linear-gradient(
90deg,
${({ theme }) => theme.bg5},
${({ theme }) => theme.bg5} ${({ value }) => value}%,
${({ theme }) => theme.bg3} ${({ value }) => value}%,
${({ theme }) => theme.bg3}
);
height: 2px;
}
&::-ms-track {
width: 100%;
border-color: transparent;
color: transparent;
background: ${({ theme }) => theme.bg5};
height: 2px;
}
&::-ms-fill-lower {
background: ${({ theme }) => theme.bg5};
}
&::-ms-fill-upper {
background: ${({ theme }) => theme.bg3};
}
`
interface InputSliderProps {
value: number
@@ -54,8 +105,23 @@ interface InputSliderProps {
}
export default function InputSlider({ value, onChange }: InputSliderProps) {
function wrappedOnChange(_, value) {
onChange(value)
}
return <StyledSlider value={value} onChange={wrappedOnChange} aria-labelledby="input-slider" step={1} />
const changeCallback = useCallback(
e => {
onChange(e.target.value)
},
[onChange]
)
return (
<StyledRangeInput
type="range"
value={value}
style={{ width: '90%', marginLeft: 15, marginRight: 15, padding: '15px 0' }}
onChange={changeCallback}
aria-labelledby="input-slider"
step={1}
min={0}
max={100}
/>
)
}

View File

@@ -224,7 +224,7 @@ export default function SlippageTabs({ rawSlippage, setRawSlippage, deadline, se
<AutoColumn gap="sm">
<RowFixed padding={'0 20px'}>
<TYPE.black fontSize={14} color={theme.text2}>
<TYPE.black fontSize={14} fontWeight={400} color={theme.text2}>
Deadline
</TYPE.black>
<QuestionHelper text="Your transaction will revert if it is pending for more than this long." />

View File

@@ -4,7 +4,7 @@ import { isAddress } from '../../utils'
import { useActiveWeb3React } from '../../hooks'
import { WETH } from '@uniswap/sdk'
import { ReactComponent as EthereumLogo } from '../../assets/images/ethereum-logo.svg'
import EthereumLogo from '../../assets/images/ethereum-logo.png'
const TOKEN_ICON_API = address =>
`https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/${address}/logo.png`
@@ -28,7 +28,7 @@ const Emoji = styled.span<{ size?: string }>`
margin-bottom: -4px;
`
const StyledEthereumLogo = styled(EthereumLogo)<{ size: string }>`
const StyledEthereumLogo = styled.img<{ size: string }>`
width: ${({ size }) => size};
height: ${({ size }) => size};
box-shadow: 0px 6px 10px rgba(0, 0, 0, 0.075);
@@ -55,7 +55,7 @@ export default function TokenLogo({
let path = ''
// hard code to show ETH instead of WETH in UI
if (address === WETH[chainId].address) {
return <StyledEthereumLogo size={size} {...rest} />
return <StyledEthereumLogo src={EthereumLogo} size={size} {...rest} />
} else if (!error && !BAD_IMAGES[address] && isAddress(address)) {
path = TOKEN_ICON_API(address)
} else {

View File

@@ -8,7 +8,7 @@ import { useActiveWeb3React } from '../../hooks'
import { useAllTokens } from '../../hooks/Tokens'
import { Field } from '../../state/swap/actions'
import { useTokenWarningDismissal } from '../../state/user/hooks'
import { Link, TYPE } from '../../theme'
import { ExternalLink, TYPE } from '../../theme'
import { getEtherscanLink } from '../../utils'
import PropsOfExcluding from '../../utils/props-of-excluding'
import QuestionHelper from '../QuestionHelper'
@@ -18,11 +18,11 @@ const Wrapper = styled.div<{ error: boolean }>`
background: ${({ theme, error }) => transparentize(0.9, error ? theme.red1 : theme.yellow1)};
position: relative;
padding: 1rem;
border: 0.5px solid ${({ theme, error }) => transparentize(0.4, error ? theme.red1 : theme.yellow1)};
/* border: 0.5px solid ${({ theme, error }) => transparentize(0.4, error ? theme.red1 : theme.yellow1)}; */
border-radius: 10px;
margin-bottom: 20px;
display: grid;
grid-template-rows: auto auto auto;
grid-template-rows: 14px auto auto;
grid-row-gap: 14px;
`
@@ -42,15 +42,15 @@ const CloseColor = styled(Close)`
const CloseIcon = styled.div`
position: absolute;
right: 1rem;
top: 14px;
top: 12px;
&:hover {
cursor: pointer;
opacity: 0.6;
}
& > * {
height: 14px;
width: 14px;
height: 16px;
width: 16px;
}
`
@@ -111,9 +111,9 @@ export default function TokenWarningCard({ token, ...rest }: TokenWarningCardPro
? `${token.name} (${token.symbol})`
: token.name || token.symbol}
</div>
<Link style={{ fontWeight: 400 }} href={getEtherscanLink(chainId, token.address, 'address')}>
<ExternalLink style={{ fontWeight: 400 }} href={getEtherscanLink(chainId, token.address, 'address')}>
(View on Etherscan)
</Link>
</ExternalLink>
</Row>
<Row>
<TYPE.italic>Verify this is the correct token before making any transactions.</TYPE.italic>

View File

@@ -8,7 +8,7 @@ import useInterval from '../../hooks/useInterval'
import { useRemovePopup } from '../../state/application/hooks'
import { TYPE } from '../../theme'
import { Link } from '../../theme/components'
import { ExternalLink } from '../../theme/components'
import { getEtherscanLink } from '../../utils'
import { AutoColumn } from '../Column'
import { AutoRow } from '../Row'
@@ -62,7 +62,7 @@ export default function TxnPopup({
<TYPE.body fontWeight={500}>
{summary ? summary : 'Hash: ' + hash.slice(0, 8) + '...' + hash.slice(58, 65)}
</TYPE.body>
<Link href={getEtherscanLink(chainId, hash, 'transaction')}>View on Etherscan</Link>
<ExternalLink href={getEtherscanLink(chainId, hash, 'transaction')}>View on Etherscan</ExternalLink>
</AutoColumn>
<Fader count={count} />
</AutoRow>

View File

@@ -1,6 +1,6 @@
import React from 'react'
import styled from 'styled-components'
import { Link } from '../../theme'
import { ExternalLink } from '../../theme'
const InfoCard = styled.button<{ active?: boolean }>`
background-color: ${({ theme, active }) => (active ? theme.bg3 : theme.bg2)};
@@ -134,7 +134,7 @@ export default function Option({
</OptionCardClickable>
)
if (link) {
return <Link href={link}>{content}</Link>
return <ExternalLink href={link}>{content}</ExternalLink>
}
return content

View File

@@ -5,9 +5,8 @@ import Option from './Option'
import { SUPPORTED_WALLETS } from '../../constants'
import WalletConnectData from './WalletConnectData'
import { walletconnect, injected } from '../../connectors'
import { Spinner } from '../../theme'
import Circle from '../../assets/images/circle.svg'
import { darken } from 'polished'
import Loader from '../Loader'
const PendingSection = styled.div`
${({ theme }) => theme.flexColumnNoWrap};
@@ -19,14 +18,8 @@ const PendingSection = styled.div`
}
`
const SpinnerWrapper = styled(Spinner)`
font-size: 4rem;
const StyledLoader = styled(Loader)`
margin-right: 1rem;
svg {
path {
color: ${({ theme }) => theme.bg4};
}
}
`
const LoadingMessage = styled.div<{ error?: boolean }>`
@@ -93,7 +86,7 @@ export default function PendingView({
{!error && connector === walletconnect && <WalletConnectData size={size} uri={uri} />}
<LoadingMessage error={error}>
<LoadingWrapper>
{!error && <SpinnerWrapper src={Circle} />}
{!error && <StyledLoader />}
{error ? (
<ErrorGroup>
<div>Error connecting.</div>

View File

@@ -12,7 +12,7 @@ import AccountDetails from '../AccountDetails'
import PendingView from './PendingView'
import Option from './Option'
import { SUPPORTED_WALLETS } from '../../constants'
import { Link } from '../../theme'
import { ExternalLink } from '../../theme'
import MetamaskIcon from '../../assets/images/metamask.png'
import { ReactComponent as Close } from '../../assets/images/x.svg'
import { injected, walletconnect, fortmatic, portis } from '../../connectors'
@@ -358,9 +358,9 @@ export default function WalletModal({
{walletView !== WALLET_VIEWS.PENDING && (
<Blurb>
<span>New to Ethereum? &nbsp;</span>{' '}
<Link href="https://ethereum.org/use/#3-what-is-a-wallet-and-which-one-should-i-use">
<ExternalLink href="https://ethereum.org/use/#3-what-is-a-wallet-and-which-one-should-i-use">
Learn more about wallets
</Link>
</ExternalLink>
</Blurb>
)}
</ContentWrapper>

View File

@@ -5,9 +5,8 @@ import { useTranslation } from 'react-i18next'
import { network } from '../../connectors'
import { useEagerConnect, useInactiveListener } from '../../hooks'
import { Spinner } from '../../theme'
import Circle from '../../assets/images/circle.svg'
import { NetworkContextName } from '../../constants'
import Loader from '../Loader'
const MessageWrapper = styled.div`
display: flex;
@@ -20,16 +19,6 @@ const Message = styled.h2`
color: ${({ theme }) => theme.secondary1};
`
const SpinnerWrapper = styled(Spinner)`
font-size: 4rem;
svg {
path {
color: ${({ theme }) => theme.secondary1};
}
}
`
export default function Web3ReactManager({ children }) {
const { t } = useTranslation()
const { active } = useWeb3React()
@@ -78,7 +67,7 @@ export default function Web3ReactManager({ children }) {
if (!active && !networkActive) {
return showLoader ? (
<MessageWrapper>
<SpinnerWrapper src={Circle} />
<Loader />
</MessageWrapper>
) : null
}

View File

@@ -16,18 +16,12 @@ import FortmaticIcon from '../../assets/images/fortmaticIcon.png'
import WalletConnectIcon from '../../assets/images/walletConnectIcon.svg'
import CoinbaseWalletIcon from '../../assets/images/coinbaseWalletIcon.svg'
import { Spinner } from '../../theme'
import LightCircle from '../../assets/svg/lightcircle.svg'
import { RowBetween } from '../Row'
import { shortenAddress } from '../../utils'
import { useAllTransactions } from '../../state/transactions/hooks'
import { NetworkContextName } from '../../constants'
import { injected, walletconnect, walletlink, fortmatic, portis } from '../../connectors'
const SpinnerWrapper = styled(Spinner)`
margin: 0 0.25rem 0 0.25rem;
`
import Loader from '../Loader'
const IconWrapper = styled.div<{ size?: number }>`
${({ theme }) => theme.flexColumnNoWrap};
@@ -189,7 +183,7 @@ export default function Web3Status() {
<Web3StatusConnected id="web3-status-connected" onClick={toggleWalletModal} pending={hasPendingTransactions}>
{hasPendingTransactions ? (
<RowBetween>
<Text>{pending?.length} Pending</Text> <SpinnerWrapper src={LightCircle} alt="loader" />
<Text>{pending?.length} Pending</Text> <Loader stroke="white" />
</RowBetween>
) : (
<Text>{ENSName || shortenAddress(account)}</Text>

View File

@@ -37,6 +37,11 @@ export default function SwapModalFooter({
confirmText: string
}) {
const theme = useContext(ThemeContext)
if (!trade) {
return null
}
return (
<>
<AutoColumn gap="0px">

View File

@@ -11,29 +11,30 @@ import TokenLogo from '../TokenLogo'
import { TruncatedText } from './styleds'
export default function SwapModalHeader({
formattedAmounts,
tokens,
formattedAmounts,
slippageAdjustedAmounts,
priceImpactSeverity,
independentField
}: {
formattedAmounts?: { [field in Field]?: string }
tokens?: { [field in Field]?: Token }
slippageAdjustedAmounts?: { [field in Field]?: TokenAmount }
tokens: { [field in Field]?: Token }
formattedAmounts: { [field in Field]?: string }
slippageAdjustedAmounts: { [field in Field]?: TokenAmount }
priceImpactSeverity: number
independentField: Field
}) {
const theme = useContext(ThemeContext)
return (
<AutoColumn gap={'md'} style={{ marginTop: '20px' }}>
<RowBetween align="flex-end">
<TruncatedText fontSize={24} fontWeight={500}>
{!!formattedAmounts[Field.INPUT] && formattedAmounts[Field.INPUT]}
{formattedAmounts[Field.INPUT]}
</TruncatedText>
<RowFixed gap="4px">
<TokenLogo address={tokens[Field.INPUT]?.address} size={'24px'} />
<Text fontSize={24} fontWeight={500} style={{ marginLeft: '10px' }}>
{tokens[Field.INPUT]?.symbol || ''}
{tokens[Field.INPUT]?.symbol}
</Text>
</RowFixed>
</RowBetween>
@@ -42,12 +43,12 @@ export default function SwapModalHeader({
</RowFixed>
<RowBetween align="flex-end">
<TruncatedText fontSize={24} fontWeight={500} color={priceImpactSeverity > 2 ? theme.red1 : ''}>
{!!formattedAmounts[Field.OUTPUT] && formattedAmounts[Field.OUTPUT]}
{formattedAmounts[Field.OUTPUT]}
</TruncatedText>
<RowFixed gap="4px">
<TokenLogo address={tokens[Field.OUTPUT]?.address} size={'24px'} />
<Text fontSize={24} fontWeight={500} style={{ marginLeft: '10px' }}>
{tokens[Field.OUTPUT]?.symbol || ''}
{tokens[Field.OUTPUT]?.symbol}
</Text>
</RowFixed>
</RowBetween>
@@ -56,7 +57,7 @@ export default function SwapModalHeader({
<TYPE.italic textAlign="left" style={{ width: '100%' }}>
{`Output is estimated. You will receive at least `}
<b>
{slippageAdjustedAmounts[Field.OUTPUT]?.toSignificant(6)} {tokens[Field.OUTPUT]?.symbol}{' '}
{slippageAdjustedAmounts[Field.OUTPUT]?.toSignificant(6)} {tokens[Field.OUTPUT]?.symbol}
</b>
{' or the transaction will revert.'}
</TYPE.italic>

View File

@@ -2,7 +2,7 @@ import { TokenAmount } from '@uniswap/sdk'
import React from 'react'
import { Text } from 'rebass'
import { useActiveWeb3React } from '../../hooks'
import { Link, TYPE } from '../../theme'
import { ExternalLink, TYPE } from '../../theme'
import { getEtherscanLink } from '../../utils'
import Copy from '../AccountDetails/Copy'
import { AutoColumn } from '../Column'
@@ -32,21 +32,21 @@ export function TransferModalHeader({
<AutoColumn gap="lg">
<TYPE.blue fontSize={36}>{ENSName}</TYPE.blue>
<AutoRow gap="10px">
<Link href={getEtherscanLink(chainId, ENSName, 'address')}>
<ExternalLink href={getEtherscanLink(chainId, ENSName, 'address')}>
<TYPE.blue fontSize={18}>
{recipient?.slice(0, 8)}...{recipient?.slice(34, 42)}
</TYPE.blue>
</Link>
</ExternalLink>
<Copy toCopy={recipient} />
</AutoRow>
</AutoColumn>
) : (
<AutoRow gap="10px">
<Link href={getEtherscanLink(chainId, recipient, 'address')}>
<ExternalLink href={getEtherscanLink(chainId, recipient, 'address')}>
<TYPE.blue fontSize={36}>
{recipient?.slice(0, 6)}...{recipient?.slice(36, 42)}
</TYPE.blue>
</Link>
</ExternalLink>
<Copy toCopy={recipient} />
</AutoRow>
)}

View File

@@ -2,7 +2,7 @@ import React, { useContext } from 'react'
import { Text } from 'rebass'
import { ThemeContext } from 'styled-components'
import { Link } from '../../theme'
import { ExternalLink } from '../../theme'
import { YellowCard } from '../Card'
import { AutoColumn } from '../Column'
@@ -12,10 +12,10 @@ export default function V1TradeLink({ v1TradeLinkIfBetter }: { v1TradeLinkIfBett
<YellowCard style={{ marginTop: '12px', padding: '8px 4px' }}>
<AutoColumn gap="sm" justify="center" style={{ alignItems: 'center', textAlign: 'center' }}>
<Text lineHeight="145.23%;" fontSize={14} fontWeight={400} color={theme.text1}>
There is a better price for this trade on
<Link href={v1TradeLinkIfBetter}>
<b> Uniswap V1 </b>
</Link>
There is a better price for this trade on{' '}
<ExternalLink href={v1TradeLinkIfBetter}>
<b>Uniswap V1 </b>
</ExternalLink>
</Text>
</AutoColumn>
</YellowCard>

View File

@@ -1,6 +1,10 @@
import { Interface } from '@ethersproject/abi'
import ERC20_ABI from './erc20.json'
import ERC20_BYTES32_ABI from './erc20_bytes32.json'
const ERC20_INTERFACE = new Interface(ERC20_ABI)
const ERC20_BYTES32_INTERFACE = new Interface(ERC20_BYTES32_ABI)
export default ERC20_INTERFACE
export { ERC20_ABI, ERC20_BYTES32_INTERFACE, ERC20_BYTES32_ABI }

View File

@@ -3,56 +3,12 @@
"constant": true,
"inputs": [],
"name": "name",
"outputs": [{ "name": "", "type": "bytes32" }],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": false,
"inputs": [{ "name": "_spender", "type": "address" }, { "name": "_value", "type": "uint256" }],
"name": "approve",
"outputs": [{ "name": "", "type": "bool" }],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "totalSupply",
"outputs": [{ "name": "", "type": "uint256" }],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": false,
"inputs": [
{ "name": "_from", "type": "address" },
{ "name": "_to", "type": "address" },
{ "name": "_value", "type": "uint256" }
"outputs": [
{
"name": "",
"type": "bytes32"
}
],
"name": "transferFrom",
"outputs": [{ "name": "", "type": "bool" }],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "decimals",
"outputs": [{ "name": "", "type": "uint8" }],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [{ "name": "_owner", "type": "address" }],
"name": "balanceOf",
"outputs": [{ "name": "balance", "type": "uint256" }],
"payable": false,
"stateMutability": "view",
"type": "function"
@@ -61,48 +17,14 @@
"constant": true,
"inputs": [],
"name": "symbol",
"outputs": [{ "name": "", "type": "bytes32" }],
"outputs": [
{
"name": "",
"type": "bytes32"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": false,
"inputs": [{ "name": "_to", "type": "address" }, { "name": "_value", "type": "uint256" }],
"name": "transfer",
"outputs": [{ "name": "", "type": "bool" }],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": true,
"inputs": [{ "name": "_owner", "type": "address" }, { "name": "_spender", "type": "address" }],
"name": "allowance",
"outputs": [{ "name": "", "type": "uint256" }],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{ "payable": true, "stateMutability": "payable", "type": "fallback" },
{
"anonymous": false,
"inputs": [
{ "indexed": true, "name": "owner", "type": "address" },
{ "indexed": true, "name": "spender", "type": "address" },
{ "indexed": false, "name": "value", "type": "uint256" }
],
"name": "Approval",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{ "indexed": true, "name": "from", "type": "address" },
{ "indexed": true, "name": "to", "type": "address" },
{ "indexed": false, "name": "value", "type": "uint256" }
],
"name": "Transfer",
"type": "event"
}
]

View File

@@ -0,0 +1,55 @@
[
{
"inputs": [
{
"internalType": "address",
"name": "_factoryV1",
"type": "address"
},
{
"internalType": "address",
"name": "_router",
"type": "address"
}
],
"stateMutability": "nonpayable",
"type": "constructor"
},
{
"inputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
},
{
"internalType": "uint256",
"name": "amountTokenMin",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "amountETHMin",
"type": "uint256"
},
{
"internalType": "address",
"name": "to",
"type": "address"
},
{
"internalType": "uint256",
"name": "deadline",
"type": "uint256"
}
],
"name": "migrate",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"stateMutability": "payable",
"type": "receive"
}
]

View File

@@ -0,0 +1,5 @@
import MIGRATOR_ABI from './migrator.json'
const MIGRATOR_ADDRESS = '0x16D4F26C15f3658ec65B1126ff27DD3dF2a2996b'
export { MIGRATOR_ADDRESS, MIGRATOR_ABI }

View File

@@ -1,24 +1,63 @@
import { ChainId, JSBI, Percent, Token, WETH } from '@uniswap/sdk'
import { ChainId, JSBI, Percent, Token, WETH, Pair, TokenAmount } from '@uniswap/sdk'
import { fortmatic, injected, portis, walletconnect, walletlink } from '../connectors'
export const ROUTER_ADDRESS = '0xf164fC0Ec4E93095b804a4795bBe1e041497b92a'
export const ROUTER_ADDRESS = '0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D'
// used for display in the default list when adding liquidity
export const COMMON_BASES = {
// used to construct intermediary pairs for trading
export const BASES_TO_CHECK_TRADES_AGAINST: { readonly [chainId in ChainId]: Token[] } = {
[ChainId.MAINNET]: [
WETH[ChainId.MAINNET],
new Token(ChainId.MAINNET, '0x6B175474E89094C44Da98b954EedeAC495271d0F', 18, 'DAI', 'Dai Stablecoin'),
new Token(ChainId.MAINNET, '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', 6, 'USDC', 'USD//C')
],
[ChainId.ROPSTEN]: [WETH[ChainId.ROPSTEN]],
[ChainId.RINKEBY]: [
WETH[ChainId.RINKEBY],
new Token(ChainId.RINKEBY, '0xc7AD46e0b8a400Bb3C915120d284AafbA8fc4735', 18, 'DAI', 'Dai Stablecoin')
],
[ChainId.RINKEBY]: [WETH[ChainId.RINKEBY]],
[ChainId.GÖRLI]: [WETH[ChainId.GÖRLI]],
[ChainId.KOVAN]: [WETH[ChainId.KOVAN]]
}
// used for display in the default list when adding liquidity
export const SUGGESTED_BASES = BASES_TO_CHECK_TRADES_AGAINST
// used to construct the list of all pairs we consider by default in the frontend
export const BASES_TO_TRACK_LIQUIDITY_FOR = BASES_TO_CHECK_TRADES_AGAINST
export const DUMMY_PAIRS_TO_PIN: { readonly [chainId in ChainId]?: Pair[] } = {
[ChainId.MAINNET]: [
new Pair(
new TokenAmount(
new Token(ChainId.MAINNET, '0x5d3a536E4D6DbD6114cc1Ead35777bAB948E3643', 8, 'cDAI', 'Compound Dai'),
'0'
),
new TokenAmount(
new Token(ChainId.MAINNET, '0x39AA39c021dfbaE8faC545936693aC917d5E7563', 8, 'cUSDC', 'Compound USD Coin'),
'0'
)
),
new Pair(
new TokenAmount(
new Token(ChainId.MAINNET, '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', 6, 'USDC', 'USD//C'),
'0'
),
new TokenAmount(
new Token(ChainId.MAINNET, '0xdAC17F958D2ee523a2206206994597C13D831ec7', 6, 'USDT', 'Tether USD'),
'0'
)
),
new Pair(
new TokenAmount(
new Token(ChainId.MAINNET, '0x6B175474E89094C44Da98b954EedeAC495271d0F', 18, 'DAI', 'Dai Stablecoin'),
'0'
),
new TokenAmount(
new Token(ChainId.MAINNET, '0xdAC17F958D2ee523a2206206994597C13D831ec7', 6, 'USDT', 'Tether USD'),
'0'
)
)
]
}
const MAINNET_WALLETS = {
INJECTED: {
connector: injected,

View File

@@ -7,6 +7,7 @@ export default [
new Token(ChainId.MAINNET, '0xD46bA6D942050d489DBd938a2C909A5d5039A161', 9, 'AMPL', 'Ampleforth'),
new Token(ChainId.MAINNET, '0xcD62b1C403fa761BAadFC74C525ce2B51780b184', 18, 'ANJ', 'Aragon Network Juror'),
new Token(ChainId.MAINNET, '0x960b236A07cf122663c4303350609A66A7B288C0', 18, 'ANT', 'Aragon Network Token'),
new Token(ChainId.MAINNET, '0x27054b13b1B798B345b591a4d22e6562d47eA75a', 4, 'AST', 'AirSwap Token'),
new Token(ChainId.MAINNET, '0xBA11D00c5f74255f56a5E366F4F77f5A186d7f55', 18, 'BAND', 'BandToken'),
new Token(ChainId.MAINNET, '0x0D8775F648430679A709E98d2b0Cb6250d2887EF', 18, 'BAT', 'Basic Attention Token'),
new Token(ChainId.MAINNET, '0x107c4504cd79C5d2696Ea0030a8dD4e92601B82e', 18, 'BLT', 'Bloom Token'),
@@ -31,6 +32,7 @@ export default [
'Decentralized Insurance Protocol'
),
new Token(ChainId.MAINNET, '0xC0F9bD5Fa5698B6505F643900FFA515Ea5dF54A9', 18, 'DONUT', 'Donut'),
new Token(ChainId.MAINNET, '0x86FADb80d8D2cff3C3680819E4da99C10232Ba0F', 18, 'EBASE', 'EURBASE Stablecoin'),
new Token(ChainId.MAINNET, '0xF629cBd94d3791C9250152BD8dfBDF380E2a3B9c', 18, 'ENJ', 'Enjin Coin'),
new Token(ChainId.MAINNET, '0x06f65b8CfCb13a9FE37d836fE9708dA38Ecb29B2', 18, 'FAME', 'SAINT FAME: Genesis Shirt'),
new Token(ChainId.MAINNET, '0x4946Fcea7C692606e8908002e55A582af44AC121', 18, 'FOAM', 'FOAM Token'),
@@ -77,6 +79,9 @@ export default [
new Token(ChainId.MAINNET, '0xF970b8E36e23F7fC3FD752EeA86f8Be8D83375A6', 18, 'RCN', 'Ripio Credit Network Token'),
new Token(ChainId.MAINNET, '0x255Aa6DF07540Cb5d3d297f0D0D4D84cb52bc8e6', 18, 'RDN', 'Raiden Token'),
new Token(ChainId.MAINNET, '0x408e41876cCCDC0F92210600ef50372656052a38', 18, 'REN', 'Republic Token'),
new Token(ChainId.MAINNET, '0x459086F2376525BdCebA5bDDA135e4E9d3FeF5bf', 8, 'renBCH', 'renBCH'),
new Token(ChainId.MAINNET, '0xEB4C2781e4ebA804CE9a9803C67d0893436bB27D', 8, 'renBTC', 'renBTC'),
new Token(ChainId.MAINNET, '0x1C5db575E2Ff833E46a2E9864C22F4B22E0B37C2', 8, 'renZEC', 'renZEC'),
new Token(ChainId.MAINNET, '0x1985365e9f78359a9B6AD760e32412f4a445E862', 18, 'REP', 'Reputation'),
new Token(ChainId.MAINNET, '0x9469D013805bFfB7D3DEBe5E7839237e535ec483', 18, 'RING', 'Darwinia Network Native Token'),
new Token(ChainId.MAINNET, '0x607F4C5BB672230e8672085532f7e901544a7375', 9, 'RLC', 'iEx.ec Network Token'),

View File

@@ -1,9 +1,17 @@
import { Interface } from '@ethersproject/abi'
import { ChainId } from '@uniswap/sdk'
import V1_EXCHANGE_ABI from './v1_exchange.json'
import V1_FACTORY_ABI from './v1_factory.json'
const V1_FACTORY_ADDRESS = '0xc0a47dFe034B400B47bDaD5FecDa2621de6c4d95'
const V1_FACTORY_ADDRESSES: { [chainId in ChainId]: string } = {
[ChainId.MAINNET]: '0xc0a47dFe034B400B47bDaD5FecDa2621de6c4d95',
[ChainId.ROPSTEN]: '0x9c83dCE8CA20E9aAF9D3efc003b2ea62aBC08351',
[ChainId.RINKEBY]: '0xf5D915570BC477f9B8D6C0E980aA81757A3AaC36',
[ChainId.GÖRLI]: '0x6Ce570d02D73d4c384b46135E87f8C592A8c86dA',
[ChainId.KOVAN]: '0xD3E51Ef092B2845f10401a0159B2B96e8B6c3D30'
}
const V1_FACTORY_INTERFACE = new Interface(V1_FACTORY_ABI)
const V1_EXCHANGE_INTERFACE = new Interface(V1_EXCHANGE_ABI)
export { V1_FACTORY_ADDRESS, V1_FACTORY_INTERFACE, V1_FACTORY_ABI, V1_EXCHANGE_INTERFACE, V1_EXCHANGE_ABI }
export { V1_FACTORY_ADDRESSES, V1_FACTORY_INTERFACE, V1_FACTORY_ABI, V1_EXCHANGE_INTERFACE, V1_EXCHANGE_ABI }

View File

@@ -1,8 +1,10 @@
import { Token, TokenAmount, Pair } from '@uniswap/sdk'
import { useMemo } from 'react'
import { abi as IUniswapV2PairABI } from '@uniswap/v2-core/build/IUniswapV2Pair.json'
import { Interface } from '@ethersproject/abi'
import { usePairContract } from '../hooks/useContract'
import { useSingleCallResult } from '../state/multicall/hooks'
import { useSingleCallResult, useMultipleContractSingleData } from '../state/multicall/hooks'
/*
* if loading, return undefined
@@ -22,3 +24,30 @@ export function usePair(tokenA?: Token, tokenB?: Token): undefined | Pair | null
return new Pair(new TokenAmount(token0, reserve0.toString()), new TokenAmount(token1, reserve1.toString()))
}, [loading, reserves, tokenA, tokenB])
}
const PAIR_INTERFACE = new Interface(IUniswapV2PairABI)
export function usePairs(tokens: [Token | undefined, Token | undefined][]): (undefined | Pair | null)[] {
const pairAddresses = useMemo(
() =>
tokens.map(([tokenA, tokenB]) => {
return tokenA && tokenB && !tokenA.equals(tokenB) ? Pair.getAddress(tokenA, tokenB) : undefined
}),
[tokens]
)
const results = useMultipleContractSingleData(pairAddresses, PAIR_INTERFACE, 'getReserves')
return useMemo(() => {
return results.map((result, i) => {
const { result: reserves, loading } = result
const tokenA = tokens[i][0]
const tokenB = tokens[i][1]
if (loading || !tokenA || !tokenB) return undefined
if (!reserves) return null
const { reserve0, reserve1 } = reserves
const [token0, token1] = tokenA.sortsBefore(tokenB) ? [tokenA, tokenB] : [tokenB, tokenA]
return new Pair(new TokenAmount(token0, reserve0.toString()), new TokenAmount(token1, reserve1.toString()))
})
}, [results, tokens])
}

View File

@@ -30,42 +30,39 @@ function useMockV1Pair(token?: Token): MockV1Pair | undefined {
: undefined
}
// returns ALL v1 exchange addresses
export function useAllV1ExchangeAddresses(): string[] {
const factory = useV1FactoryContract()
const exchangeCount = useSingleCallResult(factory, 'tokenCount')?.result
const parsedCount = parseInt(exchangeCount?.toString() ?? '0')
const indices = useMemo(() => [...Array(parsedCount).keys()].map(ix => [ix]), [parsedCount])
const data = useSingleContractMultipleData(factory, 'getTokenWithId', indices, NEVER_RELOAD)
return useMemo(() => data?.map(({ result }) => result?.[0])?.filter(x => x) ?? [], [data])
}
// returns all v1 exchange addresses in the user's token list
export function useAllTokenV1ExchangeAddresses(): string[] {
export function useAllTokenV1Exchanges(): { [exchangeAddress: string]: Token } {
const allTokens = useAllTokens()
const factory = useV1FactoryContract()
const args = useMemo(() => Object.keys(allTokens).map(tokenAddress => [tokenAddress]), [allTokens])
const data = useSingleContractMultipleData(factory, 'getExchange', args, NEVER_RELOAD)
return useMemo(() => data?.map(({ result }) => result?.[0])?.filter(x => x) ?? [], [data])
return useMemo(
() =>
data?.reduce<{ [exchangeAddress: string]: Token }>((memo, { result }, ix) => {
const token = allTokens[args[ix][0]]
if (result?.[0]) {
memo[result?.[0]] = token
}
return memo
}, {}) ?? {},
[allTokens, args, data]
)
}
// returns whether any of the tokens in the user's token list have liquidity on v1
export function useUserProbablyHasV1Liquidity(): boolean | undefined {
const exchangeAddresses = useAllTokenV1ExchangeAddresses()
export function useUserHasLiquidityInAllTokens(): boolean | undefined {
const exchanges = useAllTokenV1Exchanges()
const { account, chainId } = useActiveWeb3React()
const fakeTokens = useMemo(
() => (chainId ? exchangeAddresses.map(address => new Token(chainId, address, 18, 'UNI-V1')) : []),
[chainId, exchangeAddresses]
const fakeLiquidityTokens = useMemo(
() => (chainId ? Object.keys(exchanges).map(address => new Token(chainId, address, 18, 'UNI-V1')) : []),
[chainId, exchanges]
)
const balances = useTokenBalances(account ?? undefined, fakeTokens)
const balances = useTokenBalances(account ?? undefined, fakeLiquidityTokens)
return useMemo(
() =>

View File

@@ -1,10 +1,13 @@
import { parseBytes32String } from '@ethersproject/strings'
import { ChainId, Token, WETH } from '@uniswap/sdk'
import { useEffect, useMemo } from 'react'
import { ALL_TOKENS } from '../constants/tokens'
import { useAddUserToken, useFetchTokenByAddress, useUserAddedTokens } from '../state/user/hooks'
import { NEVER_RELOAD, useSingleCallResult } from '../state/multicall/hooks'
import { useAddUserToken, useUserAddedTokens } from '../state/user/hooks'
import { isAddress } from '../utils'
import { useActiveWeb3React } from './index'
import { useBytes32TokenContract, useTokenContract } from './useContract'
export function useAllTokens(): { [address: string]: Token } {
const { chainId } = useActiveWeb3React()
@@ -35,36 +38,83 @@ export function useAllTokens(): { [address: string]: Token } {
}, [userAddedTokens, chainId])
}
export function useToken(tokenAddress?: string): Token | undefined {
// parse a name or symbol from a token response
const BYTES32_REGEX = /^0x[a-fA-F0-9]{64}$/
function parseStringOrBytes32(str: string | undefined, bytes32: string | undefined, defaultValue: string): string {
return str && str.length > 0
? str
: bytes32 && BYTES32_REGEX.test(bytes32)
? parseBytes32String(bytes32)
: defaultValue
}
// undefined if invalid or does not exist
// null if loading
// otherwise returns the token
export function useToken(tokenAddress?: string): Token | undefined | null {
const { chainId } = useActiveWeb3React()
const tokens = useAllTokens()
const address = isAddress(tokenAddress)
const tokenContract = useTokenContract(address ? address : undefined, false)
const tokenContractBytes32 = useBytes32TokenContract(address ? address : undefined, false)
const token: Token | undefined = address ? tokens[address] : undefined
const tokenName = useSingleCallResult(token ? undefined : tokenContract, 'name', undefined, NEVER_RELOAD)
const tokenNameBytes32 = useSingleCallResult(
token ? undefined : tokenContractBytes32,
'name',
undefined,
NEVER_RELOAD
)
const symbol = useSingleCallResult(token ? undefined : tokenContract, 'symbol', undefined, NEVER_RELOAD)
const symbolBytes32 = useSingleCallResult(token ? undefined : tokenContractBytes32, 'symbol', undefined, NEVER_RELOAD)
const decimals = useSingleCallResult(token ? undefined : tokenContract, 'decimals', undefined, NEVER_RELOAD)
return useMemo(() => {
const validatedAddress = isAddress(tokenAddress)
if (!validatedAddress) return
return tokens[validatedAddress]
}, [tokens, tokenAddress])
if (token) return token
if (!chainId || !address) return undefined
if (decimals.loading || symbol.loading || tokenName.loading) return null
if (decimals.result) {
return new Token(
chainId,
address,
decimals.result[0],
parseStringOrBytes32(symbol.result?.[0], symbolBytes32.result?.[0], 'UNKNOWN'),
parseStringOrBytes32(tokenName.result?.[0], tokenNameBytes32.result?.[0], 'Unknown Token')
)
}
return undefined
}, [
address,
chainId,
decimals.loading,
decimals.result,
symbol.loading,
symbol.result,
symbolBytes32.result,
token,
tokenName.loading,
tokenName.result,
tokenNameBytes32.result
])
}
// gets token information by address (typically user input) and
// automatically adds it for the user if the token address is valid
export function useTokenByAddressAndAutomaticallyAdd(tokenAddress?: string): Token | undefined {
const fetchTokenByAddress = useFetchTokenByAddress()
// automatically adds it for the user if it's a valid token address
export function useTokenByAddressAndAutomaticallyAdd(tokenAddress?: string): Token | undefined | null {
const addToken = useAddUserToken()
const token = useToken(tokenAddress)
const { chainId } = useActiveWeb3React()
const allTokens = useAllTokens()
useEffect(() => {
if (!chainId || !isAddress(tokenAddress)) return
const weth = WETH[chainId as ChainId]
if (weth && weth.address === isAddress(tokenAddress)) return
if (tokenAddress && !token) {
fetchTokenByAddress(tokenAddress).then(token => {
if (token !== null) {
addToken(token)
}
})
}
}, [tokenAddress, token, fetchTokenByAddress, addToken, chainId])
if (!chainId || !token) return
if (WETH[chainId as ChainId]?.address === token.address) return
if (allTokens[token.address]) return
addToken(token)
}, [token, addToken, chainId, allTokens])
return token
}

View File

@@ -1,47 +1,44 @@
import { useMemo } from 'react'
import { WETH, Token, TokenAmount, Trade, ChainId, Pair } from '@uniswap/sdk'
import { useActiveWeb3React } from './index'
import { usePair } from '../data/Reserves'
import { Token, TokenAmount, Trade, ChainId, Pair } from '@uniswap/sdk'
import flatMap from 'lodash.flatmap'
import { useActiveWeb3React } from './index'
import { usePairs } from '../data/Reserves'
import { BASES_TO_CHECK_TRADES_AGAINST } from '../constants'
const DAI = new Token(ChainId.MAINNET, '0x6B175474E89094C44Da98b954EedeAC495271d0F', 18, 'DAI', 'Dai Stablecoin')
const USDC = new Token(ChainId.MAINNET, '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', 6, 'USDC', 'USD//C')
function useAllCommonPairs(tokenA?: Token, tokenB?: Token): Pair[] {
const { chainId } = useActiveWeb3React()
// check for direct pair between tokens
const pairBetween = usePair(tokenA, tokenB)
const bases = useMemo(() => BASES_TO_CHECK_TRADES_AGAINST[chainId as ChainId] ?? [], [chainId])
// get token<->WETH pairs
const aToETH = usePair(tokenA, WETH[chainId as ChainId])
const bToETH = usePair(tokenB, WETH[chainId as ChainId])
// get token<->DAI pairs
const aToDAI = usePair(tokenA, chainId === ChainId.MAINNET ? DAI : undefined)
const bToDAI = usePair(tokenB, chainId === ChainId.MAINNET ? DAI : undefined)
// get token<->USDC pairs
const aToUSDC = usePair(tokenA, chainId === ChainId.MAINNET ? USDC : undefined)
const bToUSDC = usePair(tokenB, chainId === ChainId.MAINNET ? USDC : undefined)
// get connecting pairs
const DAIToETH = usePair(chainId === ChainId.MAINNET ? DAI : undefined, WETH[chainId as ChainId])
const USDCToETH = usePair(chainId === ChainId.MAINNET ? USDC : undefined, WETH[chainId as ChainId])
const DAIToUSDC = usePair(
chainId === ChainId.MAINNET ? DAI : undefined,
chainId === ChainId.MAINNET ? USDC : undefined
const allPairCombinations: [Token | undefined, Token | undefined][] = useMemo(
() => [
// the direct pair
[tokenA, tokenB],
// token A against all bases
...bases.map((base): [Token | undefined, Token | undefined] => [tokenA, base]),
// token B against all bases
...bases.map((base): [Token | undefined, Token | undefined] => [tokenB, base]),
// each base against all bases
...flatMap(bases, (base): [Token, Token][] => bases.map(otherBase => [base, otherBase]))
],
[tokenA, tokenB, bases]
)
const allPairs = usePairs(allPairCombinations)
// only pass along valid pairs, non-duplicated pairs
return useMemo(
() =>
[pairBetween, aToETH, bToETH, aToDAI, bToDAI, aToUSDC, bToUSDC, DAIToETH, USDCToETH, DAIToUSDC]
allPairs
// filter out invalid pairs
.filter((p): p is Pair => !!p)
// filter out duplicated pairs
.filter(
(p, i, pairs) => i === pairs.findIndex(pair => pair?.liquidityToken.address === p.liquidityToken.address)
),
[pairBetween, aToETH, bToETH, aToDAI, bToDAI, aToUSDC, bToUSDC, DAIToETH, USDCToETH, DAIToUSDC]
[allPairs]
)
}

View File

@@ -1,11 +1,13 @@
import { Web3Provider } from '@ethersproject/providers'
import { ChainId } from '@uniswap/sdk'
import { useWeb3React as useWeb3ReactCore } from '@web3-react/core'
import { Web3ReactContextInterface } from '@web3-react/core/dist/types'
import { useEffect, useState } from 'react'
import { isMobile } from 'react-device-detect'
import { injected } from '../connectors'
import { NetworkContextName } from '../constants'
export function useActiveWeb3React() {
export function useActiveWeb3React(): Web3ReactContextInterface<Web3Provider> & { chainId?: ChainId } {
const context = useWeb3ReactCore<Web3Provider>()
const contextNetwork = useWeb3ReactCore<Web3Provider>(NetworkContextName)
return context.active ? context : contextNetwork

View File

@@ -2,8 +2,10 @@ import { Contract } from '@ethersproject/contracts'
import { ChainId } from '@uniswap/sdk'
import { abi as IUniswapV2PairABI } from '@uniswap/v2-core/build/IUniswapV2Pair.json'
import { useMemo } from 'react'
import { ERC20_BYTES32_ABI } from '../constants/abis/erc20'
import ERC20_ABI from '../constants/abis/erc20.json'
import { V1_EXCHANGE_ABI, V1_FACTORY_ABI, V1_FACTORY_ADDRESS } from '../constants/v1'
import { MIGRATOR_ABI, MIGRATOR_ADDRESS } from '../constants/abis/migrator'
import { V1_EXCHANGE_ABI, V1_FACTORY_ABI, V1_FACTORY_ADDRESSES } from '../constants/v1'
import { MULTICALL_ABI, MULTICALL_NETWORKS } from '../constants/multicall'
import { getContract } from '../utils'
import { useActiveWeb3React } from './index'
@@ -25,17 +27,25 @@ function useContract(address?: string, ABI?: any, withSignerIfPossible = true):
export function useV1FactoryContract(): Contract | null {
const { chainId } = useActiveWeb3React()
return useContract(chainId === 1 ? V1_FACTORY_ADDRESS : undefined, V1_FACTORY_ABI, false)
return useContract(V1_FACTORY_ADDRESSES[chainId as ChainId], V1_FACTORY_ABI, false)
}
export function useV1ExchangeContract(address: string): Contract | null {
return useContract(address, V1_EXCHANGE_ABI, false)
}
export function useV2MigratorContract(): Contract | null {
return useContract(MIGRATOR_ADDRESS, MIGRATOR_ABI, true)
}
export function useTokenContract(tokenAddress?: string, withSignerIfPossible = true): Contract | null {
return useContract(tokenAddress, ERC20_ABI, withSignerIfPossible)
}
export function useBytes32TokenContract(tokenAddress?: string, withSignerIfPossible = true): Contract | null {
return useContract(tokenAddress, ERC20_BYTES32_ABI, withSignerIfPossible)
}
export function usePairContract(pairAddress?: string, withSignerIfPossible = true): Contract | null {
return useContract(pairAddress, IUniswapV2PairABI, withSignerIfPossible)
}

View File

@@ -90,11 +90,13 @@ export function useSwapCallback(
chainId as ChainId
)
let estimate, method: Function, args: Array<string | string[] | number>, value: BigNumber | null
// let estimate: Function, method: Function,
let methodNames: string[],
args: Array<string | string[] | number>,
value: BigNumber | null = null
switch (swapType) {
case SwapType.EXACT_TOKENS_FOR_TOKENS:
estimate = routerContract.estimateGas.swapExactTokensForTokens
method = routerContract.swapExactTokensForTokens
methodNames = ['swapExactTokensForTokens', 'swapExactTokensForTokensSupportingFeeOnTransferTokens']
args = [
slippageAdjustedInput.raw.toString(),
slippageAdjustedOutput.raw.toString(),
@@ -102,11 +104,9 @@ export function useSwapCallback(
recipient,
deadlineFromNow
]
value = null
break
case SwapType.TOKENS_FOR_EXACT_TOKENS:
estimate = routerContract.estimateGas.swapTokensForExactTokens
method = routerContract.swapTokensForExactTokens
methodNames = ['swapTokensForExactTokens']
args = [
slippageAdjustedOutput.raw.toString(),
slippageAdjustedInput.raw.toString(),
@@ -114,17 +114,14 @@ export function useSwapCallback(
recipient,
deadlineFromNow
]
value = null
break
case SwapType.EXACT_ETH_FOR_TOKENS:
estimate = routerContract.estimateGas.swapExactETHForTokens
method = routerContract.swapExactETHForTokens
methodNames = ['swapExactETHForTokens', 'swapExactETHForTokensSupportingFeeOnTransferTokens']
args = [slippageAdjustedOutput.raw.toString(), path, recipient, deadlineFromNow]
value = BigNumber.from(slippageAdjustedInput.raw.toString())
break
case SwapType.TOKENS_FOR_EXACT_ETH:
estimate = routerContract.estimateGas.swapTokensForExactETH
method = routerContract.swapTokensForExactETH
methodNames = ['swapTokensForExactETH']
args = [
slippageAdjustedOutput.raw.toString(),
slippageAdjustedInput.raw.toString(),
@@ -132,11 +129,9 @@ export function useSwapCallback(
recipient,
deadlineFromNow
]
value = null
break
case SwapType.EXACT_TOKENS_FOR_ETH:
estimate = routerContract.estimateGas.swapExactTokensForETH
method = routerContract.swapExactTokensForETH
methodNames = ['swapExactTokensForETH', 'swapExactTokensForETHSupportingFeeOnTransferTokens']
args = [
slippageAdjustedInput.raw.toString(),
slippageAdjustedOutput.raw.toString(),
@@ -144,58 +139,99 @@ export function useSwapCallback(
recipient,
deadlineFromNow
]
value = null
break
case SwapType.ETH_FOR_EXACT_TOKENS:
estimate = routerContract.estimateGas.swapETHForExactTokens
method = routerContract.swapETHForExactTokens
methodNames = ['swapETHForExactTokens']
args = [slippageAdjustedOutput.raw.toString(), path, recipient, deadlineFromNow]
value = BigNumber.from(slippageAdjustedInput.raw.toString())
break
}
return estimate(...args, value ? { value } : {})
.then(estimatedGasLimit =>
method(...args, {
...(value ? { value } : {}),
gasLimit: calculateGasMargin(estimatedGasLimit)
})
const safeGasEstimates = await Promise.all(
methodNames.map(methodName =>
routerContract.estimateGas[methodName](...args, value ? { value } : {})
.then(calculateGasMargin)
.catch(error => {
console.error(`estimateGas failed for ${methodName}`, error)
})
)
.then(response => {
if (recipient === account) {
addTransaction(response, {
summary:
'Swap ' +
slippageAdjustedInput.toSignificant(3) +
' ' +
trade.inputAmount.token.symbol +
' for ' +
slippageAdjustedOutput.toSignificant(3) +
' ' +
trade.outputAmount.token.symbol
})
} else {
addTransaction(response, {
summary:
'Swap ' +
slippageAdjustedInput.toSignificant(3) +
' ' +
trade.inputAmount.token.symbol +
' for ' +
slippageAdjustedOutput.toSignificant(3) +
' ' +
trade.outputAmount.token.symbol +
' to ' +
(ensName ?? recipient)
})
}
)
return response.hash
})
.catch(error => {
console.error(`Swap or gas estimate failed`, error)
throw error
const indexOfSuccessfulEstimation = safeGasEstimates.findIndex(safeGasEstimate =>
BigNumber.isBigNumber(safeGasEstimate)
)
// all estimations failed...
if (indexOfSuccessfulEstimation === -1) {
// if only 1 method exists, either:
// a) the token is doing something weird not related to FoT (e.g. enforcing a whitelist)
// b) the token is FoT and the user specified an exact output, which is not allowed
if (methodNames.length === 1) {
throw Error(
`An error occurred. If either of the tokens you're swapping take a fee on transfer, you must specify an exact input amount.`
)
}
// if 2 methods exists, either:
// a) the token is doing something weird not related to FoT (e.g. enforcing a whitelist)
// b) the token is FoT and is taking more than the specified slippage
else if (methodNames.length === 2) {
throw Error(
`An error occurred. If either of the tokens you're swapping take a fee on transfer, you must specify a slippage tolerance higher than the fee.`
)
} else {
throw Error('This transaction would fail. Please contact support.')
}
} else {
const methodName = methodNames[indexOfSuccessfulEstimation]
const safeGasEstimate = safeGasEstimates[indexOfSuccessfulEstimation]
return routerContract[methodName](...args, {
gasLimit: safeGasEstimate,
...(value ? { value } : {})
})
.then((response: any) => {
if (recipient === account) {
addTransaction(response, {
summary:
'Swap ' +
slippageAdjustedInput.toSignificant(3) +
' ' +
trade.inputAmount.token.symbol +
' for ' +
slippageAdjustedOutput.toSignificant(3) +
' ' +
trade.outputAmount.token.symbol
})
} else {
addTransaction(response, {
summary:
'Swap ' +
slippageAdjustedInput.toSignificant(3) +
' ' +
trade.inputAmount.token.symbol +
' for ' +
slippageAdjustedOutput.toSignificant(3) +
' ' +
trade.outputAmount.token.symbol +
' to ' +
(ensName ?? recipient)
})
}
return response.hash
})
.catch((error: any) => {
// if the user rejected the tx, pass this along
if (error?.code === 4001) {
throw error
}
// otherwise, the error was unexpected and we need to convey that
else {
console.error(`swap failed for ${methodName}`, error)
throw Error('An error occurred while swapping. Please contact support.')
}
})
}
}
}, [account, allowedSlippage, addTransaction, chainId, deadline, inputAllowance, library, trade, ensName, recipient])
}

View File

@@ -64,13 +64,12 @@ export default function AddLiquidity({ match: { params } }: RouteComponentProps<
const isValid = !error
// modal and loading
const [showConfirm, setShowConfirm] = useState<boolean>(false)
const [showAdvanced, setShowAdvanced] = useState<boolean>(false)
const [attemptingTxn, setAttemptingTxn] = useState<boolean>(false) // clicked confirm
const [pendingConfirmation, setPendingConfirmation] = useState<boolean>(true) // waiting for user confirmation
// txn values
const [showAdvanced, setShowAdvanced] = useState<boolean>(false) // toggling slippage, deadline, etc. on and off
const [showConfirm, setShowConfirm] = useState<boolean>(false) // show confirmation modal
const [attemptingTxn, setAttemptingTxn] = useState<boolean>(false) // waiting for user confirmaion/rejection
const [txHash, setTxHash] = useState<string>('')
// tx parameters
const [deadline, setDeadline] = useState<number>(DEFAULT_DEADLINE_FROM_NOW)
const [allowedSlippage, setAllowedSlippage] = useState<number>(INITIAL_ALLOWED_SLIPPAGE)
@@ -114,8 +113,6 @@ export default function AddLiquidity({ match: { params } }: RouteComponentProps<
const addTransaction = useTransactionAdder()
async function onAdd() {
setAttemptingTxn(true)
const router = getRouterContract(chainId, library, account)
const amountsMin = {
@@ -155,12 +152,15 @@ export default function AddLiquidity({ match: { params } }: RouteComponentProps<
value = null
}
setAttemptingTxn(true)
await estimate(...args, value ? { value } : {})
.then(estimatedGasLimit =>
method(...args, {
...(value ? { value } : {}),
gasLimit: calculateGasMargin(estimatedGasLimit)
}).then(response => {
setAttemptingTxn(false)
addTransaction(response, {
summary:
'Add ' +
@@ -174,7 +174,6 @@ export default function AddLiquidity({ match: { params } }: RouteComponentProps<
})
setTxHash(response.hash)
setPendingConfirmation(false)
ReactGA.event({
category: 'Liquidity',
@@ -183,12 +182,12 @@ export default function AddLiquidity({ match: { params } }: RouteComponentProps<
})
})
)
.catch((e: Error) => {
console.error(e)
setPendingConfirmation(true)
.catch(error => {
setAttemptingTxn(false)
setShowConfirm(false)
setShowAdvanced(false)
// we only care if the error is something _other_ than the user rejected the tx
if (error?.code !== 4001) {
console.error(error)
}
})
}
@@ -311,13 +310,15 @@ export default function AddLiquidity({ match: { params } }: RouteComponentProps<
<ConfirmationModal
isOpen={showConfirm}
onDismiss={() => {
setPendingConfirmation(true)
setAttemptingTxn(false)
setShowConfirm(false)
// if there was a tx hash, we want to clear the input
if (txHash) {
onUserInput(Field.TOKEN_A, '')
}
setTxHash('')
}}
attemptingTxn={attemptingTxn}
pendingConfirmation={pendingConfirmation}
hash={txHash ? txHash : ''}
hash={txHash}
topContent={() => modalHeader()}
bottomContent={modalBottom}
pendingText={pendingText}

View File

@@ -9,6 +9,8 @@ import Web3ReactManager from '../components/Web3ReactManager'
import DarkModeQueryParamReader from '../theme/DarkModeQueryParamReader'
import AddLiquidity from './AddLiquidity'
import CreatePool from './CreatePool'
import MigrateV1 from './MigrateV1'
import MigrateV1Exchange from './MigrateV1/MigrateV1Exchange'
import Pool from './Pool'
import PoolFinder from './PoolFinder'
import RemoveLiquidity from './RemoveLiquidity'
@@ -21,7 +23,6 @@ const AppWrapper = styled.div`
flex-flow: column;
align-items: flex-start;
overflow-x: hidden;
height: 100vh;
`
const HeaderWrapper = styled.div`
@@ -50,7 +51,7 @@ const BodyWrapper = styled.div`
const BackgroundGradient = styled.div`
width: 100%;
height: 200vh;
height: 170vh;
background: ${({ theme }) => `radial-gradient(50% 50% at 50% 50%, ${theme.primary1} 0%, ${theme.bg1} 100%)`};
position: absolute;
top: 0px;
@@ -100,6 +101,8 @@ export default function App() {
<Route exact strict path="/create" component={CreatePool} />
<Route exact strict path="/add/:tokens" component={AddLiquidity} />
<Route exact strict path="/remove/:tokens" component={RemoveLiquidity} />
<Route exact strict path="/migrate/v1" component={MigrateV1} />
<Route exact strict path="/migrate/v1/:address" component={MigrateV1Exchange} />
<Route component={RedirectPathToSwapOnly} />
</Switch>
</Web3ReactManager>
@@ -109,7 +112,6 @@ export default function App() {
<BackgroundGradient />
</AppWrapper>
</Router>
<div id="popover-container" />
</Suspense>
)
}

View File

@@ -2,7 +2,7 @@ import React from 'react'
import styled from 'styled-components'
import NavigationTabs from '../components/NavigationTabs'
const Body = styled.div`
export const BodyWrapper = styled.div`
position: relative;
max-width: 420px;
width: 100%;
@@ -18,9 +18,9 @@ const Body = styled.div`
*/
export default function AppBody({ children }: { children: React.ReactNode }) {
return (
<Body>
<BodyWrapper>
<NavigationTabs />
<>{children}</>
</Body>
</BodyWrapper>
)
}

View File

@@ -8,7 +8,7 @@ import TokenLogo from '../../components/TokenLogo'
import SearchModal from '../../components/SearchModal'
import { Text } from 'rebass'
import { Plus } from 'react-feather'
import { TYPE, Link } from '../../theme'
import { TYPE, StyledInternalLink } from '../../theme'
import { AutoColumn, ColumnCenter } from '../../components/Column'
import { ButtonPrimary, ButtonDropwdown, ButtonDropwdownLight } from '../../components/Button'
@@ -27,7 +27,7 @@ enum STEP {
SHOW_CREATE_PAGE = 'SHOW_CREATE_PAGE' // show create page
}
export default function CreatePool({ history, location }: RouteComponentProps) {
export default function CreatePool({ location }: RouteComponentProps) {
const { chainId } = useActiveWeb3React()
const [showSearch, setShowSearch] = useState<boolean>(false)
const [activeField, setActiveField] = useState<number>(Fields.TOKEN0)
@@ -116,8 +116,8 @@ export default function CreatePool({ history, location }: RouteComponentProps) {
{pair ? ( // pair already exists - prompt to add liquidity to existing pool
<AutoRow padding="10px" justify="center">
<TYPE.body textAlign="center">
Pool already exists!
<Link onClick={() => history.push('/add/' + token0Address + '-' + token1Address)}> Join the pool.</Link>
Pool already exists!{' '}
<StyledInternalLink to={`/add/${token0Address}-${token1Address}`}>Join the pool.</StyledInternalLink>
</TYPE.body>
</AutoRow>
) : (

View File

@@ -0,0 +1,11 @@
import React from 'react'
import { AutoColumn } from '../../components/Column'
import { TYPE } from '../../theme'
export function EmptyState({ message }: { message: string }) {
return (
<AutoColumn style={{ minHeight: 200, justifyContent: 'center', alignItems: 'center' }}>
<TYPE.body>{message}</TYPE.body>
</AutoColumn>
)
}

View File

@@ -0,0 +1,289 @@
import { TransactionResponse } from '@ethersproject/abstract-provider'
import { ChainId, Fraction, JSBI, Percent, Token, TokenAmount, WETH } from '@uniswap/sdk'
import React, { useCallback, useMemo, useState } from 'react'
import { ArrowLeft } from 'react-feather'
import ReactGA from 'react-ga'
import { Redirect, RouteComponentProps } from 'react-router'
import { ButtonConfirmed } from '../../components/Button'
import { PinkCard, YellowCard, LightCard } from '../../components/Card'
import { AutoColumn } from '../../components/Column'
import QuestionHelper from '../../components/QuestionHelper'
import { AutoRow, RowBetween } from '../../components/Row'
import { DEFAULT_DEADLINE_FROM_NOW, INITIAL_ALLOWED_SLIPPAGE } from '../../constants'
import { MIGRATOR_ADDRESS } from '../../constants/abis/migrator'
import { usePair } from '../../data/Reserves'
import { useTotalSupply } from '../../data/TotalSupply'
import { useActiveWeb3React } from '../../hooks'
import { useTokenByAddressAndAutomaticallyAdd } from '../../hooks/Tokens'
import { ApprovalState, useApproveCallback } from '../../hooks/useApproveCallback'
import { useV1ExchangeContract, useV2MigratorContract } from '../../hooks/useContract'
import { NEVER_RELOAD, useSingleCallResult } from '../../state/multicall/hooks'
import { useIsTransactionPending, useTransactionAdder } from '../../state/transactions/hooks'
import { useETHBalances, useTokenBalance } from '../../state/wallet/hooks'
import { TYPE } from '../../theme'
import { isAddress } from '../../utils'
import { BodyWrapper } from '../AppBody'
import { EmptyState } from './EmptyState'
import TokenLogo from '../../components/TokenLogo'
import { FormattedPoolTokenAmount } from './index'
const WEI_DENOM = JSBI.exponentiate(JSBI.BigInt(10), JSBI.BigInt(18))
const ZERO = JSBI.BigInt(0)
const ONE = JSBI.BigInt(1)
const ZERO_FRACTION = new Fraction(ZERO, ONE)
const ALLOWED_OUTPUT_MIN_PERCENT = new Percent(JSBI.BigInt(10000 - INITIAL_ALLOWED_SLIPPAGE), JSBI.BigInt(10000))
function V1PairMigration({ liquidityTokenAmount, token }: { liquidityTokenAmount: TokenAmount; token: Token }) {
const { account, chainId } = useActiveWeb3React()
const totalSupply = useTotalSupply(liquidityTokenAmount.token)
const exchangeETHBalance = useETHBalances([liquidityTokenAmount.token.address])?.[liquidityTokenAmount.token.address]
const exchangeTokenBalance = useTokenBalance(liquidityTokenAmount.token.address, token)
const v2Pair = usePair(WETH[chainId as ChainId], token)
const isFirstLiquidityProvider: boolean = v2Pair === null
const v2SpotPrice = v2Pair?.reserveOf(token)?.divide(v2Pair?.reserveOf(WETH[chainId as ChainId]))
const [confirmingMigration, setConfirmingMigration] = useState<boolean>(false)
const [pendingMigrationHash, setPendingMigrationHash] = useState<string | null>(null)
const shareFraction: Fraction = totalSupply ? new Percent(liquidityTokenAmount.raw, totalSupply.raw) : ZERO_FRACTION
const ethWorth: Fraction = exchangeETHBalance
? new Fraction(shareFraction.multiply(exchangeETHBalance).quotient, WEI_DENOM)
: ZERO_FRACTION
const tokenWorth: TokenAmount = exchangeTokenBalance
? new TokenAmount(token, shareFraction.multiply(exchangeTokenBalance.raw).quotient)
: new TokenAmount(token, ZERO)
const [approval, approve] = useApproveCallback(liquidityTokenAmount, MIGRATOR_ADDRESS)
const v1SpotPrice =
exchangeTokenBalance && exchangeETHBalance
? exchangeTokenBalance.divide(new Fraction(exchangeETHBalance, WEI_DENOM))
: null
const priceDifferenceFraction: Fraction | undefined =
v1SpotPrice && v2SpotPrice
? v1SpotPrice
.divide(v2SpotPrice)
.multiply('100')
.subtract('100')
: undefined
const priceDifferenceAbs: Fraction | undefined = priceDifferenceFraction?.lessThan(ZERO)
? priceDifferenceFraction?.multiply('-1')
: priceDifferenceFraction
const minAmountETH: JSBI | undefined =
v2SpotPrice && tokenWorth
? tokenWorth
.divide(v2SpotPrice)
.multiply(WEI_DENOM)
.multiply(ALLOWED_OUTPUT_MIN_PERCENT).quotient
: ethWorth?.numerator
const minAmountToken: JSBI | undefined =
v2SpotPrice && ethWorth
? ethWorth
.multiply(v2SpotPrice)
.multiply(JSBI.exponentiate(JSBI.BigInt(10), JSBI.BigInt(token.decimals)))
.multiply(ALLOWED_OUTPUT_MIN_PERCENT).quotient
: tokenWorth?.numerator
const addTransaction = useTransactionAdder()
const isMigrationPending = useIsTransactionPending(pendingMigrationHash)
const migrator = useV2MigratorContract()
const migrate = useCallback(() => {
if (!minAmountToken || !minAmountETH) return
setConfirmingMigration(true)
migrator
.migrate(
token.address,
minAmountToken.toString(),
minAmountETH.toString(),
account,
Math.floor(new Date().getTime() / 1000) + DEFAULT_DEADLINE_FROM_NOW
)
.then((response: TransactionResponse) => {
ReactGA.event({
category: 'Migrate',
action: 'V1->V2',
label: token?.symbol
})
addTransaction(response, {
summary: `Migrate ${token.symbol} liquidity to V2`
})
setPendingMigrationHash(response.hash)
})
.catch(() => {
setConfirmingMigration(false)
})
}, [minAmountToken, minAmountETH, migrator, token, account, addTransaction])
const noLiquidityTokens = liquidityTokenAmount && liquidityTokenAmount.equalTo(ZERO)
const largePriceDifference = Boolean(priceDifferenceAbs && !priceDifferenceAbs.lessThan(JSBI.BigInt(5)))
const isSuccessfullyMigrated = Boolean(noLiquidityTokens && pendingMigrationHash)
return (
<AutoColumn gap="20px">
{!isFirstLiquidityProvider ? (
largePriceDifference ? (
<YellowCard>
<TYPE.body style={{ marginBottom: 8, fontWeight: 400 }}>
It is best to deposit liquidity into Uniswap V2 at a price you believe is correct. If you believe the
price is incorrect, you can either make a swap to move the price or wait for someone else to do so.
</TYPE.body>
<AutoColumn gap="8px">
<RowBetween>
<TYPE.body>V1 Price:</TYPE.body>
<TYPE.black>
{v1SpotPrice?.toSignificant(6)} {token.symbol}/ETH
</TYPE.black>
</RowBetween>
<RowBetween>
<TYPE.body>V2 Price:</TYPE.body>
<TYPE.black>
{v2SpotPrice?.toSignificant(6)} {token.symbol}/ETH
</TYPE.black>
</RowBetween>
<RowBetween>
<div>Price Difference:</div>
<div>{priceDifferenceAbs.toSignificant(4)}%</div>
</RowBetween>
</AutoColumn>
</YellowCard>
) : null
) : (
<PinkCard>
<AutoColumn gap="10px">
<div>
You are the first liquidity provider for this pair on Uniswap V2. Your liquidity will be migrated at the
current V1 price. Your transaction cost also includes the gas to create the pool.
</div>
<div>V1 Price</div>
<AutoColumn>
<div>
{v1SpotPrice?.invert()?.toSignificant(6)} ETH/{token.symbol}
</div>
<div>
{v1SpotPrice?.toSignificant(6)} {token.symbol}/ETH
</div>
</AutoColumn>
</AutoColumn>
</PinkCard>
)}
<LightCard>
<AutoRow style={{ justifyContent: 'flex-start', width: 'fit-content' }}>
<TokenLogo size="24px" address={token.address} />{' '}
<div style={{ marginLeft: '.75rem' }}>
<TYPE.mediumHeader>
{<FormattedPoolTokenAmount tokenAmount={liquidityTokenAmount} />} {token.symbol} Pool Tokens
</TYPE.mediumHeader>
</div>
</AutoRow>
<div style={{ display: 'flex', marginTop: '1rem' }}>
<AutoColumn gap="12px" style={{ flex: '1', marginRight: 12 }}>
<ButtonConfirmed
confirmed={approval === ApprovalState.APPROVED}
disabled={approval !== ApprovalState.NOT_APPROVED}
onClick={approve}
>
{approval === ApprovalState.PENDING
? 'Approving...'
: approval === ApprovalState.APPROVED
? 'Approved'
: 'Approve'}
</ButtonConfirmed>
</AutoColumn>
<AutoColumn gap="12px" style={{ flex: '1' }}>
<ButtonConfirmed
confirmed={isSuccessfullyMigrated}
disabled={
isSuccessfullyMigrated ||
noLiquidityTokens ||
isMigrationPending ||
approval !== ApprovalState.APPROVED ||
confirmingMigration
}
onClick={migrate}
>
{isSuccessfullyMigrated ? 'Success' : isMigrationPending ? 'Migrating...' : 'Migrate'}
</ButtonConfirmed>
</AutoColumn>
</div>
</LightCard>
<TYPE.darkGray style={{ textAlign: 'center' }}>
{'Your ' + token.symbol + ' liquidity will become Uniswap V2 ' + token.symbol + '/ETH liquidity.'}
</TYPE.darkGray>
</AutoColumn>
)
}
export default function MigrateV1Exchange({
history,
match: {
params: { address }
}
}: RouteComponentProps<{ address: string }>) {
const validated = isAddress(address)
const { chainId, account } = useActiveWeb3React()
const exchangeContract = useV1ExchangeContract(validated ? validated : undefined)
const tokenAddress = useSingleCallResult(exchangeContract, 'tokenAddress', undefined, NEVER_RELOAD)?.result?.[0]
const token = useTokenByAddressAndAutomaticallyAdd(tokenAddress)
const liquidityToken: Token | undefined = useMemo(
() => (validated && token ? new Token(chainId, validated, 18, `UNI-V1-${token.symbol}`) : undefined),
[chainId, token, validated]
)
const userLiquidityBalance = useTokenBalance(account, liquidityToken)
const handleBack = useCallback(() => {
history.push('/migrate/v1')
}, [history])
if (!validated) {
console.error('Invalid address in path', address)
return <Redirect to="/migrate/v1" />
}
if (!account) {
return (
<BodyWrapper>
<TYPE.largeHeader>You must connect an account.</TYPE.largeHeader>
</BodyWrapper>
)
}
return (
<BodyWrapper style={{ padding: 24 }}>
<AutoColumn gap="16px">
<AutoRow style={{ alignItems: 'center', justifyContent: 'space-between' }} gap="8px">
<div style={{ cursor: 'pointer' }}>
<ArrowLeft onClick={handleBack} />
</div>
<TYPE.mediumHeader>Migrate {token?.symbol} Pool Tokens</TYPE.mediumHeader>
<div>
<QuestionHelper text="Migrate your liquidity tokens from Uniswap V1 to Uniswap V2." />
</div>
</AutoRow>
{userLiquidityBalance && token ? (
<V1PairMigration liquidityTokenAmount={userLiquidityBalance} token={token} />
) : (
<EmptyState message="Loading..." />
)}
</AutoColumn>
</BodyWrapper>
)
}

View File

@@ -1,40 +1,144 @@
import { JSBI, Token } from '@uniswap/sdk'
import React, { useMemo } from 'react'
import { Fraction, JSBI, Token, TokenAmount } from '@uniswap/sdk'
import React, { useCallback, useContext, useMemo, useState } from 'react'
import { ArrowLeft } from 'react-feather'
import { RouteComponentProps } from 'react-router'
import { useAllV1ExchangeAddresses } from '../../data/V1'
import { ThemeContext } from 'styled-components'
import { ButtonPrimary } from '../../components/Button'
import { AutoColumn } from '../../components/Column'
import { AutoRow } from '../../components/Row'
import { SearchInput } from '../../components/SearchModal/styleds'
import TokenLogo from '../../components/TokenLogo'
import { useAllTokenV1Exchanges } from '../../data/V1'
import { useActiveWeb3React } from '../../hooks'
import { useTokenByAddressAndAutomaticallyAdd } from '../../hooks/Tokens'
import { useWalletModalToggle } from '../../state/application/hooks'
import { useTokenBalances } from '../../state/wallet/hooks'
import { TYPE } from '../../theme'
import { GreyCard } from '../../components/Card'
import { BodyWrapper } from '../AppBody'
import { EmptyState } from './EmptyState'
const PLACEHOLDER_ACCOUNT = (
<div>
<h1>You must connect a wallet to use this tool.</h1>
</div>
)
const POOL_TOKEN_AMOUNT_MIN = new Fraction(JSBI.BigInt(1), JSBI.BigInt(1000000))
/**
* Page component for migrating liquidity from V1
*/
export default function MigrateV1({}: RouteComponentProps) {
export function FormattedPoolTokenAmount({ tokenAmount }: { tokenAmount: TokenAmount }) {
return (
<>
{tokenAmount.equalTo(JSBI.BigInt(0))
? '0'
: tokenAmount.greaterThan(POOL_TOKEN_AMOUNT_MIN)
? tokenAmount.toSignificant(6)
: `<${POOL_TOKEN_AMOUNT_MIN.toSignificant(1)}`}
</>
)
}
export default function MigrateV1({ history }: RouteComponentProps) {
const { account, chainId } = useActiveWeb3React()
const v1ExchangeAddresses = useAllV1ExchangeAddresses()
const allV1Exchanges = useAllTokenV1Exchanges()
const v1ExchangeTokens: Token[] = useMemo(() => {
return v1ExchangeAddresses.map(exchangeAddress => new Token(chainId, exchangeAddress, 18))
}, [chainId, v1ExchangeAddresses])
const v1LiquidityTokens: Token[] = useMemo(() => {
return Object.keys(allV1Exchanges).map(exchangeAddress => new Token(chainId, exchangeAddress, 18))
}, [chainId, allV1Exchanges])
const tokenBalances = useTokenBalances(account, v1ExchangeTokens)
const v1LiquidityBalances = useTokenBalances(account, v1LiquidityTokens)
const unmigratedExchangeAddresses = useMemo(
const [tokenSearch, setTokenSearch] = useState<string>('')
const handleTokenSearchChange = useCallback(e => setTokenSearch(e.target.value), [setTokenSearch])
const searchedToken: Token | undefined = useTokenByAddressAndAutomaticallyAdd(tokenSearch)
const unmigratedLiquidityExchangeAddresses: TokenAmount[] = useMemo(
() =>
Object.keys(tokenBalances).filter(tokenAddress =>
tokenBalances[tokenAddress] ? JSBI.greaterThan(tokenBalances[tokenAddress]?.raw, JSBI.BigInt(0)) : false
),
[tokenBalances]
Object.keys(v1LiquidityBalances)
.filter(tokenAddress =>
v1LiquidityBalances[tokenAddress]
? JSBI.greaterThan(v1LiquidityBalances[tokenAddress]?.raw, JSBI.BigInt(0))
: false
)
.map(tokenAddress => v1LiquidityBalances[tokenAddress])
.sort((a1, a2) => {
if (searchedToken) {
if (allV1Exchanges[a1.token.address].address === searchedToken.address) return -1
if (allV1Exchanges[a2.token.address].address === searchedToken.address) return 1
}
return a1.token.address < a2.token.address ? -1 : 1
}),
[allV1Exchanges, searchedToken, v1LiquidityBalances]
)
if (!account) {
return PLACEHOLDER_ACCOUNT
}
const theme = useContext(ThemeContext)
return <div>{unmigratedExchangeAddresses?.join('\n')}</div>
const toggleWalletModal = useWalletModalToggle()
const handleBackClick = useCallback(() => {
history.push('/pool')
}, [history])
return (
<BodyWrapper style={{ maxWidth: 450, padding: 24 }}>
<AutoColumn gap="24px">
<AutoRow style={{ justifyContent: 'space-between' }}>
<div>
<ArrowLeft style={{ cursor: 'pointer' }} onClick={handleBackClick} />
</div>
<TYPE.largeHeader>Migrate Liquidity</TYPE.largeHeader>
<div></div>
</AutoRow>
<GreyCard>
<TYPE.main style={{ lineHeight: '140%' }}>
For each pool, approve the migration helper and click migrate liquidity. Your liquidity will be withdrawn
from Uniswap V1 and deposited into Uniswap V2.
</TYPE.main>
<TYPE.black padding={'1rem 0 0 0'} style={{ lineHeight: '140%' }}>
If your liquidity does not appear below automatically, you may need to find it by pasting the token address
into the search box below.
</TYPE.black>
</GreyCard>
<AutoRow>
<SearchInput
value={tokenSearch}
onChange={handleTokenSearchChange}
placeholder="Find liquidity by pasting a token address."
/>
</AutoRow>
{unmigratedLiquidityExchangeAddresses.map(poolTokenAmount => (
<div
key={poolTokenAmount.token.address}
style={{ borderRadius: '20px', padding: 16, backgroundColor: theme.bg2 }}
>
<AutoRow style={{ justifyContent: 'space-between' }}>
<AutoRow style={{ justifyContent: 'flex-start', width: 'fit-content' }}>
<TokenLogo size="32px" address={allV1Exchanges[poolTokenAmount.token.address].address} />{' '}
<div style={{ marginLeft: '.75rem' }}>
<TYPE.main fontWeight={600}>
<FormattedPoolTokenAmount tokenAmount={poolTokenAmount} />
</TYPE.main>
<TYPE.main fontWeight={500}>
{allV1Exchanges[poolTokenAmount.token.address].symbol} Pool Tokens
</TYPE.main>
</div>
</AutoRow>
<div>
<ButtonPrimary
onClick={() => {
history.push(`/migrate/v1/${poolTokenAmount.token.address}`)
}}
style={{ padding: '8px 12px', borderRadius: '12px' }}
>
Migrate
</ButtonPrimary>
</div>
</AutoRow>
</div>
))}
{account && unmigratedLiquidityExchangeAddresses.length === 0 ? (
<EmptyState message="No V1 Liquidity found." />
) : null}
{!account ? <ButtonPrimary onClick={toggleWalletModal}>Connect to a wallet</ButtonPrimary> : null}
</AutoColumn>
</BodyWrapper>
)
}

View File

@@ -6,9 +6,9 @@ import { RouteComponentProps } from 'react-router-dom'
import Question from '../../components/QuestionHelper'
import SearchModal from '../../components/SearchModal'
import PositionCard from '../../components/PositionCard'
import { useUserProbablyHasV1Liquidity } from '../../data/V1'
import { useUserHasLiquidityInAllTokens } from '../../data/V1'
import { useTokenBalances } from '../../state/wallet/hooks'
import { Link, TYPE } from '../../theme'
import { StyledInternalLink, TYPE } from '../../theme'
import { Text } from 'rebass'
import { LightCard } from '../../components/Card'
import { RowBetween } from '../../components/Row'
@@ -59,7 +59,7 @@ export default function Pool({ history }: RouteComponentProps) {
return <PositionCardWrapper key={i} dummyPair={pair} />
})
const hasV1Liquidity = useUserProbablyHasV1Liquidity()
const hasV1Liquidity = useUserHasLiquidityInAllTokens()
return (
<AppBody>
@@ -98,19 +98,14 @@ export default function Pool({ history }: RouteComponentProps) {
{!hasV1Liquidity ? (
<>
{filteredExchangeList?.length !== 0 ? `Don't see a pool you joined? ` : 'Already joined a pool? '}{' '}
<Link
id="import-pool-link"
onClick={() => {
history.push('/find')
}}
>
<StyledInternalLink id="import-pool-link" to="/find">
Import it.
</Link>
</StyledInternalLink>
</>
) : (
<Link id="migrate-v1-liquidity-link" href="https://migrate.uniswap.exchange">
<StyledInternalLink id="migrate-v1-liquidity-link" to="/migrate/v1">
Migrate your V1 liquidity.
</Link>
</StyledInternalLink>
)}
</Text>
</AutoColumn>

View File

@@ -15,7 +15,7 @@ import { useActiveWeb3React } from '../../hooks'
import { useToken } from '../../hooks/Tokens'
import { usePairAdder } from '../../state/user/hooks'
import { useTokenBalanceTreatingWETHasETH } from '../../state/wallet/hooks'
import { Link } from '../../theme'
import { StyledInternalLink } from '../../theme'
import AppBody from '../AppBody'
enum Fields {
@@ -119,13 +119,9 @@ export default function PoolFinder({ history }: RouteComponentProps) {
<LightCard padding="45px 10px">
<AutoColumn gap="sm" justify="center">
<Text textAlign="center">Pool found, you dont have liquidity on this pair yet.</Text>
<Link
onClick={() => {
history.push('/add/' + token0Address + '-' + token1Address)
}}
>
<StyledInternalLink to={`/add/${token0Address}-${token1Address}`}>
<Text textAlign="center">Add liquidity to this pair instead.</Text>
</Link>
</StyledInternalLink>
</AutoColumn>
</LightCard>
)
@@ -133,13 +129,7 @@ export default function PoolFinder({ history }: RouteComponentProps) {
<LightCard padding="45px">
<AutoColumn gap="sm" justify="center">
<Text color="">No pool found.</Text>
<Link
onClick={() => {
history.push('/add/' + token0Address + '-' + token1Address)
}}
>
Create pool?
</Link>
<StyledInternalLink to={`/add/${token0Address}-${token1Address}`}>Create pool?</StyledInternalLink>
</AutoColumn>
</LightCard>
) : (

View File

@@ -1,7 +1,7 @@
import { splitSignature } from '@ethersproject/bytes'
import { Contract } from '@ethersproject/contracts'
import { Percent, WETH } from '@uniswap/sdk'
import React, { useContext, useState } from 'react'
import React, { useCallback, useContext, useState } from 'react'
import { ArrowDown, Plus } from 'react-feather'
import ReactGA from 'react-ga'
import { RouteComponentProps } from 'react-router'
@@ -34,6 +34,7 @@ import { useDerivedBurnInfo, useBurnState } from '../../state/burn/hooks'
import AdvancedSwapDetailsDropdown from '../../components/swap/AdvancedSwapDetailsDropdown'
import { Field } from '../../state/burn/actions'
import { useWalletModalToggle } from '../../state/application/hooks'
import { BigNumber } from '@ethersproject/bignumber'
export default function RemoveLiquidity({ match: { params } }: RouteComponentProps<{ tokens: string }>) {
useDefaultsFromURLMatchParams(params)
@@ -51,14 +52,13 @@ export default function RemoveLiquidity({ match: { params } }: RouteComponentPro
const isValid = !error
// modal and loading
const [showConfirm, setShowConfirm] = useState<boolean>(false)
const [showAdvanced, setShowAdvanced] = useState<boolean>(false)
const [showDetailed, setShowDetailed] = useState<boolean>(false)
const [attemptingTxn, setAttemptingTxn] = useState(false) // clicked confirm
const [pendingConfirmation, setPendingConfirmation] = useState(true) // waiting for
// txn values
const [showDetailed, setShowDetailed] = useState<boolean>(false) // toggling detailed view
const [showAdvanced, setShowAdvanced] = useState<boolean>(false) // toggling slippage, deadline, etc. on and off
const [showConfirm, setShowConfirm] = useState<boolean>(false) // show confirmation modal
const [attemptingTxn, setAttemptingTxn] = useState<boolean>(false) // waiting for user confirmaion/rejection
const [txHash, setTxHash] = useState<string>('')
// tx parameters
const [deadline, setDeadline] = useState<number>(DEFAULT_DEADLINE_FROM_NOW)
const [allowedSlippage, setAllowedSlippage] = useState<number>(INITIAL_ALLOWED_SLIPPAGE)
@@ -144,17 +144,9 @@ export default function RemoveLiquidity({ match: { params } }: RouteComponentPro
})
}
function resetModalState() {
setSignatureData(null)
setAttemptingTxn(false)
setPendingConfirmation(true)
}
// tx sending
const addTransaction = useTransactionAdder()
async function onRemove() {
setAttemptingTxn(true)
const router = getRouterContract(chainId, library, account)
const amountsMin = {
@@ -167,13 +159,12 @@ export default function RemoveLiquidity({ match: { params } }: RouteComponentPro
const deadlineFromNow = Math.ceil(Date.now() / 1000) + deadline
let estimate, method: Function, args: Array<string | string[] | number | boolean>
let methodNames: string[], args: Array<string | string[] | number | boolean>
// we have approval, use normal remove liquidity
if (approval === ApprovalState.APPROVED) {
// removeLiquidityETH
if (oneTokenIsETH) {
estimate = router.estimateGas.removeLiquidityETH
method = router.removeLiquidityETH
methodNames = ['removeLiquidityETH', 'removeLiquidityETHSupportingFeeOnTransferTokens']
args = [
tokens[tokenBIsETH ? Field.TOKEN_A : Field.TOKEN_B].address,
parsedAmounts[Field.LIQUIDITY].raw.toString(),
@@ -185,8 +176,7 @@ export default function RemoveLiquidity({ match: { params } }: RouteComponentPro
}
// removeLiquidity
else {
estimate = router.estimateGas.removeLiquidity
method = router.removeLiquidity
methodNames = ['removeLiquidity']
args = [
tokens[Field.TOKEN_A].address,
tokens[Field.TOKEN_B].address,
@@ -202,8 +192,7 @@ export default function RemoveLiquidity({ match: { params } }: RouteComponentPro
else if (signatureData !== null) {
// removeLiquidityETHWithPermit
if (oneTokenIsETH) {
estimate = router.estimateGas.removeLiquidityETHWithPermit
method = router.removeLiquidityETHWithPermit
methodNames = ['removeLiquidityETHWithPermit', 'removeLiquidityETHWithPermitSupportingFeeOnTransferTokens']
args = [
tokens[tokenBIsETH ? Field.TOKEN_A : Field.TOKEN_B].address,
parsedAmounts[Field.LIQUIDITY].raw.toString(),
@@ -219,8 +208,7 @@ export default function RemoveLiquidity({ match: { params } }: RouteComponentPro
}
// removeLiquidityETHWithPermit
else {
estimate = router.estimateGas.removeLiquidityWithPermit
method = router.removeLiquidityWithPermit
methodNames = ['removeLiquidityWithPermit']
args = [
tokens[Field.TOKEN_A].address,
tokens[Field.TOKEN_B].address,
@@ -236,14 +224,37 @@ export default function RemoveLiquidity({ match: { params } }: RouteComponentPro
]
}
} else {
console.error('Attempting to confirm without approval or a signature.')
console.error('Attempting to confirm without approval or a signature. Please contact support.')
}
await estimate(...args)
.then(estimatedGasLimit =>
method(...args, {
gasLimit: calculateGasMargin(estimatedGasLimit)
}).then(response => {
const safeGasEstimates = await Promise.all(
methodNames.map(methodName =>
router.estimateGas[methodName](...args)
.then(calculateGasMargin)
.catch(error => {
console.error(`estimateGas failed for ${methodName}`, error)
})
)
)
const indexOfSuccessfulEstimation = safeGasEstimates.findIndex(safeGasEstimate =>
BigNumber.isBigNumber(safeGasEstimate)
)
// all estimations failed...
if (indexOfSuccessfulEstimation === -1) {
console.error('This transaction would fail. Please contact support.')
} else {
const methodName = methodNames[indexOfSuccessfulEstimation]
const safeGasEstimate = safeGasEstimates[indexOfSuccessfulEstimation]
setAttemptingTxn(true)
await router[methodName](...args, {
gasLimit: safeGasEstimate
})
.then(response => {
setAttemptingTxn(false)
addTransaction(response, {
summary:
'Remove ' +
@@ -257,7 +268,6 @@ export default function RemoveLiquidity({ match: { params } }: RouteComponentPro
})
setTxHash(response.hash)
setPendingConfirmation(false)
ReactGA.event({
category: 'Liquidity',
@@ -265,13 +275,14 @@ export default function RemoveLiquidity({ match: { params } }: RouteComponentPro
label: [tokens[Field.TOKEN_A]?.symbol, tokens[Field.TOKEN_B]?.symbol].join('/')
})
})
)
.catch(e => {
console.error(e)
resetModalState()
setShowConfirm(false)
setShowAdvanced(false)
})
.catch(error => {
setAttemptingTxn(false)
// we only care if the error is something _other_ than the user rejected the tx
if (error?.code !== 4001) {
console.error(error)
}
})
}
}
function modalHeader() {
@@ -384,6 +395,13 @@ export default function RemoveLiquidity({ match: { params } }: RouteComponentPro
tokens[Field.TOKEN_A]?.symbol
} and ${parsedAmounts[Field.TOKEN_B]?.toSignificant(6)} ${tokens[Field.TOKEN_B]?.symbol}`
const liquidityPercentChangeCallback = useCallback(
(value: number) => {
onUserInput(Field.LIQUIDITY_PERCENT, value.toString())
},
[onUserInput]
)
return (
<>
<AppBody>
@@ -391,12 +409,15 @@ export default function RemoveLiquidity({ match: { params } }: RouteComponentPro
<ConfirmationModal
isOpen={showConfirm}
onDismiss={() => {
resetModalState()
setShowConfirm(false)
setShowAdvanced(false)
setSignatureData(null) // important that we clear signature data to avoid bad sigs
// if there was a tx hash, we want to clear the input
if (txHash) {
onUserInput(Field.LIQUIDITY_PERCENT, '0')
}
setTxHash('')
}}
attemptingTxn={attemptingTxn}
pendingConfirmation={pendingConfirmation}
hash={txHash ? txHash : ''}
topContent={modalHeader}
bottomContent={modalBottom}
@@ -426,9 +447,7 @@ export default function RemoveLiquidity({ match: { params } }: RouteComponentPro
<>
<Slider
value={Number.parseInt(parsedAmounts[Field.LIQUIDITY_PERCENT].toFixed(0))}
onChange={value => {
onUserInput(Field.LIQUIDITY_PERCENT, value.toString())
}}
onChange={liquidityPercentChangeCallback}
/>
<RowBetween>
<MaxButton onClick={() => onUserInput(Field.LIQUIDITY_PERCENT, '25')} width="20%">

View File

@@ -74,13 +74,12 @@ export default function Send({ location: { search } }: RouteComponentProps) {
const dependentField: Field = independentField === Field.INPUT ? Field.OUTPUT : Field.INPUT
// modal and loading
const [showConfirm, setShowConfirm] = useState<boolean>(false)
const [showAdvanced, setShowAdvanced] = useState<boolean>(false)
const [attemptingTxn, setAttemptingTxn] = useState<boolean>(false) // clicked confirmed
const [pendingConfirmation, setPendingConfirmation] = useState<boolean>(true) // waiting for user confirmation
// txn values
const [showAdvanced, setShowAdvanced] = useState<boolean>(false) // toggling slippage, deadline, etc. on and off
const [showConfirm, setShowConfirm] = useState<boolean>(false) // show confirmation modal
const [attemptingTxn, setAttemptingTxn] = useState<boolean>(false) // waiting for user confirmaion/rejection
const [txHash, setTxHash] = useState<string>('')
// tx parameters
const [deadline, setDeadline] = useState<number>(DEFAULT_DEADLINE_FROM_NOW)
const [allowedSlippage, setAllowedSlippage] = useState<number>(INITIAL_ALLOWED_SLIPPAGE)
@@ -95,6 +94,16 @@ export default function Send({ location: { search } }: RouteComponentProps) {
// check whether the user has approved the router on the input token
const [approval, approveCallback] = useApproveCallbackFromTrade(bestTrade, allowedSlippage)
// check if user has gone through approval process, used to show two step buttons, reset on token change
const [approvalSubmitted, setApprovalSubmitted] = useState<boolean>(false)
// mark when a user has submitted an approval, reset onTokenSelection for input field
useEffect(() => {
if (approval === ApprovalState.PENDING) {
setApprovalSubmitted(true)
}
}, [approval, approvalSubmitted])
const formattedAmounts = {
[independentField]: typedValue,
[dependentField]: parsedAmounts[dependentField] ? parsedAmounts[dependentField].toSignificant(6) : ''
@@ -127,17 +136,6 @@ export default function Send({ location: { search } }: RouteComponentProps) {
const atMaxAmountInput: boolean =
!!maxAmountInput && !!parsedAmounts[Field.INPUT] ? maxAmountInput.equalTo(parsedAmounts[Field.INPUT]) : undefined
// reset modal state when closed
function resetModal() {
// clear input if txn submitted
if (!pendingConfirmation) {
onUserInput(Field.INPUT, '')
}
setPendingConfirmation(true)
setAttemptingTxn(false)
setShowAdvanced(false)
}
const swapCallback = useSwapCallback(bestTrade, allowedSlippage, deadline, recipient)
function onSwap() {
@@ -146,16 +144,24 @@ export default function Send({ location: { search } }: RouteComponentProps) {
}
setAttemptingTxn(true)
swapCallback().then(hash => {
setTxHash(hash)
setPendingConfirmation(false)
swapCallback()
.then(hash => {
setAttemptingTxn(false)
setTxHash(hash)
ReactGA.event({
category: 'Send',
action: recipient === account ? 'Swap w/o Send' : 'Swap w/ Send',
label: [bestTrade.inputAmount.token.symbol, bestTrade.outputAmount.token.symbol].join(';')
ReactGA.event({
category: 'Send',
action: recipient === account ? 'Swap w/o Send' : 'Swap w/ Send',
label: [bestTrade.inputAmount.token.symbol, bestTrade.outputAmount.token.symbol].join(';')
})
})
.catch(error => {
setAttemptingTxn(false)
// we only care if the error is something _other_ than the user rejected the tx
if (error?.code !== 4001) {
console.error(error)
}
})
})
}
const sendCallback = useSendCallback(parsedAmounts?.[Field.INPUT], recipient)
@@ -163,16 +169,19 @@ export default function Send({ location: { search } }: RouteComponentProps) {
async function onSend() {
setAttemptingTxn(true)
sendCallback()
.then(hash => {
setAttemptingTxn(false)
setTxHash(hash)
ReactGA.event({ category: 'Send', action: 'Send', label: tokens[Field.INPUT]?.symbol })
setPendingConfirmation(false)
})
.catch(() => {
resetModal()
setShowConfirm(false)
.catch(error => {
setAttemptingTxn(false)
// we only care if the error is something _other_ than the user rejected the tx
if (error?.code !== 4001) {
console.error(error)
}
})
}
@@ -181,6 +190,13 @@ export default function Send({ location: { search } }: RouteComponentProps) {
// warnings on slippage
const severity = !sendingWithSwap ? 0 : warningSeverity(priceImpactWithoutFee)
// show approval buttons when: no errors on input, not approved or pending, or has been approved in this session
const showApproveFlow =
((sendingWithSwap && isSwapValid) || (!sendingWithSwap && isSendValid)) &&
(approval === ApprovalState.NOT_APPROVED ||
approval === ApprovalState.PENDING ||
(approvalSubmitted && approval === ApprovalState.APPROVED))
function modalHeader() {
if (!sendingWithSwap) {
return <TransferModalHeader amount={parsedAmounts?.[Field.INPUT]} ENSName={ENS} recipient={recipient} />
@@ -289,11 +305,13 @@ export default function Send({ location: { search } }: RouteComponentProps) {
isOpen={showConfirm}
title={sendingWithSwap ? 'Confirm swap and send' : 'Confirm Send'}
onDismiss={() => {
resetModal()
setShowConfirm(false)
if (txHash) {
onUserInput(Field.INPUT, '')
}
setTxHash('')
}}
attemptingTxn={attemptingTxn}
pendingConfirmation={pendingConfirmation}
hash={txHash}
topContent={modalHeader}
bottomContent={modalBottom}
@@ -363,11 +381,13 @@ export default function Send({ location: { search } }: RouteComponentProps) {
onMax={() => {
maxAmountInput && onUserInput(Field.INPUT, maxAmountInput.toExact())
}}
onTokenSelection={address => onTokenSelection(Field.INPUT, address)}
onTokenSelection={address => {
setApprovalSubmitted(false)
onTokenSelection(Field.INPUT, address)
}}
otherSelectedTokenAddress={tokens[Field.OUTPUT]?.address}
id="swap-currency-input"
/>
{sendingWithSwap ? (
<ColumnCenter>
<RowBetween padding="0 1rem 0 12px">
@@ -431,7 +451,7 @@ export default function Send({ location: { search } }: RouteComponentProps) {
/>
</AutoColumn>
{!noRoute && tokens[Field.OUTPUT] && tokens[Field.INPUT] && (
<Card padding={'.25rem 1.25rem 0 .75rem'} borderRadius={'20px'}>
<Card padding={'.25rem .75rem 0 .75rem'} borderRadius={'20px'}>
<AutoColumn gap="4px">
<RowBetween align="center">
<Text fontWeight={500} fontSize={14} color={theme.text2}>
@@ -471,14 +491,36 @@ export default function Send({ location: { search } }: RouteComponentProps) {
<GreyCard style={{ textAlign: 'center' }}>
<TYPE.main mb="4px">Insufficient liquidity for this trade.</TYPE.main>
</GreyCard>
) : approval === ApprovalState.NOT_APPROVED || approval === ApprovalState.PENDING ? (
<ButtonLight onClick={approveCallback} disabled={approval === ApprovalState.PENDING}>
{approval === ApprovalState.PENDING ? (
<Dots>Approving {tokens[Field.INPUT]?.symbol}</Dots>
) : (
'Approve ' + tokens[Field.INPUT]?.symbol
)}
</ButtonLight>
) : showApproveFlow ? (
<RowBetween>
<ButtonPrimary
onClick={approveCallback}
disabled={approval !== ApprovalState.NOT_APPROVED || approvalSubmitted}
width="48%"
altDisbaledStyle={approval === ApprovalState.PENDING} // show solid button while waiting
>
{approval === ApprovalState.PENDING ? (
<Dots>Approving</Dots>
) : approvalSubmitted && approval === ApprovalState.APPROVED ? (
'Approved'
) : (
'Approve ' + tokens[Field.INPUT]?.symbol
)}
</ButtonPrimary>
<ButtonError
onClick={() => {
setShowConfirm(true)
}}
width="48%"
id="send-button"
disabled={approval !== ApprovalState.APPROVED}
error={sendingWithSwap && isSwapValid && severity > 2}
>
<Text fontSize={16} fontWeight={500}>
{`Send${severity > 2 ? ' Anyway' : ''}`}
</Text>
</ButtonError>
</RowBetween>
) : (
<ButtonError
onClick={() => {

View File

@@ -1,11 +1,11 @@
import { JSBI, TokenAmount, WETH } from '@uniswap/sdk'
import React, { useContext, useState } from 'react'
import React, { useContext, useState, useEffect } from 'react'
import { ArrowDown } from 'react-feather'
import ReactGA from 'react-ga'
import { RouteComponentProps } from 'react-router-dom'
import { Text } from 'rebass'
import { ThemeContext } from 'styled-components'
import { ButtonError, ButtonLight } from '../../components/Button'
import { ButtonError, ButtonLight, ButtonPrimary } from '../../components/Button'
import Card, { GreyCard } from '../../components/Card'
import { AutoColumn } from '../../components/Column'
import ConfirmationModal from '../../components/ConfirmationModal'
@@ -55,13 +55,12 @@ export default function Swap({ location: { search } }: RouteComponentProps) {
const dependentField: Field = independentField === Field.INPUT ? Field.OUTPUT : Field.INPUT
// modal and loading
const [showConfirm, setShowConfirm] = useState<boolean>(false)
const [showAdvanced, setShowAdvanced] = useState<boolean>(false)
const [attemptingTxn, setAttemptingTxn] = useState<boolean>(false) // clicked confirmed
const [pendingConfirmation, setPendingConfirmation] = useState<boolean>(true) // waiting for user confirmation
// txn values
const [showAdvanced, setShowAdvanced] = useState<boolean>(false) // toggling slippage, deadline, etc. on and off
const [showConfirm, setShowConfirm] = useState<boolean>(false) // show confirmation modal
const [attemptingTxn, setAttemptingTxn] = useState<boolean>(false) // waiting for user confirmaion/rejection
const [txHash, setTxHash] = useState<string>('')
// tx parameters
const [deadline, setDeadline] = useState<number>(DEFAULT_DEADLINE_FROM_NOW)
const [allowedSlippage, setAllowedSlippage] = useState<number>(INITIAL_ALLOWED_SLIPPAGE)
@@ -81,6 +80,23 @@ export default function Swap({ location: { search } }: RouteComponentProps) {
// check whether the user has approved the router on the input token
const [approval, approveCallback] = useApproveCallbackFromTrade(bestTrade, allowedSlippage)
// check if user has gone through approval process, used to show two step buttons, reset on token change
const [approvalSubmitted, setApprovalSubmitted] = useState<boolean>(false)
// show approve flow when: no error on inputs, not approved or pending, or approved in current session
const showApproveFlow =
!error &&
(approval === ApprovalState.NOT_APPROVED ||
approval === ApprovalState.PENDING ||
(approvalSubmitted && approval === ApprovalState.APPROVED))
// mark when a user has submitted an approval, reset onTokenSelection for input field
useEffect(() => {
if (approval === ApprovalState.PENDING) {
setApprovalSubmitted(true)
}
}, [approval, approvalSubmitted])
const maxAmountInput: TokenAmount =
!!tokenBalances[Field.INPUT] &&
!!tokens[Field.INPUT] &&
@@ -97,17 +113,6 @@ export default function Swap({ location: { search } }: RouteComponentProps) {
const slippageAdjustedAmounts = computeSlippageAdjustedAmounts(bestTrade, allowedSlippage)
// reset modal state when closed
function resetModal() {
// clear input if txn submitted
if (!pendingConfirmation) {
onUserInput(Field.INPUT, '')
}
setPendingConfirmation(true)
setAttemptingTxn(false)
setShowAdvanced(false)
}
// the callback to execute the swap
const swapCallback = useSwapCallback(bestTrade, allowedSlippage, deadline)
@@ -119,16 +124,24 @@ export default function Swap({ location: { search } }: RouteComponentProps) {
}
setAttemptingTxn(true)
swapCallback().then(hash => {
setTxHash(hash)
setPendingConfirmation(false)
swapCallback()
.then(hash => {
setAttemptingTxn(false)
setTxHash(hash)
ReactGA.event({
category: 'Swap',
action: 'Swap w/o Send',
label: [bestTrade.inputAmount.token.symbol, bestTrade.outputAmount.token.symbol].join('/')
ReactGA.event({
category: 'Swap',
action: 'Swap w/o Send',
label: [bestTrade.inputAmount.token.symbol, bestTrade.outputAmount.token.symbol].join('/')
})
})
.catch(error => {
setAttemptingTxn(false)
// we only care if the error is something _other_ than the user rejected the tx
if (error?.code !== 4001) {
console.error(error)
}
})
})
}
// errors
@@ -140,11 +153,11 @@ export default function Swap({ location: { search } }: RouteComponentProps) {
function modalHeader() {
return (
<SwapModalHeader
independentField={independentField}
priceImpactSeverity={priceImpactSeverity}
tokens={tokens}
formattedAmounts={formattedAmounts}
slippageAdjustedAmounts={slippageAdjustedAmounts}
priceImpactSeverity={priceImpactSeverity}
independentField={independentField}
/>
)
}
@@ -180,11 +193,14 @@ export default function Swap({ location: { search } }: RouteComponentProps) {
isOpen={showConfirm}
title="Confirm Swap"
onDismiss={() => {
resetModal()
setShowConfirm(false)
// if there was a tx hash, we want to clear the input
if (txHash) {
onUserInput(Field.INPUT, '')
}
setTxHash('')
}}
attemptingTxn={attemptingTxn}
pendingConfirmation={pendingConfirmation}
hash={txHash}
topContent={modalHeader}
bottomContent={modalBottom}
@@ -203,7 +219,10 @@ export default function Swap({ location: { search } }: RouteComponentProps) {
onMax={() => {
maxAmountInput && onUserInput(Field.INPUT, maxAmountInput.toExact())
}}
onTokenSelection={address => onTokenSelection(Field.INPUT, address)}
onTokenSelection={address => {
setApprovalSubmitted(false) // reset 2 step UI for approvals
onTokenSelection(Field.INPUT, address)
}}
otherSelectedTokenAddress={tokens[Field.OUTPUT]?.address}
id="swap-currency-input"
/>
@@ -213,13 +232,15 @@ export default function Swap({ location: { search } }: RouteComponentProps) {
<ArrowWrapper>
<ArrowDown
size="16"
onClick={onSwitchTokens}
onClick={() => {
setApprovalSubmitted(false) // reset 2 step UI for approvals
onSwitchTokens()
}}
color={tokens[Field.INPUT] && tokens[Field.OUTPUT] ? theme.primary1 : theme.text2}
/>
</ArrowWrapper>
</AutoColumn>
</CursorPointer>
<CurrencyInputPanel
field={Field.OUTPUT}
value={formattedAmounts[Field.OUTPUT]}
@@ -235,7 +256,7 @@ export default function Swap({ location: { search } }: RouteComponentProps) {
</>
{!noRoute && tokens[Field.OUTPUT] && tokens[Field.INPUT] && (
<Card padding={'.25rem 1.25rem 0 .75rem'} borderRadius={'20px'}>
<Card padding={'.25rem .75rem 0 .75rem'} borderRadius={'20px'}>
<AutoColumn gap="4px">
<RowBetween align="center">
<Text fontWeight={500} fontSize={14} color={theme.text2}>
@@ -269,14 +290,36 @@ export default function Swap({ location: { search } }: RouteComponentProps) {
<GreyCard style={{ textAlign: 'center' }}>
<TYPE.main mb="4px">Insufficient liquidity for this trade.</TYPE.main>
</GreyCard>
) : approval === ApprovalState.NOT_APPROVED || approval === ApprovalState.PENDING ? (
<ButtonLight onClick={approveCallback} disabled={approval === ApprovalState.PENDING}>
{approval === ApprovalState.PENDING ? (
<Dots>Approving {tokens[Field.INPUT]?.symbol}</Dots>
) : (
'Approve ' + tokens[Field.INPUT]?.symbol
)}
</ButtonLight>
) : showApproveFlow ? (
<RowBetween>
<ButtonPrimary
onClick={approveCallback}
disabled={approval !== ApprovalState.NOT_APPROVED || approvalSubmitted}
width="48%"
altDisbaledStyle={approval === ApprovalState.PENDING} // show solid button while waiting
>
{approval === ApprovalState.PENDING ? (
<Dots>Approving</Dots>
) : approvalSubmitted && approval === ApprovalState.APPROVED ? (
'Approved'
) : (
'Approve ' + tokens[Field.INPUT]?.symbol
)}
</ButtonPrimary>
<ButtonError
onClick={() => {
setShowConfirm(true)
}}
width="48%"
id="swap-button"
disabled={!isValid || approval !== ApprovalState.APPROVED}
error={isValid && priceImpactSeverity > 2}
>
<Text fontSize={16} fontWeight={500}>
{`Swap${priceImpactSeverity > 2 ? ' Anyway' : ''}`}
</Text>
</ButtonError>
</RowBetween>
) : (
<ButtonError
onClick={() => {

View File

@@ -23,5 +23,5 @@ export type PopupContent =
export const updateBlockNumber = createAction<{ chainId: number; blockNumber: number }>('updateBlockNumber')
export const toggleWalletModal = createAction<void>('toggleWalletModal')
export const addPopup = createAction<{ content: PopupContent }>('addPopup')
export const addPopup = createAction<{ key?: string; content: PopupContent }>('addPopup')
export const removePopup = createAction<{ key: string }>('removePopup')

View File

@@ -20,12 +20,12 @@ export function useWalletModalToggle(): () => void {
}
// returns a function that allows adding a popup
export function useAddPopup(): (content: PopupContent) => void {
export function useAddPopup(): (content: PopupContent, key?: string) => void {
const dispatch = useDispatch()
return useCallback(
(content: PopupContent) => {
dispatch(addPopup({ content }))
(content: PopupContent, key?: string) => {
dispatch(addPopup({ content, key }))
},
[dispatch]
)

View File

@@ -28,9 +28,10 @@ export default createReducer(initialState, builder =>
.addCase(toggleWalletModal, state => {
state.walletModalOpen = !state.walletModalOpen
})
.addCase(addPopup, (state, { payload: { content } }) => {
.addCase(addPopup, (state, { payload: { content, key } }) => {
if (key && state.popupList.some(popup => popup.key === key)) return
state.popupList.push({
key: nanoid(),
key: key || nanoid(),
show: true,
content
})

View File

@@ -1,4 +1,4 @@
import { useEffect, useState } from 'react'
import { useCallback, useEffect, useState } from 'react'
import { useActiveWeb3React } from '../../hooks'
import useDebounce from '../../hooks/useDebounce'
import useIsWindowVisible from '../../hooks/useIsWindowVisible'
@@ -10,41 +10,48 @@ export default function Updater() {
const dispatch = useDispatch()
const windowVisible = useIsWindowVisible()
const [maxBlockNumber, setMaxBlockNumber] = useState<number | null>(null)
// because blocks arrive in bunches with longer polling periods, we just want
// to process the latest one.
const debouncedMaxBlockNumber = useDebounce<number | null>(maxBlockNumber, 100)
// update block number
useEffect(() => {
if (!library || !chainId) return
const [state, setState] = useState<{ chainId: number | undefined; blockNumber: number | null }>({
chainId,
blockNumber: null
})
const blockListener = (blockNumber: number) => {
setMaxBlockNumber(maxBlockNumber => {
if (typeof maxBlockNumber !== 'number') return blockNumber
return Math.max(maxBlockNumber, blockNumber)
const blockNumberCallback = useCallback(
(blockNumber: number) => {
setState(state => {
if (chainId === state.chainId) {
if (typeof state.blockNumber !== 'number') return { chainId, blockNumber }
return { chainId, blockNumber: Math.max(blockNumber, state.blockNumber) }
}
return state
})
}
},
[chainId, setState]
)
setMaxBlockNumber(null)
// attach/detach listeners
useEffect(() => {
if (!library || !chainId || !windowVisible) return
setState({ chainId, blockNumber: null })
library
.getBlockNumber()
.then(blockNumber => dispatch(updateBlockNumber({ chainId, blockNumber })))
.catch(error => console.error(`Failed to get block number for chainId ${chainId}`, error))
.then(blockNumberCallback)
.catch(error => console.error(`Failed to get block number for chainId: ${chainId}`, error))
library.on('block', blockListener)
library.on('block', blockNumberCallback)
return () => {
library.removeListener('block', blockListener)
library.removeListener('block', blockNumberCallback)
}
}, [dispatch, chainId, library])
}, [dispatch, chainId, library, blockNumberCallback, windowVisible])
const debouncedState = useDebounce(state, 100)
useEffect(() => {
if (!chainId || !debouncedMaxBlockNumber) return
if (windowVisible) {
dispatch(updateBlockNumber({ chainId, blockNumber: debouncedMaxBlockNumber }))
}
}, [chainId, debouncedMaxBlockNumber, windowVisible, dispatch])
if (!debouncedState.chainId || !debouncedState.blockNumber || !windowVisible) return
dispatch(updateBlockNumber({ chainId: debouncedState.chainId, blockNumber: debouncedState.blockNumber }))
}, [windowVisible, dispatch, debouncedState.blockNumber, debouncedState.chainId])
return null
}

View File

@@ -0,0 +1,57 @@
import { parseCallKey, toCallKey } from './actions'
describe('actions', () => {
describe('#parseCallKey', () => {
it('does not throw for invalid address', () => {
expect(parseCallKey('0x-0x')).toEqual({ address: '0x', callData: '0x' })
})
it('does not throw for invalid calldata', () => {
expect(parseCallKey('0x6b175474e89094c44da98b954eedeac495271d0f-abc')).toEqual({
address: '0x6b175474e89094c44da98b954eedeac495271d0f',
callData: 'abc'
})
})
it('throws for invalid format', () => {
expect(() => parseCallKey('abc')).toThrow('Invalid call key: abc')
})
it('throws for uppercase calldata', () => {
expect(parseCallKey('0x6b175474e89094c44da98b954eedeac495271d0f-0xabcD')).toEqual({
address: '0x6b175474e89094c44da98b954eedeac495271d0f',
callData: '0xabcD'
})
})
it('parses pieces into address', () => {
expect(parseCallKey('0x6b175474e89094c44da98b954eedeac495271d0f-0xabcd')).toEqual({
address: '0x6b175474e89094c44da98b954eedeac495271d0f',
callData: '0xabcd'
})
})
})
describe('#toCallKey', () => {
it('throws for invalid address', () => {
expect(() => toCallKey({ callData: '0x', address: '0x' })).toThrow('Invalid address: 0x')
})
it('throws for invalid calldata', () => {
expect(() =>
toCallKey({
address: '0x6b175474e89094c44da98b954eedeac495271d0f',
callData: 'abc'
})
).toThrow('Invalid hex: abc')
})
it('throws for uppercase hex', () => {
expect(() =>
toCallKey({
address: '0x6b175474e89094c44da98b954eedeac495271d0f',
callData: '0xabcD'
})
).toThrow('Invalid hex: 0xabcD')
})
it('concatenates address to data', () => {
expect(toCallKey({ address: '0x6b175474e89094c44da98b954eedeac495271d0f', callData: '0xabcd' })).toEqual(
'0x6b175474e89094c44da98b954eedeac495271d0f-0xabcd'
)
})
})
})

View File

@@ -1,12 +1,19 @@
import { createAction } from '@reduxjs/toolkit'
import { isAddress } from '../../utils'
export interface Call {
address: string
callData: string
}
const ADDRESS_REGEX = /^0x[a-fA-F0-9]{40}$/
const LOWER_HEX_REGEX = /^0x[a-f0-9]*$/
export function toCallKey(call: Call): string {
if (!ADDRESS_REGEX.test(call.address)) {
throw new Error(`Invalid address: ${call.address}`)
}
if (!LOWER_HEX_REGEX.test(call.callData)) {
throw new Error(`Invalid hex: ${call.callData}`)
}
return `${call.address}-${call.callData}`
}
@@ -15,15 +22,6 @@ export function parseCallKey(callKey: string): Call {
if (pcs.length !== 2) {
throw new Error(`Invalid call key: ${callKey}`)
}
const addr = isAddress(pcs[0])
if (!addr) {
throw new Error(`Invalid address: ${pcs[0]}`)
}
if (!pcs[1].match(/^0x[a-fA-F0-9]*$/)) {
throw new Error(`Invalid hex: ${pcs[1]}`)
}
return {
address: pcs[0],
callData: pcs[1]

View File

@@ -121,22 +121,38 @@ const INVALID_CALL_STATE: CallState = { valid: false, result: undefined, loading
const LOADING_CALL_STATE: CallState = { valid: true, result: undefined, loading: true, syncing: true, error: false }
function toCallState(
result: CallResult | undefined,
callResult: CallResult | undefined,
contractInterface: Interface | undefined,
fragment: FunctionFragment | undefined,
latestBlockNumber: number | undefined
): CallState {
if (!result) return INVALID_CALL_STATE
const { valid, data, blockNumber } = result
if (!callResult) return INVALID_CALL_STATE
const { valid, data, blockNumber } = callResult
if (!valid) return INVALID_CALL_STATE
if (valid && !blockNumber) return LOADING_CALL_STATE
if (!contractInterface || !fragment || !latestBlockNumber) return LOADING_CALL_STATE
const success = data && data.length > 2
const syncing = (blockNumber ?? 0) < latestBlockNumber
let result: Result | undefined = undefined
if (success && data) {
try {
result = contractInterface.decodeFunctionResult(fragment, data)
} catch (error) {
console.debug('Result data parsing failed', fragment, data)
return {
valid: true,
loading: false,
error: true,
syncing,
result
}
}
}
return {
valid: true,
loading: false,
syncing: (blockNumber ?? 0) < latestBlockNumber,
result: success && data ? contractInterface.decodeFunctionResult(fragment, data) : undefined,
syncing,
result: result,
error: !success
}
}

View File

@@ -2,6 +2,8 @@ import { addMulticallListeners, removeMulticallListeners, updateMulticallResults
import reducer, { MulticallState } from './reducer'
import { Store, createStore } from '@reduxjs/toolkit'
const DAI_ADDRESS = '0x6b175474e89094c44da98b954eedeac495271d0f'
describe('multicall reducer', () => {
let store: Store<MulticallState>
beforeEach(() => {
@@ -20,7 +22,7 @@ describe('multicall reducer', () => {
chainId: 1,
calls: [
{
address: '0x',
address: DAI_ADDRESS,
callData: '0x'
}
]
@@ -29,7 +31,7 @@ describe('multicall reducer', () => {
expect(store.getState()).toEqual({
callListeners: {
[1]: {
'0x-0x': {
[`${DAI_ADDRESS}-0x`]: {
[1]: 1
}
}
@@ -45,7 +47,7 @@ describe('multicall reducer', () => {
removeMulticallListeners({
calls: [
{
address: '0x',
address: DAI_ADDRESS,
callData: '0x'
}
],
@@ -60,7 +62,7 @@ describe('multicall reducer', () => {
chainId: 1,
calls: [
{
address: '0x',
address: DAI_ADDRESS,
callData: '0x'
}
]
@@ -70,14 +72,17 @@ describe('multicall reducer', () => {
removeMulticallListeners({
calls: [
{
address: '0x',
address: DAI_ADDRESS,
callData: '0x'
}
],
chainId: 1
})
)
expect(store.getState()).toEqual({ callResults: {}, callListeners: { [1]: { '0x-0x': {} } } })
expect(store.getState()).toEqual({
callResults: {},
callListeners: { [1]: { [`${DAI_ADDRESS}-0x`]: {} } }
})
})
})

View File

@@ -42,6 +42,14 @@ export function useAllTransactions(): { [txHash: string]: TransactionDetails } {
return state[chainId ?? -1] ?? {}
}
export function useIsTransactionPending(transactionHash?: string): boolean {
const transactions = useAllTransactions()
if (!transactionHash || !transactions[transactionHash]) return false
return !transactions[transactionHash].receipt
}
// returns whether a token has a pending approval transaction
export function useHasPendingApproval(tokenAddress?: string): boolean {
const allTransactions = useAllTransactions()

View File

@@ -44,20 +44,17 @@ export default function Updater() {
}
})
)
// add success or failure popup
if (receipt.status === 1) {
addPopup({
addPopup(
{
txn: {
hash,
success: true,
success: receipt.status === 1,
summary: allTransactions[hash]?.summary
}
})
} else {
addPopup({
txn: { hash, success: false, summary: allTransactions[hash]?.summary }
})
}
},
hash
)
}
})
.catch(error => {

View File

@@ -1,9 +1,10 @@
import { ChainId, JSBI, Pair, Token, TokenAmount, WETH } from '@uniswap/sdk'
import { useActiveWeb3React } from '../../hooks'
import { ChainId, JSBI, Pair, Token, TokenAmount } from '@uniswap/sdk'
import flatMap from 'lodash.flatmap'
import { useCallback, useMemo } from 'react'
import { shallowEqual, useDispatch, useSelector } from 'react-redux'
import { useActiveWeb3React } from '../../hooks'
import { useAllTokens } from '../../hooks/Tokens'
import { getTokenInfoWithFallback, isAddress } from '../../utils'
import { AppDispatch, AppState } from '../index'
import {
addSerializedPair,
@@ -14,7 +15,7 @@ import {
SerializedToken,
updateUserDarkMode
} from './actions'
import flatMap from 'lodash.flatmap'
import { BASES_TO_TRACK_LIQUIDITY_FOR, DUMMY_PAIRS_TO_PIN } from '../../constants'
function serializeToken(token: Token): SerializedToken {
return {
@@ -62,26 +63,6 @@ export function useDarkModeManager(): [boolean, () => void] {
return [darkMode, toggleSetDarkMode]
}
export function useFetchTokenByAddress(): (address: string) => Promise<Token | null> {
const { library, chainId } = useActiveWeb3React()
return useCallback(
async (address: string): Promise<Token | null> => {
if (!library || !chainId) return null
const validatedAddress = isAddress(address)
if (!validatedAddress) return null
const { name, symbol, decimals } = await getTokenInfoWithFallback(validatedAddress, library)
if (decimals === null) {
return null
} else {
return new Token(chainId, validatedAddress, decimals, symbol, name)
}
},
[library, chainId]
)
}
export function useAddUserToken(): (token: Token) => void {
const dispatch = useDispatch<AppDispatch>()
return useCallback(
@@ -154,16 +135,14 @@ export function useTokenWarningDismissal(chainId?: number, token?: Token): [bool
}, [chainId, token, dismissalState, dispatch])
}
const bases = [
...Object.values(WETH),
new Token(ChainId.MAINNET, '0x6B175474E89094C44Da98b954EedeAC495271d0F', 18, 'DAI', 'Dai Stablecoin'),
new Token(ChainId.MAINNET, '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', 6, 'USDC', 'USD//C')
]
export function useAllDummyPairs(): Pair[] {
const { chainId } = useActiveWeb3React()
const tokens = useAllTokens()
// pinned pairs
const pinnedPairs = useMemo(() => DUMMY_PAIRS_TO_PIN[chainId as ChainId] ?? [], [chainId])
// pairs for every token against every base
const generatedPairs: Pair[] = useMemo(
() =>
flatMap(
@@ -173,9 +152,8 @@ export function useAllDummyPairs(): Pair[] {
token => {
// for each token on the current chain,
return (
bases
// loop through all the bases valid for the current chain,
.filter(base => base.chainId === chainId)
// loop though all bases on the current chain
(BASES_TO_TRACK_LIQUIDITY_FOR[chainId as ChainId] ?? [])
// to construct pairs of the given token with each base
.map(base => {
if (base.equals(token)) {
@@ -191,8 +169,8 @@ export function useAllDummyPairs(): Pair[] {
[tokens, chainId]
)
// pairs saved by users
const savedSerializedPairs = useSelector<AppState, AppState['user']['pairs']>(({ user: { pairs } }) => pairs)
const userPairs = useMemo(
() =>
Object.values<SerializedPair>(savedSerializedPairs[chainId ?? -1] ?? {}).map(
@@ -208,7 +186,8 @@ export function useAllDummyPairs(): Pair[] {
return useMemo(() => {
const cache: { [pairKey: string]: boolean } = {}
return (
generatedPairs
pinnedPairs
.concat(generatedPairs)
.concat(userPairs)
// filter out duplicate pairs
.filter(pair => {
@@ -219,5 +198,5 @@ export function useAllDummyPairs(): Pair[] {
return (cache[pairKey] = true)
})
)
}, [generatedPairs, userPairs])
}, [pinnedPairs, generatedPairs, userPairs])
}

View File

@@ -1,5 +1,6 @@
import React, { HTMLProps, useCallback } from 'react'
import ReactGA from 'react-ga'
import { Link } from 'react-router-dom'
import styled, { keyframes } from 'styled-components'
import { darken } from 'polished'
import { X } from 'react-feather'
@@ -38,6 +39,51 @@ export const CloseIcon = styled(X)<{ onClick: () => void }>`
cursor: pointer;
`
// A button that triggers some onClick result, but looks like a link.
export const LinkStyledButton = styled.button`
border: none;
text-decoration: none;
background: none;
cursor: pointer;
color: ${({ theme }) => theme.primary1};
font-weight: 500;
:hover {
text-decoration: underline;
}
:focus {
outline: none;
text-decoration: underline;
}
:active {
text-decoration: none;
}
`
// An internal link from the react-router-dom library that is correctly styled
export const StyledInternalLink = styled(Link)`
text-decoration: none;
cursor: pointer;
color: ${({ theme }) => theme.primary1};
font-weight: 500;
:hover {
text-decoration: underline;
}
:focus {
outline: none;
text-decoration: underline;
}
:active {
text-decoration: none;
}
`
const StyledLink = styled.a`
text-decoration: none;
cursor: pointer;
@@ -58,20 +104,19 @@ const StyledLink = styled.a`
}
`
export function Link({
onClick,
/**
* Outbound link that handles firing google analytics events
*/
export function ExternalLink({
target = '_blank',
href,
rel = 'noopener noreferrer',
...rest
}: Omit<HTMLProps<HTMLAnchorElement>, 'as' | 'ref'>) {
}: Omit<HTMLProps<HTMLAnchorElement>, 'as' | 'ref' | 'onClick'> & { href: string }) {
const handleClick = useCallback(
(event: React.MouseEvent<HTMLAnchorElement>) => {
onClick && onClick(event) // first call back into the original onClick
if (!href) return
// don't prevent default, don't redirect
if (target === '_blank') {
// don't prevent default, don't redirect if it's a new tab
if (target === '_blank' || event.ctrlKey || event.metaKey) {
ReactGA.outboundLink({ label: href }, () => {
console.debug('Fired outbound link event', href)
})
@@ -83,7 +128,7 @@ export function Link({
})
}
},
[href, onClick, target]
[href, target]
)
return <StyledLink target={target} rel={rel} href={href} onClick={handleClick} {...rest} />
}

View File

@@ -168,30 +168,21 @@ export const TYPE = {
export const FixedGlobalStyle = createGlobalStyle`
@import url('https://rsms.me/inter/inter.css');
html { font-family: 'Inter', sans-serif; letter-spacing: -0.018em;}
html, body, input, textarea, button { font-family: 'Inter', sans-serif; letter-spacing: -0.018em;}
@supports (font-variation-settings: normal) {
html { font-family: 'Inter var', sans-serif; }
html, body, input, textarea, button { font-family: 'Inter var', sans-serif; }
}
html,
body {
margin: 0;
padding: 0;
width: 100%;
height: 100%;
overflow: hidden;
}
* {
box-sizing: border-box;
}
body > div {
height: 100%;
overflow: auto;
-webkit-overflow-scrolling: touch;
}
html {
font-size: 16px;
font-variant: none;

View File

@@ -2,15 +2,10 @@ import { Contract } from '@ethersproject/contracts'
import { getAddress } from '@ethersproject/address'
import { AddressZero } from '@ethersproject/constants'
import { JsonRpcSigner, Web3Provider } from '@ethersproject/providers'
import { parseBytes32String } from '@ethersproject/strings'
import { BigNumber } from '@ethersproject/bignumber'
import { abi as IUniswapV2PairABI } from '@uniswap/v2-core/build/IUniswapV2Pair.json'
import { abi as IUniswapV2Router01ABI } from '@uniswap/v2-periphery/build/IUniswapV2Router01.json'
import { abi as IUniswapV2Router02ABI } from '@uniswap/v2-periphery/build/IUniswapV2Router02.json'
import { ROUTER_ADDRESS } from '../constants'
import ERC20_ABI from '../constants/abis/erc20.json'
import ERC20_BYTES32_ABI from '../constants/abis/erc20_bytes32.json'
import { ChainId, JSBI, Percent, TokenAmount } from '@uniswap/sdk'
// returns the checksummed address if the address is valid, otherwise returns false
@@ -93,8 +88,8 @@ export function getContract(address: string, ABI: any, library: Web3Provider, ac
}
// account is optional
export function getRouterContract(chainId: number, library: Web3Provider, account?: string) {
return getContract(ROUTER_ADDRESS, IUniswapV2Router01ABI, library, account)
export function getRouterContract(_: number, library: Web3Provider, account?: string) {
return getContract(ROUTER_ADDRESS, IUniswapV2Router02ABI, library, account)
}
// account is optional
@@ -102,51 +97,6 @@ export function getExchangeContract(pairAddress: string, library: Web3Provider,
return getContract(pairAddress, IUniswapV2PairABI, library, account)
}
// get token info and fall back to unknown if not available, except for the
// decimals which falls back to null
export async function getTokenInfoWithFallback(
tokenAddress: string,
library: Web3Provider
): Promise<{ name: string; symbol: string; decimals: null | number }> {
if (!isAddress(tokenAddress)) {
throw Error(`Invalid 'tokenAddress' parameter '${tokenAddress}'.`)
}
const token = getContract(tokenAddress, ERC20_ABI, library)
const namePromise: Promise<string> = token.name().catch(() =>
getContract(tokenAddress, ERC20_BYTES32_ABI, library)
.name()
.then(parseBytes32String)
.catch((e: Error) => {
console.debug('Failed to get name for token address', e, tokenAddress)
return 'Unknown'
})
)
const symbolPromise: Promise<string> = token.symbol().catch(() => {
const contractBytes32 = getContract(tokenAddress, ERC20_BYTES32_ABI, library)
return contractBytes32
.symbol()
.then(parseBytes32String)
.catch((e: Error) => {
console.debug('Failed to get symbol for token address', e, tokenAddress)
return 'UNKNOWN'
})
})
const decimalsPromise: Promise<number | null> = token.decimals().catch((e: Error) => {
console.debug('Failed to get decimals for token address', e, tokenAddress)
return null
})
const [name, symbol, decimals]: [string, string, number | null] = (await Promise.all([
namePromise,
symbolPromise,
decimalsPromise
])) as [string, string, number | null]
return { name, symbol, decimals }
}
export function escapeRegExp(string: string): string {
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') // $& means the whole matched string
}

211
yarn.lock
View File

@@ -1005,7 +1005,7 @@
dependencies:
regenerator-runtime "^0.13.4"
"@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.3.1", "@babel/runtime@^7.3.4", "@babel/runtime@^7.4.0", "@babel/runtime@^7.4.4", "@babel/runtime@^7.4.5", "@babel/runtime@^7.5.5", "@babel/runtime@^7.7.2", "@babel/runtime@^7.8.3", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.2":
"@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.3.1", "@babel/runtime@^7.3.4", "@babel/runtime@^7.4.0", "@babel/runtime@^7.4.5", "@babel/runtime@^7.5.5", "@babel/runtime@^7.7.2", "@babel/runtime@^7.8.4", "@babel/runtime@^7.9.2":
version "7.9.6"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.9.6.tgz#a9102eb5cadedf3f31d08a9ecf294af7827ea29f"
integrity sha512-64AF1xY3OAkFHqOb9s4jpgk1Mm5vDZ4L3acHvAml+53nO1XbXLuDodsVpO4OIUsmemlUHMxNdYMNJmsvOwLrvQ==
@@ -1138,7 +1138,7 @@
"@emotion/utils" "0.11.3"
babel-plugin-emotion "^10.0.27"
"@emotion/hash@0.8.0", "@emotion/hash@^0.8.0":
"@emotion/hash@0.8.0":
version "0.8.0"
resolved "https://registry.yarnpkg.com/@emotion/hash/-/hash-0.8.0.tgz#bbbff68978fefdbe68ccb533bc8cbe1d1afb5413"
integrity sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow==
@@ -2040,80 +2040,6 @@
"@types/yargs" "^15.0.0"
chalk "^3.0.0"
"@material-ui/core@^4.9.5":
version "4.9.13"
resolved "https://registry.yarnpkg.com/@material-ui/core/-/core-4.9.13.tgz#024962bcdda05139e1bad17a1815bf4088702b15"
integrity sha512-GEXNwUr+laZ0N+F1efmHB64Fyg+uQIRXLqbSejg3ebSXgLYNpIjnMOPRfWdu4rICq0dAIgvvNXGkKDMcf3AMpA==
dependencies:
"@babel/runtime" "^7.4.4"
"@material-ui/react-transition-group" "^4.3.0"
"@material-ui/styles" "^4.9.13"
"@material-ui/system" "^4.9.13"
"@material-ui/types" "^5.0.1"
"@material-ui/utils" "^4.9.12"
"@types/react-transition-group" "^4.2.0"
clsx "^1.0.4"
hoist-non-react-statics "^3.3.2"
popper.js "^1.16.1-lts"
prop-types "^15.7.2"
react-is "^16.8.0"
react-transition-group "^4.3.0"
"@material-ui/react-transition-group@^4.3.0":
version "4.3.0"
resolved "https://registry.yarnpkg.com/@material-ui/react-transition-group/-/react-transition-group-4.3.0.tgz#92529142addb5cc179dbf42d246c7e3fe4d6104b"
integrity sha512-CwQ0aXrlUynUTY6sh3UvKuvye1o92en20VGAs6TORnSxUYeRmkX8YeTUN3lAkGiBX1z222FxLFO36WWh6q73rQ==
dependencies:
"@babel/runtime" "^7.5.5"
dom-helpers "^5.0.1"
loose-envify "^1.4.0"
prop-types "^15.6.2"
"@material-ui/styles@^4.9.13":
version "4.9.13"
resolved "https://registry.yarnpkg.com/@material-ui/styles/-/styles-4.9.13.tgz#08b3976bdd21c38bc076693d95834f97539f3b15"
integrity sha512-lWlXJanBdHQ18jW/yphedRokHcvZD1GdGzUF/wQxKDsHwDDfO45ZkAxuSBI202dG+r1Ph483Z3pFykO2obeSRA==
dependencies:
"@babel/runtime" "^7.4.4"
"@emotion/hash" "^0.8.0"
"@material-ui/types" "^5.0.1"
"@material-ui/utils" "^4.9.6"
clsx "^1.0.4"
csstype "^2.5.2"
hoist-non-react-statics "^3.3.2"
jss "^10.0.3"
jss-plugin-camel-case "^10.0.3"
jss-plugin-default-unit "^10.0.3"
jss-plugin-global "^10.0.3"
jss-plugin-nested "^10.0.3"
jss-plugin-props-sort "^10.0.3"
jss-plugin-rule-value-function "^10.0.3"
jss-plugin-vendor-prefixer "^10.0.3"
prop-types "^15.7.2"
"@material-ui/system@^4.9.13":
version "4.9.13"
resolved "https://registry.yarnpkg.com/@material-ui/system/-/system-4.9.13.tgz#adefb3b6a5ddf0b00fe4e82ac63bb48276e9749d"
integrity sha512-6AlpvdW6KJJ5bF1Xo2OD13sCN8k+nlL36412/bWnWZOKIfIMo/Lb8c8d1DOIaT/RKWxTEUaWnKZjabVnA3eZjA==
dependencies:
"@babel/runtime" "^7.4.4"
"@material-ui/utils" "^4.9.6"
prop-types "^15.7.2"
"@material-ui/types@^5.0.1":
version "5.0.1"
resolved "https://registry.yarnpkg.com/@material-ui/types/-/types-5.0.1.tgz#c4954063cdc196eb327ee62c041368b1aebb6d61"
integrity sha512-wURPSY7/3+MAtng3i26g+WKwwNE3HEeqa/trDBR5+zWKmcjO+u9t7Npu/J1r+3dmIa/OeziN9D/18IrBKvKffw==
"@material-ui/utils@^4.9.12", "@material-ui/utils@^4.9.6":
version "4.9.12"
resolved "https://registry.yarnpkg.com/@material-ui/utils/-/utils-4.9.12.tgz#0d639f1c1ed83fffb2ae10c21d15a938795d9e65"
integrity sha512-/0rgZPEOcZq5CFA4+4n6Q6zk7fi8skHhH2Bcra8R3epoJEYy5PL55LuMazPtPH1oKeRausDV/Omz4BbgFsn1HQ==
dependencies:
"@babel/runtime" "^7.4.4"
prop-types "^15.7.2"
react-is "^16.8.0"
"@mrmlnc/readdir-enhanced@^2.2.1":
version "2.2.1"
resolved "https://registry.yarnpkg.com/@mrmlnc/readdir-enhanced/-/readdir-enhanced-2.2.1.tgz#524af240d1a360527b730475ecfa1344aa540dde"
@@ -2683,13 +2609,6 @@
"@types/history" "*"
"@types/react" "*"
"@types/react-transition-group@^4.2.0":
version "4.2.4"
resolved "https://registry.yarnpkg.com/@types/react-transition-group/-/react-transition-group-4.2.4.tgz#c7416225987ccdb719262766c1483da8f826838d"
integrity sha512-8DMUaDqh0S70TjkqU0DxOu80tFUiiaS9rxkWip/nb7gtvAsbqOXm02UCmR8zdcjWujgeYPiPNTVpVpKzUDotwA==
dependencies:
"@types/react" "*"
"@types/react-window@^1.8.2":
version "1.8.2"
resolved "https://registry.yarnpkg.com/@types/react-window/-/react-window-1.8.2.tgz#a5a6b2762ce73ffaab7911ee1397cf645f2459fe"
@@ -2894,10 +2813,10 @@
resolved "https://registry.yarnpkg.com/@uniswap/v2-core/-/v2-core-1.0.0.tgz#e0fab91a7d53e8cafb5326ae4ca18351116b0844"
integrity sha512-BJiXrBGnN8mti7saW49MXwxDBRFiWemGetE58q8zgfnPPzQKq55ADltEILqOt6VFZ22kVeVKbF8gVd8aY3l7pA==
"@uniswap/v2-periphery@1.0.0-beta.0":
version "1.0.0-beta.0"
resolved "https://registry.yarnpkg.com/@uniswap/v2-periphery/-/v2-periphery-1.0.0-beta.0.tgz#53ccbd5f6a3e43fd37f04660625873dc7d5c57b2"
integrity sha512-r0Iuk7L3gZzbmlZeNhMdLmG0fOqfHCoxmWqYQ9OX4r3w0lJ6dyErJ8kVdTy8PtairkuHkXWeH3OdyzzzBtyRpw==
"@uniswap/v2-periphery@^1.1.0-beta.0":
version "1.1.0-beta.0"
resolved "https://registry.yarnpkg.com/@uniswap/v2-periphery/-/v2-periphery-1.1.0-beta.0.tgz#20a4ccfca22f1a45402303aedb5717b6918ebe6d"
integrity sha512-6dkwAMKza8nzqYiXEr2D86dgW3TTavUvCR0w2Tu33bAbM8Ah43LKAzH7oKKPRT5VJQaMi1jtkGs1E8JPor1n5g==
dependencies:
"@uniswap/lib" "1.1.1"
"@uniswap/v2-core" "1.0.0"
@@ -5101,7 +5020,7 @@ clone@^2.0.0, clone@^2.1.1:
resolved "https://registry.yarnpkg.com/clone/-/clone-2.1.2.tgz#1b7f4b9f591f1e8f83670401600345a02887435f"
integrity sha1-G39Ln1kfHo+DZwQBYANFoCiHQ18=
clsx@^1.0.4, clsx@^1.1.0:
clsx@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/clsx/-/clsx-1.1.0.tgz#62937c6adfea771247c34b54d320fb99624f5702"
integrity sha512-3avwM37fSK5oP6M5rQ9CNe99lwxhXDOeSWVPAOYF6OazUTgZCMb0yWlJpmdD74REy1gkEaFiub2ULv4fq9GUhA==
@@ -5650,14 +5569,6 @@ css-tree@1.0.0-alpha.39:
mdn-data "2.0.6"
source-map "^0.6.1"
css-vendor@^2.0.7:
version "2.0.8"
resolved "https://registry.yarnpkg.com/css-vendor/-/css-vendor-2.0.8.tgz#e47f91d3bd3117d49180a3c935e62e3d9f7f449d"
integrity sha512-x9Aq0XTInxrkuFeHKbYC7zWY8ai7qJ04Kxd9MnvbC1uO5DagxoHQjm4JvG+vCdXOoFtCjbL2XSZfxmoYa9uQVQ==
dependencies:
"@babel/runtime" "^7.8.3"
is-in-browser "^1.0.2"
css-what@2.1:
version "2.1.3"
resolved "https://registry.yarnpkg.com/css-what/-/css-what-2.1.3.tgz#a6d7604573365fe74686c3f311c56513d88285f2"
@@ -5780,7 +5691,7 @@ cssstyle@^1.0.0, cssstyle@^1.1.1:
dependencies:
cssom "0.3.x"
csstype@^2.2.0, csstype@^2.5.2, csstype@^2.5.7, csstype@^2.6.5, csstype@^2.6.6, csstype@^2.6.7, csstype@^2.6.9:
csstype@^2.2.0, csstype@^2.5.7, csstype@^2.6.6, csstype@^2.6.9:
version "2.6.10"
resolved "https://registry.yarnpkg.com/csstype/-/csstype-2.6.10.tgz#e63af50e66d7c266edb6b32909cfd0aabe03928b"
integrity sha512-D34BqZU4cIlMCY93rZHbrq9pjTAQJ3U8S8rfBqjwHxkGPThWFjzZDQpgMJY0QViLxth6ZKYiwFBo14RdN44U/w==
@@ -6127,14 +6038,6 @@ dom-converter@^0.2:
dependencies:
utila "~0.4"
dom-helpers@^5.0.1:
version "5.1.4"
resolved "https://registry.yarnpkg.com/dom-helpers/-/dom-helpers-5.1.4.tgz#4609680ab5c79a45f2531441f1949b79d6587f4b"
integrity sha512-TjMyeVUvNEnOnhzs6uAn9Ya47GmMo3qq7m+Lr/3ON0Rs5kHvb8I+SQYjLUSYn7qhEm0QjW0yrBkvz9yOrwwz1A==
dependencies:
"@babel/runtime" "^7.8.7"
csstype "^2.6.7"
dom-serializer@0:
version "0.2.2"
resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.2.2.tgz#1afb81f533717175d478655debc5e332d9f9bb51"
@@ -8042,7 +7945,7 @@ hmac-drbg@^1.0.0:
minimalistic-assert "^1.0.0"
minimalistic-crypto-utils "^1.0.1"
hoist-non-react-statics@^3.1.0, hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.2:
hoist-non-react-statics@^3.1.0, hoist-non-react-statics@^3.3.0:
version "3.3.2"
resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45"
integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==
@@ -8228,11 +8131,6 @@ human-signals@^1.1.1:
resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-1.1.1.tgz#c5b1cd14f50aeae09ab6c59fe63ba3395fe4dfa3"
integrity sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==
hyphenate-style-name@^1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/hyphenate-style-name/-/hyphenate-style-name-1.0.3.tgz#097bb7fa0b8f1a9cf0bd5c734cf95899981a9b48"
integrity sha512-EcuixamT82oplpoJ2XU4pDtKGWQ7b00CD9f1ug9IaQ3p1bkHMiKCZ9ut9QDI6qsa6cpUuB+A/I+zLtdNK4n2DQ==
i18next-browser-languagedetector@^3.0.1:
version "3.1.1"
resolved "https://registry.yarnpkg.com/i18next-browser-languagedetector/-/i18next-browser-languagedetector-3.1.1.tgz#1a0c236d4339476cc3632da60ff947bbc2e3ba3c"
@@ -8686,11 +8584,6 @@ is-hex-prefixed@1.0.0:
resolved "https://registry.yarnpkg.com/is-hex-prefixed/-/is-hex-prefixed-1.0.0.tgz#7d8d37e6ad77e5d127148913c573e082d777f554"
integrity sha1-fY035q135dEnFIkTxXPggtd39VQ=
is-in-browser@^1.0.2, is-in-browser@^1.1.3:
version "1.1.3"
resolved "https://registry.yarnpkg.com/is-in-browser/-/is-in-browser-1.1.3.tgz#56ff4db683a078c6082eb95dad7dc62e1d04f835"
integrity sha1-Vv9NtoOgeMYILrldrX3GLh0E+DU=
is-installed-globally@0.1.0:
version "0.1.0"
resolved "https://registry.yarnpkg.com/is-installed-globally/-/is-installed-globally-0.1.0.tgz#0dfd98f5a9111716dd535dda6492f67bf3d25a80"
@@ -9566,75 +9459,6 @@ jsprim@^1.2.2:
json-schema "0.2.3"
verror "1.10.0"
jss-plugin-camel-case@^10.0.3:
version "10.1.1"
resolved "https://registry.yarnpkg.com/jss-plugin-camel-case/-/jss-plugin-camel-case-10.1.1.tgz#8e73ecc4f1d0f8dfe4dd31f6f9f2782588970e78"
integrity sha512-MDIaw8FeD5uFz1seQBKz4pnvDLnj5vIKV5hXSVdMaAVq13xR6SVTVWkIV/keyTs5txxTvzGJ9hXoxgd1WTUlBw==
dependencies:
"@babel/runtime" "^7.3.1"
hyphenate-style-name "^1.0.3"
jss "10.1.1"
jss-plugin-default-unit@^10.0.3:
version "10.1.1"
resolved "https://registry.yarnpkg.com/jss-plugin-default-unit/-/jss-plugin-default-unit-10.1.1.tgz#2df86016dfe73085eead843f5794e3890e9c5c47"
integrity sha512-UkeVCA/b3QEA4k0nIKS4uWXDCNmV73WLHdh2oDGZZc3GsQtlOCuiH3EkB/qI60v2MiCq356/SYWsDXt21yjwdg==
dependencies:
"@babel/runtime" "^7.3.1"
jss "10.1.1"
jss-plugin-global@^10.0.3:
version "10.1.1"
resolved "https://registry.yarnpkg.com/jss-plugin-global/-/jss-plugin-global-10.1.1.tgz#36b0d6d9facb74dfd99590643708a89260747d14"
integrity sha512-VBG3wRyi3Z8S4kMhm8rZV6caYBegsk+QnQZSVmrWw6GVOT/Z4FA7eyMu5SdkorDlG/HVpHh91oFN56O4R9m2VA==
dependencies:
"@babel/runtime" "^7.3.1"
jss "10.1.1"
jss-plugin-nested@^10.0.3:
version "10.1.1"
resolved "https://registry.yarnpkg.com/jss-plugin-nested/-/jss-plugin-nested-10.1.1.tgz#5c3de2b8bda344de1ebcef3a4fd30870a29a8a8c"
integrity sha512-ozEu7ZBSVrMYxSDplPX3H82XHNQk2DQEJ9TEyo7OVTPJ1hEieqjDFiOQOxXEj9z3PMqkylnUbvWIZRDKCFYw5Q==
dependencies:
"@babel/runtime" "^7.3.1"
jss "10.1.1"
tiny-warning "^1.0.2"
jss-plugin-props-sort@^10.0.3:
version "10.1.1"
resolved "https://registry.yarnpkg.com/jss-plugin-props-sort/-/jss-plugin-props-sort-10.1.1.tgz#34bddcbfaf9430ec8ccdf92729f03bb10caf1785"
integrity sha512-g/joK3eTDZB4pkqpZB38257yD4LXB0X15jxtZAGbUzcKAVUHPl9Jb47Y7lYmiGsShiV4YmQRqG1p2DHMYoK91g==
dependencies:
"@babel/runtime" "^7.3.1"
jss "10.1.1"
jss-plugin-rule-value-function@^10.0.3:
version "10.1.1"
resolved "https://registry.yarnpkg.com/jss-plugin-rule-value-function/-/jss-plugin-rule-value-function-10.1.1.tgz#be00dac6fc394aaddbcef5860b9eca6224d96382"
integrity sha512-ClV1lvJ3laU9la1CUzaDugEcwnpjPTuJ0yGy2YtcU+gG/w9HMInD5vEv7xKAz53Bk4WiJm5uLOElSEshHyhKNw==
dependencies:
"@babel/runtime" "^7.3.1"
jss "10.1.1"
jss-plugin-vendor-prefixer@^10.0.3:
version "10.1.1"
resolved "https://registry.yarnpkg.com/jss-plugin-vendor-prefixer/-/jss-plugin-vendor-prefixer-10.1.1.tgz#8348b20749f790beebab3b6a8f7075b07c2cfcfd"
integrity sha512-09MZpQ6onQrhaVSF6GHC4iYifQ7+4YC/tAP6D4ZWeZotvCMq1mHLqNKRIaqQ2lkgANjlEot2JnVi1ktu4+L4pw==
dependencies:
"@babel/runtime" "^7.3.1"
css-vendor "^2.0.7"
jss "10.1.1"
jss@10.1.1, jss@^10.0.3:
version "10.1.1"
resolved "https://registry.yarnpkg.com/jss/-/jss-10.1.1.tgz#450b27d53761af3e500b43130a54cdbe157ea332"
integrity sha512-Xz3qgRUFlxbWk1czCZibUJqhVPObrZHxY3FPsjCXhDld4NOj1BgM14Ir5hVm+Qr6OLqVljjGvoMcCdXNOAbdkQ==
dependencies:
"@babel/runtime" "^7.3.1"
csstype "^2.6.5"
is-in-browser "^1.1.3"
tiny-warning "^1.0.2"
jsx-ast-utils@^2.2.1, jsx-ast-utils@^2.2.3:
version "2.2.3"
resolved "https://registry.yarnpkg.com/jsx-ast-utils/-/jsx-ast-utils-2.2.3.tgz#8a9364e402448a3ce7f14d357738310d9248054f"
@@ -11340,11 +11164,6 @@ polished@^3.3.2:
dependencies:
"@babel/runtime" "^7.9.2"
popper.js@^1.16.1-lts:
version "1.16.1"
resolved "https://registry.yarnpkg.com/popper.js/-/popper.js-1.16.1.tgz#2a223cb3dc7b6213d740e40372be40de43e65b1b"
integrity sha512-Wb4p1J4zyFTbM+u6WuO4XstYx4Ky9Cewe4DWrel7B0w6VVICvPwdOpotjzcf6eD8TsckVnIMNONQyPIUFOUbCQ==
portfinder@^1.0.25:
version "1.0.26"
resolved "https://registry.yarnpkg.com/portfinder/-/portfinder-1.0.26.tgz#475658d56ca30bed72ac7f1378ed350bd1b64e70"
@@ -12467,7 +12286,7 @@ react-i18next@^10.7.0:
"@babel/runtime" "^7.3.1"
html-parse-stringify2 "2.0.1"
react-is@^16.12.0, react-is@^16.6.0, react-is@^16.7.0, react-is@^16.8.0, react-is@^16.8.1, react-is@^16.8.4, react-is@^16.9.0:
react-is@^16.12.0, react-is@^16.6.0, react-is@^16.7.0, react-is@^16.8.1, react-is@^16.8.4, react-is@^16.9.0:
version "16.13.1"
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
@@ -12616,16 +12435,6 @@ react-style-singleton@^2.1.0:
invariant "^2.2.4"
tslib "^1.0.0"
react-transition-group@^4.3.0:
version "4.4.1"
resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-4.4.1.tgz#63868f9325a38ea5ee9535d828327f85773345c9"
integrity sha512-Djqr7OQ2aPUiYurhPalTrVy9ddmFCCzwhqQmtN+J3+3DzLO209Fdr70QrN8Z3DsglWql6iY1lDWAfpFiBtuKGw==
dependencies:
"@babel/runtime" "^7.5.5"
dom-helpers "^5.0.1"
loose-envify "^1.4.0"
prop-types "^15.6.2"
react-use-gesture@^6.0.14:
version "6.0.14"
resolved "https://registry.yarnpkg.com/react-use-gesture/-/react-use-gesture-6.0.14.tgz#ab2d35ef72a5fb6060a6160eb12568c276f8a4b1"