Compare commits
27 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e667615449 | ||
|
|
4ab61faeae | ||
|
|
0004db3d4a | ||
|
|
c133c472be | ||
|
|
0019ccdf51 | ||
|
|
5a1a469f35 | ||
|
|
4c28f34803 | ||
|
|
104be830fc | ||
|
|
24c70791cd | ||
|
|
216fdea290 | ||
|
|
40e4ce2ed3 | ||
|
|
b2508fc6f2 | ||
|
|
f73b37287f | ||
|
|
c09eb738c3 | ||
|
|
6de3a6ec28 | ||
|
|
c1d35cc8b3 | ||
|
|
f279b2bea2 | ||
|
|
6ffbf756f8 | ||
|
|
10837d7ba1 | ||
|
|
2d6eddf9d4 | ||
|
|
aadf43efc3 | ||
|
|
227f729ecd | ||
|
|
a5b15e37f6 | ||
|
|
2408b2966e | ||
|
|
dc391d1bea | ||
|
|
e2d0514344 | ||
|
|
98d25dd2af |
@@ -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).
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
|
||||
2
cypress/support/commands.d.ts
vendored
2
cypress/support/commands.d.ts
vendored
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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/*"
|
||||
|
||||
@@ -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
75
public/locales/he.json
Normal 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": "הכניסו כתובת טוקן כדי להמשיך"
|
||||
}
|
||||
BIN
src/assets/images/ethereum-logo.png
Normal file
BIN
src/assets/images/ethereum-logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.5 KiB |
@@ -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 |
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)`
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)};
|
||||
|
||||
@@ -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
|
||||
<Link href="https://uniswap.org/blog/launch-uniswap-v2/">
|
||||
<ExternalLink href="https://uniswap.org/blog/launch-uniswap-v2/">
|
||||
<b>blog post ↗</b>
|
||||
</Link>
|
||||
</ExternalLink>
|
||||
or
|
||||
<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}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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} />
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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 />
|
||||
) : (
|
||||
'-'
|
||||
)}
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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." />
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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? </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>
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -37,6 +37,11 @@ export default function SwapModalFooter({
|
||||
confirmText: string
|
||||
}) {
|
||||
const theme = useContext(ThemeContext)
|
||||
|
||||
if (!trade) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<AutoColumn gap="0px">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
|
||||
55
src/constants/abis/migrator.json
Normal file
55
src/constants/abis/migrator.json
Normal 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"
|
||||
}
|
||||
]
|
||||
5
src/constants/abis/migrator.ts
Normal file
5
src/constants/abis/migrator.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import MIGRATOR_ABI from './migrator.json'
|
||||
|
||||
const MIGRATOR_ADDRESS = '0x16D4F26C15f3658ec65B1126ff27DD3dF2a2996b'
|
||||
|
||||
export { MIGRATOR_ADDRESS, MIGRATOR_ABI }
|
||||
@@ -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,
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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])
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
() =>
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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]
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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])
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
) : (
|
||||
|
||||
11
src/pages/MigrateV1/EmptyState.tsx
Normal file
11
src/pages/MigrateV1/EmptyState.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
289
src/pages/MigrateV1/MigrateV1Exchange.tsx
Normal file
289
src/pages/MigrateV1/MigrateV1Exchange.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 don’t 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>
|
||||
) : (
|
||||
|
||||
@@ -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%">
|
||||
|
||||
@@ -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={() => {
|
||||
|
||||
@@ -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={() => {
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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]
|
||||
)
|
||||
|
||||
@@ -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
|
||||
})
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
57
src/state/multicall/actions.test.ts
Normal file
57
src/state/multicall/actions.test.ts
Normal 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'
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -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]
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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`]: {} } }
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
@@ -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])
|
||||
}
|
||||
|
||||
@@ -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} />
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
211
yarn.lock
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user