Compare commits
38 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
97deebad37 | ||
|
|
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 | ||
|
|
f289dec684 | ||
|
|
73d3df05f2 | ||
|
|
83554f44f8 | ||
|
|
320b2e384b | ||
|
|
9492e7375a | ||
|
|
8a6a10be9d | ||
|
|
5e486fca7f | ||
|
|
87d24c404b | ||
|
|
d4011f73d1 | ||
|
|
6fc3157977 |
@@ -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,11 +33,10 @@
|
||||
"@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",
|
||||
"@web3-react/network-connector": "^6.0.9",
|
||||
"@web3-react/portis-connector": "^6.0.9",
|
||||
"@web3-react/walletconnect-connector": "^6.0.9",
|
||||
"@web3-react/walletlink-connector": "^6.0.9",
|
||||
@@ -50,7 +48,6 @@
|
||||
"eslint-plugin-prettier": "^3.1.3",
|
||||
"eslint-plugin-react": "^7.19.0",
|
||||
"eslint-plugin-react-hooks": "^4.0.0",
|
||||
"history": "^4.9.0",
|
||||
"i18next": "^15.0.9",
|
||||
"i18next-browser-languagedetector": "^3.0.1",
|
||||
"i18next-xhr-backend": "^2.0.1",
|
||||
|
||||
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,24 +116,12 @@ 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} />
|
||||
}
|
||||
}
|
||||
|
||||
export default function App() {
|
||||
export default function Popups() {
|
||||
const theme = useContext(ThemeContext)
|
||||
// get all popups
|
||||
const activePopups = useActivePopups()
|
||||
|
||||
@@ -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()
|
||||
@@ -45,20 +34,6 @@ export default function Web3ReactManager({ children }) {
|
||||
}
|
||||
}, [triedEager, networkActive, networkError, activateNetwork, active])
|
||||
|
||||
// 'pause' the network connector if we're ever connected to an account and it's active
|
||||
useEffect(() => {
|
||||
if (active && networkActive) {
|
||||
network.pause()
|
||||
}
|
||||
}, [active, networkActive])
|
||||
|
||||
// 'resume' the network connector if we're ever not connected to an account and it's active
|
||||
useEffect(() => {
|
||||
if (!active && networkActive) {
|
||||
network.resume()
|
||||
}
|
||||
}, [active, networkActive])
|
||||
|
||||
// when there's no account connected, react to logins (broadly speaking) on the injected provider, if it exists
|
||||
useInactiveListener(!triedEager)
|
||||
|
||||
@@ -92,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,12 +1,15 @@
|
||||
import { ChainId } from '@uniswap/sdk'
|
||||
import { FortmaticConnector as FortmaticConnectorCore } from '@web3-react/fortmatic-connector'
|
||||
|
||||
export const OVERLAY_READY = 'OVERLAY_READY'
|
||||
|
||||
const chainIdToNetwork = {
|
||||
1: 'mainnet',
|
||||
3: 'ropsten',
|
||||
4: 'rinkeby',
|
||||
42: 'kovan'
|
||||
type FormaticSupportedChains = Extract<ChainId, ChainId.MAINNET | ChainId.ROPSTEN | ChainId.RINKEBY | ChainId.KOVAN>
|
||||
|
||||
const CHAIN_ID_NETWORK_ARGUMENT: { readonly [chainId in FormaticSupportedChains]: string | undefined } = {
|
||||
[ChainId.MAINNET]: undefined,
|
||||
[ChainId.ROPSTEN]: 'ropsten',
|
||||
[ChainId.RINKEBY]: 'rinkeby',
|
||||
[ChainId.KOVAN]: 'kovan'
|
||||
}
|
||||
|
||||
export class FortmaticConnector extends FortmaticConnectorCore {
|
||||
@@ -14,7 +17,11 @@ export class FortmaticConnector extends FortmaticConnectorCore {
|
||||
if (!this.fortmatic) {
|
||||
const { default: Fortmatic } = await import('fortmatic')
|
||||
const { apiKey, chainId } = this as any
|
||||
this.fortmatic = new Fortmatic(apiKey, chainId === 1 || chainId === 4 ? undefined : chainIdToNetwork[chainId])
|
||||
if (chainId in CHAIN_ID_NETWORK_ARGUMENT) {
|
||||
this.fortmatic = new Fortmatic(apiKey, CHAIN_ID_NETWORK_ARGUMENT[chainId as FormaticSupportedChains])
|
||||
} else {
|
||||
throw new Error(`Unsupported network ID: ${chainId}`)
|
||||
}
|
||||
}
|
||||
|
||||
const provider = this.fortmatic.getProvider()
|
||||
@@ -29,7 +36,10 @@ export class FortmaticConnector extends FortmaticConnectorCore {
|
||||
}, 200)
|
||||
})
|
||||
|
||||
const [account] = await Promise.all([provider.enable().then(accounts => accounts[0]), pollForOverlayReady])
|
||||
const [account] = await Promise.all([
|
||||
provider.enable().then((accounts: string[]) => accounts[0]),
|
||||
pollForOverlayReady
|
||||
])
|
||||
|
||||
return { provider: this.fortmatic.getProvider(), chainId: (this as any).chainId, account }
|
||||
}
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
import { NetworkConnector as NetworkConnectorCore } from '@web3-react/network-connector'
|
||||
|
||||
export class NetworkConnector extends NetworkConnectorCore {
|
||||
pause() {
|
||||
if ((this as any).active) {
|
||||
;(this as any).providers[(this as any).currentChainId].stop()
|
||||
}
|
||||
}
|
||||
|
||||
resume() {
|
||||
if ((this as any).active) {
|
||||
;(this as any).providers[(this as any).currentChainId].start()
|
||||
}
|
||||
}
|
||||
}
|
||||
105
src/connectors/NetworkConnector.ts
Normal file
105
src/connectors/NetworkConnector.ts
Normal file
@@ -0,0 +1,105 @@
|
||||
import { ConnectorUpdate } from '@web3-react/types'
|
||||
import { AbstractConnector } from '@web3-react/abstract-connector'
|
||||
import invariant from 'tiny-invariant'
|
||||
|
||||
interface NetworkConnectorArguments {
|
||||
urls: { [chainId: number]: string }
|
||||
defaultChainId?: number
|
||||
}
|
||||
|
||||
// taken from ethers.js, compatible interface with web3 provider
|
||||
type AsyncSendable = {
|
||||
isMetaMask?: boolean
|
||||
host?: string
|
||||
path?: string
|
||||
sendAsync?: (request: any, callback: (error: any, response: any) => void) => void
|
||||
send?: (request: any, callback: (error: any, response: any) => void) => void
|
||||
}
|
||||
|
||||
class RequestError extends Error {
|
||||
constructor(message: string, public code: number, public data?: unknown) {
|
||||
super(message)
|
||||
}
|
||||
}
|
||||
|
||||
class MiniRpcProvider implements AsyncSendable {
|
||||
public readonly isMetaMask: false = false
|
||||
public readonly chainId: number
|
||||
public readonly url: string
|
||||
public readonly host: string
|
||||
public readonly path: string
|
||||
|
||||
constructor(chainId: number, url: string) {
|
||||
this.chainId = chainId
|
||||
this.url = url
|
||||
const parsed = new URL(url)
|
||||
this.host = parsed.host
|
||||
this.path = parsed.pathname
|
||||
}
|
||||
|
||||
public readonly sendAsync = (
|
||||
request: { jsonrpc: '2.0'; id: number | string | null; method: string; params?: unknown[] | object },
|
||||
callback: (error: any, response: any) => void
|
||||
): void => {
|
||||
this.request(request.method, request.params)
|
||||
.then(result => callback(null, { jsonrpc: '2.0', id: request.id, result }))
|
||||
.catch(error => callback(error, null))
|
||||
}
|
||||
|
||||
public readonly request = async (method: string, params?: unknown[] | object): Promise<unknown> => {
|
||||
const response = await fetch(this.url, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
jsonrpc: '2.0',
|
||||
id: 1,
|
||||
method,
|
||||
params
|
||||
})
|
||||
})
|
||||
if (!response.ok) throw new RequestError(`${response.status}: ${response.statusText}`, -32000)
|
||||
const body = await response.json()
|
||||
if ('error' in body) {
|
||||
throw new RequestError(body?.error?.message, body?.error?.code, body?.error?.data)
|
||||
} else if ('result' in body) {
|
||||
return body.result
|
||||
} else {
|
||||
throw new RequestError(`Received unexpected JSON-RPC response to ${method} request.`, -32000, body)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class NetworkConnector extends AbstractConnector {
|
||||
private readonly providers: { [chainId: number]: MiniRpcProvider }
|
||||
private currentChainId: number
|
||||
|
||||
constructor({ urls, defaultChainId }: NetworkConnectorArguments) {
|
||||
invariant(defaultChainId || Object.keys(urls).length === 1, 'defaultChainId is a required argument with >1 url')
|
||||
super({ supportedChainIds: Object.keys(urls).map((k): number => Number(k)) })
|
||||
|
||||
this.currentChainId = defaultChainId || Number(Object.keys(urls)[0])
|
||||
this.providers = Object.keys(urls).reduce<{ [chainId: number]: MiniRpcProvider }>((accumulator, chainId) => {
|
||||
accumulator[Number(chainId)] = new MiniRpcProvider(Number(chainId), urls[Number(chainId)])
|
||||
return accumulator
|
||||
}, {})
|
||||
}
|
||||
|
||||
public async activate(): Promise<ConnectorUpdate> {
|
||||
return { provider: this.providers[this.currentChainId], chainId: this.currentChainId, account: null }
|
||||
}
|
||||
|
||||
public async getProvider(): Promise<MiniRpcProvider> {
|
||||
return this.providers[this.currentChainId]
|
||||
}
|
||||
|
||||
public async getChainId(): Promise<number> {
|
||||
return this.currentChainId
|
||||
}
|
||||
|
||||
public async getAccount(): Promise<null> {
|
||||
return null
|
||||
}
|
||||
|
||||
public deactivate() {
|
||||
return
|
||||
}
|
||||
}
|
||||
1
src/connectors/fortmatic.d.ts
vendored
Normal file
1
src/connectors/fortmatic.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
declare module 'formatic'
|
||||
@@ -3,15 +3,20 @@ import { WalletConnectConnector } from '@web3-react/walletconnect-connector'
|
||||
import { WalletLinkConnector } from '@web3-react/walletlink-connector'
|
||||
import { PortisConnector } from '@web3-react/portis-connector'
|
||||
|
||||
import { NetworkConnector } from './Network'
|
||||
import { FortmaticConnector } from './Fortmatic'
|
||||
import { NetworkConnector } from './NetworkConnector'
|
||||
|
||||
const POLLING_INTERVAL = 10000
|
||||
const NETWORK_URL = process.env.REACT_APP_NETWORK_URL
|
||||
const FORMATIC_KEY = process.env.REACT_APP_FORTMATIC_KEY
|
||||
const PORTIS_ID = process.env.REACT_APP_PORTIS_ID
|
||||
|
||||
if (typeof NETWORK_URL === 'undefined') {
|
||||
throw new Error(`REACT_APP_NETWORK_URL must be a defined environment variable`)
|
||||
}
|
||||
|
||||
export const network = new NetworkConnector({
|
||||
urls: { [Number(process.env.REACT_APP_CHAIN_ID)]: NETWORK_URL },
|
||||
pollingInterval: POLLING_INTERVAL * 3
|
||||
urls: { [Number(process.env.REACT_APP_CHAIN_ID)]: NETWORK_URL }
|
||||
})
|
||||
|
||||
export const injected = new InjectedConnector({
|
||||
@@ -28,13 +33,13 @@ export const walletconnect = new WalletConnectConnector({
|
||||
|
||||
// mainnet only
|
||||
export const fortmatic = new FortmaticConnector({
|
||||
apiKey: process.env.REACT_APP_FORTMATIC_KEY,
|
||||
apiKey: FORMATIC_KEY ?? '',
|
||||
chainId: 1
|
||||
})
|
||||
|
||||
// mainnet only
|
||||
export const portis = new PortisConnector({
|
||||
dAppId: process.env.REACT_APP_PORTIS_ID,
|
||||
dAppId: PORTIS_ID ?? '',
|
||||
networks: [1]
|
||||
})
|
||||
|
||||
|
||||
4
src/connectors/tsconfig.json
Normal file
4
src/connectors/tsconfig.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"extends": "../../tsconfig.strict.json",
|
||||
"include": ["**/*"]
|
||||
}
|
||||
@@ -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,26 +1,63 @@
|
||||
import { ChainId, Token, WETH, JSBI, Percent } from '@uniswap/sdk'
|
||||
import { ChainId, JSBI, Percent, Token, WETH, Pair, TokenAmount } from '@uniswap/sdk'
|
||||
|
||||
import { fortmatic, injected, portis, walletconnect, walletlink } from '../connectors'
|
||||
|
||||
export const V1_FACTORY_ADDRESS = '0xc0a47dFe034B400B47bDaD5FecDa2621de6c4d95'
|
||||
export const ROUTER_ADDRESS = '0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D'
|
||||
|
||||
export const ROUTER_ADDRESS = '0xf164fC0Ec4E93095b804a4795bBe1e041497b92a'
|
||||
|
||||
// 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'),
|
||||
@@ -15,6 +16,7 @@ export default [
|
||||
new Token(ChainId.MAINNET, '0x4F9254C83EB525f9FCf346490bbb3ed28a81C667', 18, 'CELR', 'CelerToken'),
|
||||
new Token(ChainId.MAINNET, '0xF5DCe57282A584D2746FaF1593d3121Fcac444dC', 8, 'cSAI', 'Compound Dai'),
|
||||
new Token(ChainId.MAINNET, '0x5d3a536E4D6DbD6114cc1Ead35777bAB948E3643', 8, 'cDAI', 'Compound Dai'),
|
||||
new Token(ChainId.MAINNET, '0x39AA39c021dfbaE8faC545936693aC917d5E7563', 8, 'cUSDC', 'Compound USD Coin'),
|
||||
new Token(ChainId.MAINNET, '0xaaAEBE6Fe48E54f431b0C390CfaF0b017d09D42d', 4, 'CEL', 'Celsius'),
|
||||
new Token(ChainId.MAINNET, '0x06AF07097C9Eeb7fD685c692751D5C66dB49c215', 18, 'CHAI', 'Chai'),
|
||||
new Token(ChainId.MAINNET, '0x89d24A6b4CcB1B6fAA2625fE562bDD9a23260359', 18, 'SAI', 'Dai Stablecoin v1.0 (SAI)'),
|
||||
@@ -30,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'),
|
||||
@@ -76,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'),
|
||||
@@ -90,6 +96,7 @@ export default [
|
||||
new Token(ChainId.MAINNET, '0x42d6622deCe394b54999Fbd73D108123806f6a18', 18, 'SPANK', 'SPANK'),
|
||||
new Token(ChainId.MAINNET, '0xB64ef51C888972c908CFacf59B47C1AfBC0Ab8aC', 8, 'STORJ', 'StorjToken'),
|
||||
new Token(ChainId.MAINNET, '0x57Ab1ec28D129707052df4dF418D58a2D46d5f51', 18, 'sUSD', 'Synth sUSD'),
|
||||
new Token(ChainId.MAINNET, '0x261EfCdD24CeA98652B9700800a13DfBca4103fF', 18, 'sXAU', 'Synth sXAU'),
|
||||
new Token(ChainId.MAINNET, '0x8CE9137d39326AD0cD6491fb5CC0CbA0e089b6A9', 18, 'SXP', 'Swipe'),
|
||||
new Token(ChainId.MAINNET, '0x00006100F7090010005F1bd7aE6122c3C2CF0090', 18, 'TAUD', 'TrueAUD'),
|
||||
new Token(ChainId.MAINNET, '0x00000100F2A2bd000715001920eB70D229700085', 18, 'TCAD', 'TrueCAD'),
|
||||
|
||||
17
src/constants/v1/index.ts
Normal file
17
src/constants/v1/index.ts
Normal file
@@ -0,0 +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_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_ADDRESSES, V1_FACTORY_INTERFACE, V1_FACTORY_ABI, V1_EXCHANGE_INTERFACE, V1_EXCHANGE_ABI }
|
||||
971
src/constants/v1/v1_exchange.json
Normal file
971
src/constants/v1/v1_exchange.json
Normal file
@@ -0,0 +1,971 @@
|
||||
[
|
||||
{
|
||||
"name": "TokenPurchase",
|
||||
"inputs": [
|
||||
{
|
||||
"type": "address",
|
||||
"name": "buyer",
|
||||
"indexed": true
|
||||
},
|
||||
{
|
||||
"type": "uint256",
|
||||
"name": "eth_sold",
|
||||
"indexed": true
|
||||
},
|
||||
{
|
||||
"type": "uint256",
|
||||
"name": "tokens_bought",
|
||||
"indexed": true
|
||||
}
|
||||
],
|
||||
"anonymous": false,
|
||||
"type": "event"
|
||||
},
|
||||
{
|
||||
"name": "EthPurchase",
|
||||
"inputs": [
|
||||
{
|
||||
"type": "address",
|
||||
"name": "buyer",
|
||||
"indexed": true
|
||||
},
|
||||
{
|
||||
"type": "uint256",
|
||||
"name": "tokens_sold",
|
||||
"indexed": true
|
||||
},
|
||||
{
|
||||
"type": "uint256",
|
||||
"name": "eth_bought",
|
||||
"indexed": true
|
||||
}
|
||||
],
|
||||
"anonymous": false,
|
||||
"type": "event"
|
||||
},
|
||||
{
|
||||
"name": "AddLiquidity",
|
||||
"inputs": [
|
||||
{
|
||||
"type": "address",
|
||||
"name": "provider",
|
||||
"indexed": true
|
||||
},
|
||||
{
|
||||
"type": "uint256",
|
||||
"name": "eth_amount",
|
||||
"indexed": true
|
||||
},
|
||||
{
|
||||
"type": "uint256",
|
||||
"name": "token_amount",
|
||||
"indexed": true
|
||||
}
|
||||
],
|
||||
"anonymous": false,
|
||||
"type": "event"
|
||||
},
|
||||
{
|
||||
"name": "RemoveLiquidity",
|
||||
"inputs": [
|
||||
{
|
||||
"type": "address",
|
||||
"name": "provider",
|
||||
"indexed": true
|
||||
},
|
||||
{
|
||||
"type": "uint256",
|
||||
"name": "eth_amount",
|
||||
"indexed": true
|
||||
},
|
||||
{
|
||||
"type": "uint256",
|
||||
"name": "token_amount",
|
||||
"indexed": true
|
||||
}
|
||||
],
|
||||
"anonymous": false,
|
||||
"type": "event"
|
||||
},
|
||||
{
|
||||
"name": "Transfer",
|
||||
"inputs": [
|
||||
{
|
||||
"type": "address",
|
||||
"name": "_from",
|
||||
"indexed": true
|
||||
},
|
||||
{
|
||||
"type": "address",
|
||||
"name": "_to",
|
||||
"indexed": true
|
||||
},
|
||||
{
|
||||
"type": "uint256",
|
||||
"name": "_value",
|
||||
"indexed": false
|
||||
}
|
||||
],
|
||||
"anonymous": false,
|
||||
"type": "event"
|
||||
},
|
||||
{
|
||||
"name": "Approval",
|
||||
"inputs": [
|
||||
{
|
||||
"type": "address",
|
||||
"name": "_owner",
|
||||
"indexed": true
|
||||
},
|
||||
{
|
||||
"type": "address",
|
||||
"name": "_spender",
|
||||
"indexed": true
|
||||
},
|
||||
{
|
||||
"type": "uint256",
|
||||
"name": "_value",
|
||||
"indexed": false
|
||||
}
|
||||
],
|
||||
"anonymous": false,
|
||||
"type": "event"
|
||||
},
|
||||
{
|
||||
"name": "setup",
|
||||
"outputs": [],
|
||||
"inputs": [
|
||||
{
|
||||
"type": "address",
|
||||
"name": "token_addr"
|
||||
}
|
||||
],
|
||||
"constant": false,
|
||||
"payable": false,
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"name": "addLiquidity",
|
||||
"outputs": [
|
||||
{
|
||||
"type": "uint256",
|
||||
"name": "out"
|
||||
}
|
||||
],
|
||||
"inputs": [
|
||||
{
|
||||
"type": "uint256",
|
||||
"name": "min_liquidity"
|
||||
},
|
||||
{
|
||||
"type": "uint256",
|
||||
"name": "max_tokens"
|
||||
},
|
||||
{
|
||||
"type": "uint256",
|
||||
"name": "deadline"
|
||||
}
|
||||
],
|
||||
"constant": false,
|
||||
"payable": true,
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"name": "removeLiquidity",
|
||||
"outputs": [
|
||||
{
|
||||
"type": "uint256",
|
||||
"name": "outA"
|
||||
},
|
||||
{
|
||||
"type": "uint256",
|
||||
"name": "outB"
|
||||
}
|
||||
],
|
||||
"inputs": [
|
||||
{
|
||||
"type": "uint256",
|
||||
"name": "amount"
|
||||
},
|
||||
{
|
||||
"type": "uint256",
|
||||
"name": "min_eth"
|
||||
},
|
||||
{
|
||||
"type": "uint256",
|
||||
"name": "min_tokens"
|
||||
},
|
||||
{
|
||||
"type": "uint256",
|
||||
"name": "deadline"
|
||||
}
|
||||
],
|
||||
"constant": false,
|
||||
"payable": false,
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"name": "__default__",
|
||||
"outputs": [],
|
||||
"inputs": [],
|
||||
"constant": false,
|
||||
"payable": true,
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"name": "ethToTokenSwapInput",
|
||||
"outputs": [
|
||||
{
|
||||
"type": "uint256",
|
||||
"name": "out"
|
||||
}
|
||||
],
|
||||
"inputs": [
|
||||
{
|
||||
"type": "uint256",
|
||||
"name": "min_tokens"
|
||||
},
|
||||
{
|
||||
"type": "uint256",
|
||||
"name": "deadline"
|
||||
}
|
||||
],
|
||||
"constant": false,
|
||||
"payable": true,
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"name": "ethToTokenTransferInput",
|
||||
"outputs": [
|
||||
{
|
||||
"type": "uint256",
|
||||
"name": "out"
|
||||
}
|
||||
],
|
||||
"inputs": [
|
||||
{
|
||||
"type": "uint256",
|
||||
"name": "min_tokens"
|
||||
},
|
||||
{
|
||||
"type": "uint256",
|
||||
"name": "deadline"
|
||||
},
|
||||
{
|
||||
"type": "address",
|
||||
"name": "recipient"
|
||||
}
|
||||
],
|
||||
"constant": false,
|
||||
"payable": true,
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"name": "ethToTokenSwapOutput",
|
||||
"outputs": [
|
||||
{
|
||||
"type": "uint256",
|
||||
"name": "out"
|
||||
}
|
||||
],
|
||||
"inputs": [
|
||||
{
|
||||
"type": "uint256",
|
||||
"name": "tokens_bought"
|
||||
},
|
||||
{
|
||||
"type": "uint256",
|
||||
"name": "deadline"
|
||||
}
|
||||
],
|
||||
"constant": false,
|
||||
"payable": true,
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"name": "ethToTokenTransferOutput",
|
||||
"outputs": [
|
||||
{
|
||||
"type": "uint256",
|
||||
"name": "out"
|
||||
}
|
||||
],
|
||||
"inputs": [
|
||||
{
|
||||
"type": "uint256",
|
||||
"name": "tokens_bought"
|
||||
},
|
||||
{
|
||||
"type": "uint256",
|
||||
"name": "deadline"
|
||||
},
|
||||
{
|
||||
"type": "address",
|
||||
"name": "recipient"
|
||||
}
|
||||
],
|
||||
"constant": false,
|
||||
"payable": true,
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"name": "tokenToEthSwapInput",
|
||||
"outputs": [
|
||||
{
|
||||
"type": "uint256",
|
||||
"name": "out"
|
||||
}
|
||||
],
|
||||
"inputs": [
|
||||
{
|
||||
"type": "uint256",
|
||||
"name": "tokens_sold"
|
||||
},
|
||||
{
|
||||
"type": "uint256",
|
||||
"name": "min_eth"
|
||||
},
|
||||
{
|
||||
"type": "uint256",
|
||||
"name": "deadline"
|
||||
}
|
||||
],
|
||||
"constant": false,
|
||||
"payable": false,
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"name": "tokenToEthTransferInput",
|
||||
"outputs": [
|
||||
{
|
||||
"type": "uint256",
|
||||
"name": "out"
|
||||
}
|
||||
],
|
||||
"inputs": [
|
||||
{
|
||||
"type": "uint256",
|
||||
"name": "tokens_sold"
|
||||
},
|
||||
{
|
||||
"type": "uint256",
|
||||
"name": "min_eth"
|
||||
},
|
||||
{
|
||||
"type": "uint256",
|
||||
"name": "deadline"
|
||||
},
|
||||
{
|
||||
"type": "address",
|
||||
"name": "recipient"
|
||||
}
|
||||
],
|
||||
"constant": false,
|
||||
"payable": false,
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"name": "tokenToEthSwapOutput",
|
||||
"outputs": [
|
||||
{
|
||||
"type": "uint256",
|
||||
"name": "out"
|
||||
}
|
||||
],
|
||||
"inputs": [
|
||||
{
|
||||
"type": "uint256",
|
||||
"name": "eth_bought"
|
||||
},
|
||||
{
|
||||
"type": "uint256",
|
||||
"name": "max_tokens"
|
||||
},
|
||||
{
|
||||
"type": "uint256",
|
||||
"name": "deadline"
|
||||
}
|
||||
],
|
||||
"constant": false,
|
||||
"payable": false,
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"name": "tokenToEthTransferOutput",
|
||||
"outputs": [
|
||||
{
|
||||
"type": "uint256",
|
||||
"name": "out"
|
||||
}
|
||||
],
|
||||
"inputs": [
|
||||
{
|
||||
"type": "uint256",
|
||||
"name": "eth_bought"
|
||||
},
|
||||
{
|
||||
"type": "uint256",
|
||||
"name": "max_tokens"
|
||||
},
|
||||
{
|
||||
"type": "uint256",
|
||||
"name": "deadline"
|
||||
},
|
||||
{
|
||||
"type": "address",
|
||||
"name": "recipient"
|
||||
}
|
||||
],
|
||||
"constant": false,
|
||||
"payable": false,
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"name": "tokenToTokenSwapInput",
|
||||
"outputs": [
|
||||
{
|
||||
"type": "uint256",
|
||||
"name": "out"
|
||||
}
|
||||
],
|
||||
"inputs": [
|
||||
{
|
||||
"type": "uint256",
|
||||
"name": "tokens_sold"
|
||||
},
|
||||
{
|
||||
"type": "uint256",
|
||||
"name": "min_tokens_bought"
|
||||
},
|
||||
{
|
||||
"type": "uint256",
|
||||
"name": "min_eth_bought"
|
||||
},
|
||||
{
|
||||
"type": "uint256",
|
||||
"name": "deadline"
|
||||
},
|
||||
{
|
||||
"type": "address",
|
||||
"name": "token_addr"
|
||||
}
|
||||
],
|
||||
"constant": false,
|
||||
"payable": false,
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"name": "tokenToTokenTransferInput",
|
||||
"outputs": [
|
||||
{
|
||||
"type": "uint256",
|
||||
"name": "out"
|
||||
}
|
||||
],
|
||||
"inputs": [
|
||||
{
|
||||
"type": "uint256",
|
||||
"name": "tokens_sold"
|
||||
},
|
||||
{
|
||||
"type": "uint256",
|
||||
"name": "min_tokens_bought"
|
||||
},
|
||||
{
|
||||
"type": "uint256",
|
||||
"name": "min_eth_bought"
|
||||
},
|
||||
{
|
||||
"type": "uint256",
|
||||
"name": "deadline"
|
||||
},
|
||||
{
|
||||
"type": "address",
|
||||
"name": "recipient"
|
||||
},
|
||||
{
|
||||
"type": "address",
|
||||
"name": "token_addr"
|
||||
}
|
||||
],
|
||||
"constant": false,
|
||||
"payable": false,
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"name": "tokenToTokenSwapOutput",
|
||||
"outputs": [
|
||||
{
|
||||
"type": "uint256",
|
||||
"name": "out"
|
||||
}
|
||||
],
|
||||
"inputs": [
|
||||
{
|
||||
"type": "uint256",
|
||||
"name": "tokens_bought"
|
||||
},
|
||||
{
|
||||
"type": "uint256",
|
||||
"name": "max_tokens_sold"
|
||||
},
|
||||
{
|
||||
"type": "uint256",
|
||||
"name": "max_eth_sold"
|
||||
},
|
||||
{
|
||||
"type": "uint256",
|
||||
"name": "deadline"
|
||||
},
|
||||
{
|
||||
"type": "address",
|
||||
"name": "token_addr"
|
||||
}
|
||||
],
|
||||
"constant": false,
|
||||
"payable": false,
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"name": "tokenToTokenTransferOutput",
|
||||
"outputs": [
|
||||
{
|
||||
"type": "uint256",
|
||||
"name": "out"
|
||||
}
|
||||
],
|
||||
"inputs": [
|
||||
{
|
||||
"type": "uint256",
|
||||
"name": "tokens_bought"
|
||||
},
|
||||
{
|
||||
"type": "uint256",
|
||||
"name": "max_tokens_sold"
|
||||
},
|
||||
{
|
||||
"type": "uint256",
|
||||
"name": "max_eth_sold"
|
||||
},
|
||||
{
|
||||
"type": "uint256",
|
||||
"name": "deadline"
|
||||
},
|
||||
{
|
||||
"type": "address",
|
||||
"name": "recipient"
|
||||
},
|
||||
{
|
||||
"type": "address",
|
||||
"name": "token_addr"
|
||||
}
|
||||
],
|
||||
"constant": false,
|
||||
"payable": false,
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"name": "tokenToExchangeSwapInput",
|
||||
"outputs": [
|
||||
{
|
||||
"type": "uint256",
|
||||
"name": "out"
|
||||
}
|
||||
],
|
||||
"inputs": [
|
||||
{
|
||||
"type": "uint256",
|
||||
"name": "tokens_sold"
|
||||
},
|
||||
{
|
||||
"type": "uint256",
|
||||
"name": "min_tokens_bought"
|
||||
},
|
||||
{
|
||||
"type": "uint256",
|
||||
"name": "min_eth_bought"
|
||||
},
|
||||
{
|
||||
"type": "uint256",
|
||||
"name": "deadline"
|
||||
},
|
||||
{
|
||||
"type": "address",
|
||||
"name": "exchange_addr"
|
||||
}
|
||||
],
|
||||
"constant": false,
|
||||
"payable": false,
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"name": "tokenToExchangeTransferInput",
|
||||
"outputs": [
|
||||
{
|
||||
"type": "uint256",
|
||||
"name": "out"
|
||||
}
|
||||
],
|
||||
"inputs": [
|
||||
{
|
||||
"type": "uint256",
|
||||
"name": "tokens_sold"
|
||||
},
|
||||
{
|
||||
"type": "uint256",
|
||||
"name": "min_tokens_bought"
|
||||
},
|
||||
{
|
||||
"type": "uint256",
|
||||
"name": "min_eth_bought"
|
||||
},
|
||||
{
|
||||
"type": "uint256",
|
||||
"name": "deadline"
|
||||
},
|
||||
{
|
||||
"type": "address",
|
||||
"name": "recipient"
|
||||
},
|
||||
{
|
||||
"type": "address",
|
||||
"name": "exchange_addr"
|
||||
}
|
||||
],
|
||||
"constant": false,
|
||||
"payable": false,
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"name": "tokenToExchangeSwapOutput",
|
||||
"outputs": [
|
||||
{
|
||||
"type": "uint256",
|
||||
"name": "out"
|
||||
}
|
||||
],
|
||||
"inputs": [
|
||||
{
|
||||
"type": "uint256",
|
||||
"name": "tokens_bought"
|
||||
},
|
||||
{
|
||||
"type": "uint256",
|
||||
"name": "max_tokens_sold"
|
||||
},
|
||||
{
|
||||
"type": "uint256",
|
||||
"name": "max_eth_sold"
|
||||
},
|
||||
{
|
||||
"type": "uint256",
|
||||
"name": "deadline"
|
||||
},
|
||||
{
|
||||
"type": "address",
|
||||
"name": "exchange_addr"
|
||||
}
|
||||
],
|
||||
"constant": false,
|
||||
"payable": false,
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"name": "tokenToExchangeTransferOutput",
|
||||
"outputs": [
|
||||
{
|
||||
"type": "uint256",
|
||||
"name": "out"
|
||||
}
|
||||
],
|
||||
"inputs": [
|
||||
{
|
||||
"type": "uint256",
|
||||
"name": "tokens_bought"
|
||||
},
|
||||
{
|
||||
"type": "uint256",
|
||||
"name": "max_tokens_sold"
|
||||
},
|
||||
{
|
||||
"type": "uint256",
|
||||
"name": "max_eth_sold"
|
||||
},
|
||||
{
|
||||
"type": "uint256",
|
||||
"name": "deadline"
|
||||
},
|
||||
{
|
||||
"type": "address",
|
||||
"name": "recipient"
|
||||
},
|
||||
{
|
||||
"type": "address",
|
||||
"name": "exchange_addr"
|
||||
}
|
||||
],
|
||||
"constant": false,
|
||||
"payable": false,
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"name": "getEthToTokenInputPrice",
|
||||
"outputs": [
|
||||
{
|
||||
"type": "uint256",
|
||||
"name": "out"
|
||||
}
|
||||
],
|
||||
"inputs": [
|
||||
{
|
||||
"type": "uint256",
|
||||
"name": "eth_sold"
|
||||
}
|
||||
],
|
||||
"constant": true,
|
||||
"payable": false,
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"name": "getEthToTokenOutputPrice",
|
||||
"outputs": [
|
||||
{
|
||||
"type": "uint256",
|
||||
"name": "out"
|
||||
}
|
||||
],
|
||||
"inputs": [
|
||||
{
|
||||
"type": "uint256",
|
||||
"name": "tokens_bought"
|
||||
}
|
||||
],
|
||||
"constant": true,
|
||||
"payable": false,
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"name": "getTokenToEthInputPrice",
|
||||
"outputs": [
|
||||
{
|
||||
"type": "uint256",
|
||||
"name": "out"
|
||||
}
|
||||
],
|
||||
"inputs": [
|
||||
{
|
||||
"type": "uint256",
|
||||
"name": "tokens_sold"
|
||||
}
|
||||
],
|
||||
"constant": true,
|
||||
"payable": false,
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"name": "getTokenToEthOutputPrice",
|
||||
"outputs": [
|
||||
{
|
||||
"type": "uint256",
|
||||
"name": "out"
|
||||
}
|
||||
],
|
||||
"inputs": [
|
||||
{
|
||||
"type": "uint256",
|
||||
"name": "eth_bought"
|
||||
}
|
||||
],
|
||||
"constant": true,
|
||||
"payable": false,
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"name": "tokenAddress",
|
||||
"outputs": [
|
||||
{
|
||||
"type": "address",
|
||||
"name": "out"
|
||||
}
|
||||
],
|
||||
"inputs": [],
|
||||
"constant": true,
|
||||
"payable": false,
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"name": "factoryAddress",
|
||||
"outputs": [
|
||||
{
|
||||
"type": "address",
|
||||
"name": "out"
|
||||
}
|
||||
],
|
||||
"inputs": [],
|
||||
"constant": true,
|
||||
"payable": false,
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"name": "balanceOf",
|
||||
"outputs": [
|
||||
{
|
||||
"type": "uint256",
|
||||
"name": "out"
|
||||
}
|
||||
],
|
||||
"inputs": [
|
||||
{
|
||||
"type": "address",
|
||||
"name": "_owner"
|
||||
}
|
||||
],
|
||||
"constant": true,
|
||||
"payable": false,
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"name": "transfer",
|
||||
"outputs": [
|
||||
{
|
||||
"type": "bool",
|
||||
"name": "out"
|
||||
}
|
||||
],
|
||||
"inputs": [
|
||||
{
|
||||
"type": "address",
|
||||
"name": "_to"
|
||||
},
|
||||
{
|
||||
"type": "uint256",
|
||||
"name": "_value"
|
||||
}
|
||||
],
|
||||
"constant": false,
|
||||
"payable": false,
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"name": "transferFrom",
|
||||
"outputs": [
|
||||
{
|
||||
"type": "bool",
|
||||
"name": "out"
|
||||
}
|
||||
],
|
||||
"inputs": [
|
||||
{
|
||||
"type": "address",
|
||||
"name": "_from"
|
||||
},
|
||||
{
|
||||
"type": "address",
|
||||
"name": "_to"
|
||||
},
|
||||
{
|
||||
"type": "uint256",
|
||||
"name": "_value"
|
||||
}
|
||||
],
|
||||
"constant": false,
|
||||
"payable": false,
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"name": "approve",
|
||||
"outputs": [
|
||||
{
|
||||
"type": "bool",
|
||||
"name": "out"
|
||||
}
|
||||
],
|
||||
"inputs": [
|
||||
{
|
||||
"type": "address",
|
||||
"name": "_spender"
|
||||
},
|
||||
{
|
||||
"type": "uint256",
|
||||
"name": "_value"
|
||||
}
|
||||
],
|
||||
"constant": false,
|
||||
"payable": false,
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"name": "allowance",
|
||||
"outputs": [
|
||||
{
|
||||
"type": "uint256",
|
||||
"name": "out"
|
||||
}
|
||||
],
|
||||
"inputs": [
|
||||
{
|
||||
"type": "address",
|
||||
"name": "_owner"
|
||||
},
|
||||
{
|
||||
"type": "address",
|
||||
"name": "_spender"
|
||||
}
|
||||
],
|
||||
"constant": true,
|
||||
"payable": false,
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"name": "name",
|
||||
"outputs": [
|
||||
{
|
||||
"type": "bytes32",
|
||||
"name": "out"
|
||||
}
|
||||
],
|
||||
"inputs": [],
|
||||
"constant": true,
|
||||
"payable": false,
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"name": "symbol",
|
||||
"outputs": [
|
||||
{
|
||||
"type": "bytes32",
|
||||
"name": "out"
|
||||
}
|
||||
],
|
||||
"inputs": [],
|
||||
"constant": true,
|
||||
"payable": false,
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"name": "decimals",
|
||||
"outputs": [
|
||||
{
|
||||
"type": "uint256",
|
||||
"name": "out"
|
||||
}
|
||||
],
|
||||
"inputs": [],
|
||||
"constant": true,
|
||||
"payable": false,
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"name": "totalSupply",
|
||||
"outputs": [
|
||||
{
|
||||
"type": "uint256",
|
||||
"name": "out"
|
||||
}
|
||||
],
|
||||
"inputs": [],
|
||||
"constant": true,
|
||||
"payable": false,
|
||||
"type": "function"
|
||||
}
|
||||
]
|
||||
@@ -14,8 +14,7 @@
|
||||
"inputs": [{ "type": "address", "name": "template" }],
|
||||
"constant": false,
|
||||
"payable": false,
|
||||
"type": "function",
|
||||
"gas": 35725
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"name": "createExchange",
|
||||
@@ -23,8 +22,7 @@
|
||||
"inputs": [{ "type": "address", "name": "token" }],
|
||||
"constant": false,
|
||||
"payable": false,
|
||||
"type": "function",
|
||||
"gas": 187911
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"name": "getExchange",
|
||||
@@ -32,8 +30,7 @@
|
||||
"inputs": [{ "type": "address", "name": "token" }],
|
||||
"constant": true,
|
||||
"payable": false,
|
||||
"type": "function",
|
||||
"gas": 715
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"name": "getToken",
|
||||
@@ -41,8 +38,7 @@
|
||||
"inputs": [{ "type": "address", "name": "exchange" }],
|
||||
"constant": true,
|
||||
"payable": false,
|
||||
"type": "function",
|
||||
"gas": 745
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"name": "getTokenWithId",
|
||||
@@ -50,8 +46,7 @@
|
||||
"inputs": [{ "type": "uint256", "name": "token_id" }],
|
||||
"constant": true,
|
||||
"payable": false,
|
||||
"type": "function",
|
||||
"gas": 736
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"name": "exchangeTemplate",
|
||||
@@ -59,8 +54,7 @@
|
||||
"inputs": [],
|
||||
"constant": true,
|
||||
"payable": false,
|
||||
"type": "function",
|
||||
"gas": 633
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"name": "tokenCount",
|
||||
@@ -68,7 +62,6 @@
|
||||
"inputs": [],
|
||||
"constant": true,
|
||||
"payable": false,
|
||||
"type": "function",
|
||||
"gas": 663
|
||||
"type": "function"
|
||||
}
|
||||
]
|
||||
@@ -8,7 +8,7 @@ export function useTokenAllowance(token?: Token, owner?: string, spender?: strin
|
||||
const contract = useTokenContract(token?.address, false)
|
||||
|
||||
const inputs = useMemo(() => [owner, spender], [owner, spender])
|
||||
const allowance = useSingleCallResult(contract, 'allowance', inputs)
|
||||
const allowance = useSingleCallResult(contract, 'allowance', inputs).result
|
||||
|
||||
return useMemo(() => (token && allowance ? new TokenAmount(token, allowance.toString()) : undefined), [
|
||||
token,
|
||||
|
||||
@@ -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
|
||||
@@ -12,13 +14,40 @@ import { useSingleCallResult } from '../state/multicall/hooks'
|
||||
export function usePair(tokenA?: Token, tokenB?: Token): undefined | Pair | null {
|
||||
const pairAddress = tokenA && tokenB && !tokenA.equals(tokenB) ? Pair.getAddress(tokenA, tokenB) : undefined
|
||||
const contract = usePairContract(pairAddress, false)
|
||||
const reserves = useSingleCallResult(contract, 'getReserves')
|
||||
const { result: reserves, loading } = useSingleCallResult(contract, 'getReserves')
|
||||
|
||||
return useMemo(() => {
|
||||
if (!pairAddress || !contract || !tokenA || !tokenB) return undefined
|
||||
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()))
|
||||
}, [contract, pairAddress, reserves, tokenA, tokenB])
|
||||
}, [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])
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ import { useSingleCallResult } from '../state/multicall/hooks'
|
||||
export function useTotalSupply(token?: Token): TokenAmount | undefined {
|
||||
const contract = useTokenContract(token?.address, false)
|
||||
|
||||
const totalSupply: BigNumber = useSingleCallResult(contract, 'totalSupply')?.[0]
|
||||
const totalSupply: BigNumber = useSingleCallResult(contract, 'totalSupply')?.result?.[0]
|
||||
|
||||
return token && totalSupply ? new TokenAmount(token, totalSupply.toString()) : undefined
|
||||
}
|
||||
|
||||
@@ -1,18 +1,23 @@
|
||||
import { ChainId, Pair, Percent, Route, Token, TokenAmount, Trade, TradeType, WETH } from '@uniswap/sdk'
|
||||
import { ChainId, JSBI, Pair, Percent, Route, Token, TokenAmount, Trade, TradeType, WETH } from '@uniswap/sdk'
|
||||
import { useMemo } from 'react'
|
||||
import { useActiveWeb3React } from '../hooks'
|
||||
import { useAllTokens } from '../hooks/Tokens'
|
||||
import { useV1FactoryContract } from '../hooks/useContract'
|
||||
import { useSingleCallResult } from '../state/multicall/hooks'
|
||||
import { useETHBalances, useTokenBalance } from '../state/wallet/hooks'
|
||||
import { NEVER_RELOAD, useSingleCallResult, useSingleContractMultipleData } from '../state/multicall/hooks'
|
||||
import { useETHBalances, useTokenBalance, useTokenBalances } from '../state/wallet/hooks'
|
||||
|
||||
function useV1PairAddress(tokenAddress?: string): string | undefined {
|
||||
const contract = useV1FactoryContract()
|
||||
|
||||
const inputs = useMemo(() => [tokenAddress], [tokenAddress])
|
||||
return useSingleCallResult(contract, 'getExchange', inputs)?.[0]
|
||||
return useSingleCallResult(contract, 'getExchange', inputs)?.result?.[0]
|
||||
}
|
||||
|
||||
function useMockV1Pair(token?: Token) {
|
||||
class MockV1Pair extends Pair {
|
||||
readonly isV1: true = true
|
||||
}
|
||||
|
||||
function useMockV1Pair(token?: Token): MockV1Pair | undefined {
|
||||
const isWETH = token?.equals(WETH[token?.chainId])
|
||||
|
||||
// will only return an address on mainnet, and not for WETH
|
||||
@@ -21,10 +26,54 @@ function useMockV1Pair(token?: Token) {
|
||||
const ETHBalance = useETHBalances([v1PairAddress])[v1PairAddress ?? '']
|
||||
|
||||
return tokenBalance && ETHBalance && token
|
||||
? new Pair(tokenBalance, new TokenAmount(WETH[token.chainId], ETHBalance.toString()))
|
||||
? new MockV1Pair(tokenBalance, new TokenAmount(WETH[token.chainId], ETHBalance.toString()))
|
||||
: undefined
|
||||
}
|
||||
|
||||
// returns all v1 exchange addresses in the user's token list
|
||||
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?.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 useUserHasLiquidityInAllTokens(): boolean | undefined {
|
||||
const exchanges = useAllTokenV1Exchanges()
|
||||
|
||||
const { account, chainId } = useActiveWeb3React()
|
||||
|
||||
const fakeLiquidityTokens = useMemo(
|
||||
() => (chainId ? Object.keys(exchanges).map(address => new Token(chainId, address, 18, 'UNI-V1')) : []),
|
||||
[chainId, exchanges]
|
||||
)
|
||||
|
||||
const balances = useTokenBalances(account ?? undefined, fakeLiquidityTokens)
|
||||
|
||||
return useMemo(
|
||||
() =>
|
||||
Object.keys(balances).some(tokenAddress => {
|
||||
const b = balances[tokenAddress]?.raw
|
||||
return b && JSBI.greaterThan(b, JSBI.BigInt(0))
|
||||
}),
|
||||
[balances]
|
||||
)
|
||||
}
|
||||
|
||||
export function useV1TradeLinkIfBetter(
|
||||
isExactIn?: boolean,
|
||||
input?: Token,
|
||||
|
||||
@@ -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,9 +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 { V1_FACTORY_ADDRESS } from '../constants'
|
||||
import { ERC20_BYTES32_ABI } from '../constants/abis/erc20'
|
||||
import ERC20_ABI from '../constants/abis/erc20.json'
|
||||
import IUniswapV1Factory from '../constants/abis/v1_factory.json'
|
||||
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,13 +26,26 @@ function useContract(address?: string, ABI?: any, withSignerIfPossible = true):
|
||||
}
|
||||
|
||||
export function useV1FactoryContract(): Contract | null {
|
||||
return useContract(V1_FACTORY_ADDRESS, IUniswapV1Factory, false)
|
||||
const { chainId } = useActiveWeb3React()
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -40,6 +40,9 @@ function getSwapType(tokens: { [field in Field]?: Token }, isExactIn: boolean, c
|
||||
}
|
||||
}
|
||||
|
||||
// list of checksummed addresses that are forced to go through the FoT methods
|
||||
const FORCED_FOT_TOKENS = ['0xF0FAC7104aAC544e4a7CE1A55ADF2B5a25c65bD1']
|
||||
|
||||
// returns a function that will execute a swap, if the parameters are all valid
|
||||
// and the user has approved the slippage adjusted input amount for the trade
|
||||
export function useSwapCallback(
|
||||
@@ -81,6 +84,7 @@ export function useSwapCallback(
|
||||
const routerContract: Contract = getRouterContract(chainId, library, account)
|
||||
|
||||
const path = trade.route.path.map(t => t.address)
|
||||
const isForcedFOT: boolean = path.some(tokenAddress => FORCED_FOT_TOKENS.indexOf(tokenAddress) !== -1)
|
||||
|
||||
const deadlineFromNow: number = Math.ceil(Date.now() / 1000) + deadline
|
||||
|
||||
@@ -90,11 +94,15 @@ 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 = isForcedFOT
|
||||
? ['swapExactTokensForTokensSupportingFeeOnTransferTokens']
|
||||
: ['swapExactTokensForTokens', 'swapExactTokensForTokensSupportingFeeOnTransferTokens']
|
||||
args = [
|
||||
slippageAdjustedInput.raw.toString(),
|
||||
slippageAdjustedOutput.raw.toString(),
|
||||
@@ -102,11 +110,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 +120,16 @@ export function useSwapCallback(
|
||||
recipient,
|
||||
deadlineFromNow
|
||||
]
|
||||
value = null
|
||||
break
|
||||
case SwapType.EXACT_ETH_FOR_TOKENS:
|
||||
estimate = routerContract.estimateGas.swapExactETHForTokens
|
||||
method = routerContract.swapExactETHForTokens
|
||||
methodNames = isForcedFOT
|
||||
? ['swapExactETHForTokensSupportingFeeOnTransferTokens']
|
||||
: ['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 +137,11 @@ export function useSwapCallback(
|
||||
recipient,
|
||||
deadlineFromNow
|
||||
]
|
||||
value = null
|
||||
break
|
||||
case SwapType.EXACT_TOKENS_FOR_ETH:
|
||||
estimate = routerContract.estimateGas.swapExactTokensForETH
|
||||
method = routerContract.swapExactTokensForETH
|
||||
methodNames = isForcedFOT
|
||||
? ['swapExactTokensForETHSupportingFeeOnTransferTokens']
|
||||
: ['swapExactTokensForETH', 'swapExactTokensForETHSupportingFeeOnTransferTokens']
|
||||
args = [
|
||||
slippageAdjustedInput.raw.toString(),
|
||||
slippageAdjustedOutput.raw.toString(),
|
||||
@@ -144,58 +149,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>
|
||||
)
|
||||
}
|
||||
144
src/pages/MigrateV1/index.tsx
Normal file
144
src/pages/MigrateV1/index.tsx
Normal file
@@ -0,0 +1,144 @@
|
||||
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 { 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 POOL_TOKEN_AMOUNT_MIN = new Fraction(JSBI.BigInt(1), JSBI.BigInt(1000000))
|
||||
|
||||
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 allV1Exchanges = useAllTokenV1Exchanges()
|
||||
|
||||
const v1LiquidityTokens: Token[] = useMemo(() => {
|
||||
return Object.keys(allV1Exchanges).map(exchangeAddress => new Token(chainId, exchangeAddress, 18))
|
||||
}, [chainId, allV1Exchanges])
|
||||
|
||||
const v1LiquidityBalances = useTokenBalances(account, v1LiquidityTokens)
|
||||
|
||||
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(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]
|
||||
)
|
||||
|
||||
const theme = useContext(ThemeContext)
|
||||
|
||||
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,8 +6,9 @@ import { RouteComponentProps } from 'react-router-dom'
|
||||
import Question from '../../components/QuestionHelper'
|
||||
import SearchModal from '../../components/SearchModal'
|
||||
import PositionCard from '../../components/PositionCard'
|
||||
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'
|
||||
@@ -58,6 +59,8 @@ export default function Pool({ history }: RouteComponentProps) {
|
||||
return <PositionCardWrapper key={i} dummyPair={pair} />
|
||||
})
|
||||
|
||||
const hasV1Liquidity = useUserHasLiquidityInAllTokens()
|
||||
|
||||
return (
|
||||
<AppBody>
|
||||
<AutoColumn gap="lg" justify="center">
|
||||
@@ -92,15 +95,18 @@ export default function Pool({ history }: RouteComponentProps) {
|
||||
)}
|
||||
{filteredExchangeList}
|
||||
<Text textAlign="center" fontSize={14} style={{ padding: '.5rem 0 .5rem 0' }}>
|
||||
{filteredExchangeList?.length !== 0 ? `Don't see a pool you joined? ` : 'Already joined a pool? '}{' '}
|
||||
<Link
|
||||
id="import-pool-link"
|
||||
onClick={() => {
|
||||
history.push('/find')
|
||||
}}
|
||||
>
|
||||
Import it.
|
||||
</Link>
|
||||
{!hasV1Liquidity ? (
|
||||
<>
|
||||
{filteredExchangeList?.length !== 0 ? `Don't see a pool you joined? ` : 'Already joined a pool? '}{' '}
|
||||
<StyledInternalLink id="import-pool-link" to="/find">
|
||||
Import it.
|
||||
</StyledInternalLink>
|
||||
</>
|
||||
) : (
|
||||
<StyledInternalLink id="migrate-v1-liquidity-link" to="/migrate/v1">
|
||||
Migrate your V1 liquidity.
|
||||
</StyledInternalLink>
|
||||
)}
|
||||
</Text>
|
||||
</AutoColumn>
|
||||
<FixedBottom>
|
||||
|
||||
@@ -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,23 +22,31 @@ 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]
|
||||
}
|
||||
}
|
||||
|
||||
export const addMulticallListeners = createAction<{ chainId: number; calls: Call[] }>('addMulticallListeners')
|
||||
export const removeMulticallListeners = createAction<{ chainId: number; calls: Call[] }>('removeMulticallListeners')
|
||||
export interface ListenerOptions {
|
||||
// how often this data should be fetched, by default 1
|
||||
readonly blocksPerFetch?: number
|
||||
}
|
||||
|
||||
export const addMulticallListeners = createAction<{ chainId: number; calls: Call[]; options?: ListenerOptions }>(
|
||||
'addMulticallListeners'
|
||||
)
|
||||
export const removeMulticallListeners = createAction<{ chainId: number; calls: Call[]; options?: ListenerOptions }>(
|
||||
'removeMulticallListeners'
|
||||
)
|
||||
export const fetchingMulticallResults = createAction<{ chainId: number; calls: Call[]; fetchingBlockNumber: number }>(
|
||||
'fetchingMulticallResults'
|
||||
)
|
||||
export const errorFetchingMulticallResults = createAction<{
|
||||
chainId: number
|
||||
calls: Call[]
|
||||
fetchingBlockNumber: number
|
||||
}>('errorFetchingMulticallResults')
|
||||
export const updateMulticallResults = createAction<{
|
||||
chainId: number
|
||||
blockNumber: number
|
||||
|
||||
@@ -1,12 +1,19 @@
|
||||
import { Interface } from '@ethersproject/abi'
|
||||
import { Interface, FunctionFragment } from '@ethersproject/abi'
|
||||
import { BigNumber } from '@ethersproject/bignumber'
|
||||
import { Contract } from '@ethersproject/contracts'
|
||||
import { useEffect, useMemo } from 'react'
|
||||
import { useDispatch, useSelector } from 'react-redux'
|
||||
import { useActiveWeb3React } from '../../hooks'
|
||||
import useDebounce from '../../hooks/useDebounce'
|
||||
import { useBlockNumber } from '../application/hooks'
|
||||
import { AppDispatch, AppState } from '../index'
|
||||
import { addMulticallListeners, Call, removeMulticallListeners, parseCallKey, toCallKey } from './actions'
|
||||
import {
|
||||
addMulticallListeners,
|
||||
Call,
|
||||
removeMulticallListeners,
|
||||
parseCallKey,
|
||||
toCallKey,
|
||||
ListenerOptions
|
||||
} from './actions'
|
||||
|
||||
export interface Result extends ReadonlyArray<any> {
|
||||
readonly [key: string]: any
|
||||
@@ -27,8 +34,21 @@ function isValidMethodArgs(x: unknown): x is MethodArgs | undefined {
|
||||
)
|
||||
}
|
||||
|
||||
interface CallResult {
|
||||
readonly valid: boolean
|
||||
readonly data: string | undefined
|
||||
readonly blockNumber: number | undefined
|
||||
}
|
||||
|
||||
const INVALID_RESULT: CallResult = { valid: false, blockNumber: undefined, data: undefined }
|
||||
|
||||
// use this options object
|
||||
export const NEVER_RELOAD: ListenerOptions = {
|
||||
blocksPerFetch: Infinity
|
||||
}
|
||||
|
||||
// the lowest level call for subscribing to contract data
|
||||
function useCallsData(calls: (Call | undefined)[]): (string | undefined)[] {
|
||||
function useCallsData(calls: (Call | undefined)[], options?: ListenerOptions): CallResult[] {
|
||||
const { chainId } = useActiveWeb3React()
|
||||
const callResults = useSelector<AppState, AppState['multicall']['callResults']>(state => state.multicall.callResults)
|
||||
const dispatch = useDispatch<AppDispatch>()
|
||||
@@ -44,17 +64,16 @@ function useCallsData(calls: (Call | undefined)[]): (string | undefined)[] {
|
||||
[calls]
|
||||
)
|
||||
|
||||
const debouncedSerializedCallKeys = useDebounce(serializedCallKeys, 20)
|
||||
|
||||
// update listeners when there is an actual change that persists for at least 100ms
|
||||
useEffect(() => {
|
||||
const callKeys: string[] = JSON.parse(debouncedSerializedCallKeys)
|
||||
const callKeys: string[] = JSON.parse(serializedCallKeys)
|
||||
if (!chainId || callKeys.length === 0) return
|
||||
const calls = callKeys.map(key => parseCallKey(key))
|
||||
dispatch(
|
||||
addMulticallListeners({
|
||||
chainId,
|
||||
calls
|
||||
calls,
|
||||
options
|
||||
})
|
||||
)
|
||||
|
||||
@@ -62,31 +81,88 @@ function useCallsData(calls: (Call | undefined)[]): (string | undefined)[] {
|
||||
dispatch(
|
||||
removeMulticallListeners({
|
||||
chainId,
|
||||
calls
|
||||
calls,
|
||||
options
|
||||
})
|
||||
)
|
||||
}
|
||||
}, [chainId, dispatch, debouncedSerializedCallKeys])
|
||||
}, [chainId, dispatch, options, serializedCallKeys])
|
||||
|
||||
return useMemo(() => {
|
||||
return calls.map<string | undefined>(call => {
|
||||
if (!chainId || !call) return undefined
|
||||
return useMemo(
|
||||
() =>
|
||||
calls.map<CallResult>(call => {
|
||||
if (!chainId || !call) return INVALID_RESULT
|
||||
|
||||
const result = callResults[chainId]?.[toCallKey(call)]
|
||||
if (!result || !result.data || result.data === '0x') {
|
||||
return undefined
|
||||
const result = callResults[chainId]?.[toCallKey(call)]
|
||||
let data
|
||||
if (result?.data && result?.data !== '0x') {
|
||||
data = result.data
|
||||
}
|
||||
|
||||
return { valid: true, data, blockNumber: result?.blockNumber }
|
||||
}),
|
||||
[callResults, calls, chainId]
|
||||
)
|
||||
}
|
||||
|
||||
interface CallState {
|
||||
readonly valid: boolean
|
||||
// the result, or undefined if loading or errored/no data
|
||||
readonly result: Result | undefined
|
||||
// true if the result has never been fetched
|
||||
readonly loading: boolean
|
||||
// true if the result is not for the latest block
|
||||
readonly syncing: boolean
|
||||
// true if the call was made and is synced, but the return data is invalid
|
||||
readonly error: boolean
|
||||
}
|
||||
|
||||
const INVALID_CALL_STATE: CallState = { valid: false, result: undefined, loading: false, syncing: false, error: false }
|
||||
const LOADING_CALL_STATE: CallState = { valid: true, result: undefined, loading: true, syncing: true, error: false }
|
||||
|
||||
function toCallState(
|
||||
callResult: CallResult | undefined,
|
||||
contractInterface: Interface | undefined,
|
||||
fragment: FunctionFragment | undefined,
|
||||
latestBlockNumber: number | undefined
|
||||
): CallState {
|
||||
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 result.data
|
||||
})
|
||||
}, [callResults, calls, chainId])
|
||||
}
|
||||
}
|
||||
return {
|
||||
valid: true,
|
||||
loading: false,
|
||||
syncing,
|
||||
result: result,
|
||||
error: !success
|
||||
}
|
||||
}
|
||||
|
||||
export function useSingleContractMultipleData(
|
||||
contract: Contract | null | undefined,
|
||||
methodName: string,
|
||||
callInputs: OptionalMethodInputs[]
|
||||
): (Result | undefined)[] {
|
||||
callInputs: OptionalMethodInputs[],
|
||||
options?: ListenerOptions
|
||||
): CallState[] {
|
||||
const fragment = useMemo(() => contract?.interface?.getFunction(methodName), [contract, methodName])
|
||||
|
||||
const calls = useMemo(
|
||||
@@ -102,20 +178,22 @@ export function useSingleContractMultipleData(
|
||||
[callInputs, contract, fragment]
|
||||
)
|
||||
|
||||
const data = useCallsData(calls)
|
||||
const results = useCallsData(calls, options)
|
||||
|
||||
const latestBlockNumber = useBlockNumber()
|
||||
|
||||
return useMemo(() => {
|
||||
if (!fragment || !contract) return []
|
||||
return data.map(data => (data ? contract.interface.decodeFunctionResult(fragment, data) : undefined))
|
||||
}, [contract, data, fragment])
|
||||
return results.map(result => toCallState(result, contract?.interface, fragment, latestBlockNumber))
|
||||
}, [fragment, contract, results, latestBlockNumber])
|
||||
}
|
||||
|
||||
export function useMultipleContractSingleData(
|
||||
addresses: (string | undefined)[],
|
||||
contractInterface: Interface,
|
||||
methodName: string,
|
||||
callInputs?: OptionalMethodInputs
|
||||
): (Result | undefined)[] {
|
||||
callInputs?: OptionalMethodInputs,
|
||||
options?: ListenerOptions
|
||||
): CallState[] {
|
||||
const fragment = useMemo(() => contractInterface.getFunction(methodName), [contractInterface, methodName])
|
||||
const callData: string | undefined = useMemo(
|
||||
() =>
|
||||
@@ -140,19 +218,21 @@ export function useMultipleContractSingleData(
|
||||
[addresses, callData, fragment]
|
||||
)
|
||||
|
||||
const data = useCallsData(calls)
|
||||
const results = useCallsData(calls, options)
|
||||
|
||||
const latestBlockNumber = useBlockNumber()
|
||||
|
||||
return useMemo(() => {
|
||||
if (!fragment) return []
|
||||
return data.map(data => (data ? contractInterface.decodeFunctionResult(fragment, data) : undefined))
|
||||
}, [contractInterface, data, fragment])
|
||||
return results.map(result => toCallState(result, contractInterface, fragment, latestBlockNumber))
|
||||
}, [fragment, results, contractInterface, latestBlockNumber])
|
||||
}
|
||||
|
||||
export function useSingleCallResult(
|
||||
contract: Contract | null | undefined,
|
||||
methodName: string,
|
||||
inputs?: OptionalMethodInputs
|
||||
): Result | undefined {
|
||||
inputs?: OptionalMethodInputs,
|
||||
options?: ListenerOptions
|
||||
): CallState {
|
||||
const fragment = useMemo(() => contract?.interface?.getFunction(methodName), [contract, methodName])
|
||||
|
||||
const calls = useMemo<Call[]>(() => {
|
||||
@@ -166,9 +246,10 @@ export function useSingleCallResult(
|
||||
: []
|
||||
}, [contract, fragment, inputs])
|
||||
|
||||
const data = useCallsData(calls)[0]
|
||||
const result = useCallsData(calls, options)[0]
|
||||
const latestBlockNumber = useBlockNumber()
|
||||
|
||||
return useMemo(() => {
|
||||
if (!contract || !fragment || !data) return undefined
|
||||
return contract.interface.decodeFunctionResult(fragment, data)
|
||||
}, [data, fragment, contract])
|
||||
return toCallState(result, contract?.interface, fragment, latestBlockNumber)
|
||||
}, [result, contract, fragment, latestBlockNumber])
|
||||
}
|
||||
|
||||
172
src/state/multicall/reducer.test.ts
Normal file
172
src/state/multicall/reducer.test.ts
Normal file
@@ -0,0 +1,172 @@
|
||||
import { addMulticallListeners, removeMulticallListeners, updateMulticallResults } from './actions'
|
||||
import reducer, { MulticallState } from './reducer'
|
||||
import { Store, createStore } from '@reduxjs/toolkit'
|
||||
|
||||
const DAI_ADDRESS = '0x6b175474e89094c44da98b954eedeac495271d0f'
|
||||
|
||||
describe('multicall reducer', () => {
|
||||
let store: Store<MulticallState>
|
||||
beforeEach(() => {
|
||||
store = createStore(reducer)
|
||||
})
|
||||
|
||||
it('has correct initial state', () => {
|
||||
expect(store.getState().callResults).toEqual({})
|
||||
expect(store.getState().callListeners).toEqual(undefined)
|
||||
})
|
||||
|
||||
describe('addMulticallListeners', () => {
|
||||
it('adds listeners', () => {
|
||||
store.dispatch(
|
||||
addMulticallListeners({
|
||||
chainId: 1,
|
||||
calls: [
|
||||
{
|
||||
address: DAI_ADDRESS,
|
||||
callData: '0x'
|
||||
}
|
||||
]
|
||||
})
|
||||
)
|
||||
expect(store.getState()).toEqual({
|
||||
callListeners: {
|
||||
[1]: {
|
||||
[`${DAI_ADDRESS}-0x`]: {
|
||||
[1]: 1
|
||||
}
|
||||
}
|
||||
},
|
||||
callResults: {}
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('removeMulticallListeners', () => {
|
||||
it('noop', () => {
|
||||
store.dispatch(
|
||||
removeMulticallListeners({
|
||||
calls: [
|
||||
{
|
||||
address: DAI_ADDRESS,
|
||||
callData: '0x'
|
||||
}
|
||||
],
|
||||
chainId: 1
|
||||
})
|
||||
)
|
||||
expect(store.getState()).toEqual({ callResults: {}, callListeners: {} })
|
||||
})
|
||||
it('removes listeners', () => {
|
||||
store.dispatch(
|
||||
addMulticallListeners({
|
||||
chainId: 1,
|
||||
calls: [
|
||||
{
|
||||
address: DAI_ADDRESS,
|
||||
callData: '0x'
|
||||
}
|
||||
]
|
||||
})
|
||||
)
|
||||
store.dispatch(
|
||||
removeMulticallListeners({
|
||||
calls: [
|
||||
{
|
||||
address: DAI_ADDRESS,
|
||||
callData: '0x'
|
||||
}
|
||||
],
|
||||
chainId: 1
|
||||
})
|
||||
)
|
||||
expect(store.getState()).toEqual({
|
||||
callResults: {},
|
||||
callListeners: { [1]: { [`${DAI_ADDRESS}-0x`]: {} } }
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('updateMulticallResults', () => {
|
||||
it('updates data if not present', () => {
|
||||
store.dispatch(
|
||||
updateMulticallResults({
|
||||
chainId: 1,
|
||||
blockNumber: 1,
|
||||
results: {
|
||||
abc: '0x'
|
||||
}
|
||||
})
|
||||
)
|
||||
expect(store.getState()).toEqual({
|
||||
callResults: {
|
||||
[1]: {
|
||||
abc: {
|
||||
blockNumber: 1,
|
||||
data: '0x'
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
it('updates old data', () => {
|
||||
store.dispatch(
|
||||
updateMulticallResults({
|
||||
chainId: 1,
|
||||
blockNumber: 1,
|
||||
results: {
|
||||
abc: '0x'
|
||||
}
|
||||
})
|
||||
)
|
||||
store.dispatch(
|
||||
updateMulticallResults({
|
||||
chainId: 1,
|
||||
blockNumber: 2,
|
||||
results: {
|
||||
abc: '0x2'
|
||||
}
|
||||
})
|
||||
)
|
||||
expect(store.getState()).toEqual({
|
||||
callResults: {
|
||||
[1]: {
|
||||
abc: {
|
||||
blockNumber: 2,
|
||||
data: '0x2'
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
it('ignores late updates', () => {
|
||||
store.dispatch(
|
||||
updateMulticallResults({
|
||||
chainId: 1,
|
||||
blockNumber: 2,
|
||||
results: {
|
||||
abc: '0x2'
|
||||
}
|
||||
})
|
||||
)
|
||||
store.dispatch(
|
||||
updateMulticallResults({
|
||||
chainId: 1,
|
||||
blockNumber: 1,
|
||||
results: {
|
||||
abc: '0x1'
|
||||
}
|
||||
})
|
||||
)
|
||||
expect(store.getState()).toEqual({
|
||||
callResults: {
|
||||
[1]: {
|
||||
abc: {
|
||||
blockNumber: 2,
|
||||
data: '0x2'
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,53 +1,103 @@
|
||||
import { createReducer } from '@reduxjs/toolkit'
|
||||
import { addMulticallListeners, removeMulticallListeners, toCallKey, updateMulticallResults } from './actions'
|
||||
import {
|
||||
addMulticallListeners,
|
||||
errorFetchingMulticallResults,
|
||||
fetchingMulticallResults,
|
||||
removeMulticallListeners,
|
||||
toCallKey,
|
||||
updateMulticallResults
|
||||
} from './actions'
|
||||
|
||||
interface MulticallState {
|
||||
callListeners: {
|
||||
export interface MulticallState {
|
||||
callListeners?: {
|
||||
// on a per-chain basis
|
||||
[chainId: number]: {
|
||||
[callKey: string]: number
|
||||
// stores for each call key the listeners' preferences
|
||||
[callKey: string]: {
|
||||
// stores how many listeners there are per each blocks per fetch preference
|
||||
[blocksPerFetch: number]: number
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
callResults: {
|
||||
[chainId: number]: {
|
||||
[callKey: string]: {
|
||||
data: string | null
|
||||
blockNumber: number
|
||||
data?: string | null
|
||||
blockNumber?: number
|
||||
fetchingBlockNumber?: number
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const initialState: MulticallState = {
|
||||
callListeners: {},
|
||||
callResults: {}
|
||||
}
|
||||
|
||||
export default createReducer(initialState, builder =>
|
||||
builder
|
||||
.addCase(addMulticallListeners, (state, { payload: { calls, chainId } }) => {
|
||||
state.callListeners[chainId] = state.callListeners[chainId] ?? {}
|
||||
.addCase(addMulticallListeners, (state, { payload: { calls, chainId, options: { blocksPerFetch = 1 } = {} } }) => {
|
||||
const listeners: MulticallState['callListeners'] = state.callListeners
|
||||
? state.callListeners
|
||||
: (state.callListeners = {})
|
||||
listeners[chainId] = listeners[chainId] ?? {}
|
||||
calls.forEach(call => {
|
||||
const callKey = toCallKey(call)
|
||||
state.callListeners[chainId][callKey] = (state.callListeners[chainId][callKey] ?? 0) + 1
|
||||
listeners[chainId][callKey] = listeners[chainId][callKey] ?? {}
|
||||
listeners[chainId][callKey][blocksPerFetch] = (listeners[chainId][callKey][blocksPerFetch] ?? 0) + 1
|
||||
})
|
||||
})
|
||||
.addCase(removeMulticallListeners, (state, { payload: { chainId, calls } }) => {
|
||||
if (!state.callListeners[chainId]) return
|
||||
.addCase(
|
||||
removeMulticallListeners,
|
||||
(state, { payload: { chainId, calls, options: { blocksPerFetch = 1 } = {} } }) => {
|
||||
const listeners: MulticallState['callListeners'] = state.callListeners
|
||||
? state.callListeners
|
||||
: (state.callListeners = {})
|
||||
|
||||
if (!listeners[chainId]) return
|
||||
calls.forEach(call => {
|
||||
const callKey = toCallKey(call)
|
||||
if (!listeners[chainId][callKey]) return
|
||||
if (!listeners[chainId][callKey][blocksPerFetch]) return
|
||||
|
||||
if (listeners[chainId][callKey][blocksPerFetch] === 1) {
|
||||
delete listeners[chainId][callKey][blocksPerFetch]
|
||||
} else {
|
||||
listeners[chainId][callKey][blocksPerFetch]--
|
||||
}
|
||||
})
|
||||
}
|
||||
)
|
||||
.addCase(fetchingMulticallResults, (state, { payload: { chainId, fetchingBlockNumber, calls } }) => {
|
||||
state.callResults[chainId] = state.callResults[chainId] ?? {}
|
||||
calls.forEach(call => {
|
||||
const callKey = toCallKey(call)
|
||||
if (state.callListeners[chainId][callKey] === 1) {
|
||||
delete state.callListeners[chainId][callKey]
|
||||
const current = state.callResults[chainId][callKey]
|
||||
if (!current) {
|
||||
state.callResults[chainId][callKey] = {
|
||||
fetchingBlockNumber
|
||||
}
|
||||
} else {
|
||||
state.callListeners[chainId][callKey]--
|
||||
if (current.fetchingBlockNumber ?? 0 >= fetchingBlockNumber) return
|
||||
state.callResults[chainId][callKey].fetchingBlockNumber = fetchingBlockNumber
|
||||
}
|
||||
})
|
||||
})
|
||||
.addCase(errorFetchingMulticallResults, (state, { payload: { fetchingBlockNumber, chainId, calls } }) => {
|
||||
state.callResults[chainId] = state.callResults[chainId] ?? {}
|
||||
calls.forEach(call => {
|
||||
const callKey = toCallKey(call)
|
||||
const current = state.callResults[chainId][callKey]
|
||||
if (current && current.fetchingBlockNumber !== fetchingBlockNumber) return
|
||||
delete current.fetchingBlockNumber
|
||||
})
|
||||
})
|
||||
.addCase(updateMulticallResults, (state, { payload: { chainId, results, blockNumber } }) => {
|
||||
state.callResults[chainId] = state.callResults[chainId] ?? {}
|
||||
Object.keys(results).forEach(callKey => {
|
||||
const current = state.callResults[chainId][callKey]
|
||||
if (current && current.blockNumber > blockNumber) return
|
||||
if ((current?.blockNumber ?? 0) > blockNumber) return
|
||||
state.callResults[chainId][callKey] = {
|
||||
data: results[callKey],
|
||||
blockNumber
|
||||
|
||||
168
src/state/multicall/updater.test.ts
Normal file
168
src/state/multicall/updater.test.ts
Normal file
@@ -0,0 +1,168 @@
|
||||
import { activeListeningKeys, outdatedListeningKeys } from './updater'
|
||||
|
||||
describe('multicall updater', () => {
|
||||
describe('#activeListeningKeys', () => {
|
||||
it('ignores 0, returns call key to block age key', () => {
|
||||
expect(
|
||||
activeListeningKeys(
|
||||
{
|
||||
[1]: {
|
||||
['abc']: {
|
||||
4: 2, // 2 listeners care about 4 block old data
|
||||
1: 0 // 0 listeners care about 1 block old data
|
||||
}
|
||||
}
|
||||
},
|
||||
1
|
||||
)
|
||||
).toEqual({
|
||||
abc: 4
|
||||
})
|
||||
})
|
||||
it('applies min', () => {
|
||||
expect(
|
||||
activeListeningKeys(
|
||||
{
|
||||
[1]: {
|
||||
['abc']: {
|
||||
4: 2, // 2 listeners care about 4 block old data
|
||||
3: 1, // 1 listener cares about 3 block old data
|
||||
1: 0 // 0 listeners care about 1 block old data
|
||||
}
|
||||
}
|
||||
},
|
||||
1
|
||||
)
|
||||
).toEqual({
|
||||
abc: 3
|
||||
})
|
||||
})
|
||||
it('works for infinity', () => {
|
||||
expect(
|
||||
activeListeningKeys(
|
||||
{
|
||||
[1]: {
|
||||
['abc']: {
|
||||
4: 2, // 2 listeners care about 4 block old data
|
||||
1: 0 // 0 listeners care about 1 block old data
|
||||
},
|
||||
['def']: {
|
||||
Infinity: 2
|
||||
}
|
||||
}
|
||||
},
|
||||
1
|
||||
)
|
||||
).toEqual({
|
||||
abc: 4,
|
||||
def: Infinity
|
||||
})
|
||||
})
|
||||
it('multiple keys', () => {
|
||||
expect(
|
||||
activeListeningKeys(
|
||||
{
|
||||
[1]: {
|
||||
['abc']: {
|
||||
4: 2, // 2 listeners care about 4 block old data
|
||||
1: 0 // 0 listeners care about 1 block old data
|
||||
},
|
||||
['def']: {
|
||||
2: 1,
|
||||
5: 2
|
||||
}
|
||||
}
|
||||
},
|
||||
1
|
||||
)
|
||||
).toEqual({
|
||||
abc: 4,
|
||||
def: 2
|
||||
})
|
||||
})
|
||||
it('ignores negative numbers', () => {
|
||||
expect(
|
||||
activeListeningKeys(
|
||||
{
|
||||
[1]: {
|
||||
['abc']: {
|
||||
4: 2,
|
||||
1: -1,
|
||||
[-3]: 4
|
||||
}
|
||||
}
|
||||
},
|
||||
1
|
||||
)
|
||||
).toEqual({
|
||||
abc: 4
|
||||
})
|
||||
})
|
||||
it('applies min to infinity', () => {
|
||||
expect(
|
||||
activeListeningKeys(
|
||||
{
|
||||
[1]: {
|
||||
['abc']: {
|
||||
Infinity: 2, // 2 listeners care about any data
|
||||
4: 2, // 2 listeners care about 4 block old data
|
||||
1: 0 // 0 listeners care about 1 block old data
|
||||
}
|
||||
}
|
||||
},
|
||||
1
|
||||
)
|
||||
).toEqual({
|
||||
abc: 4
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('#outdatedListeningKeys', () => {
|
||||
it('returns empty if missing block number or chain id', () => {
|
||||
expect(outdatedListeningKeys({}, { abc: 2 }, undefined, undefined)).toEqual([])
|
||||
expect(outdatedListeningKeys({}, { abc: 2 }, 1, undefined)).toEqual([])
|
||||
expect(outdatedListeningKeys({}, { abc: 2 }, undefined, 1)).toEqual([])
|
||||
})
|
||||
it('returns everything for no results', () => {
|
||||
expect(outdatedListeningKeys({}, { abc: 2, def: 3 }, 1, 1)).toEqual(['abc', 'def'])
|
||||
})
|
||||
it('returns only outdated keys', () => {
|
||||
expect(
|
||||
outdatedListeningKeys({ [1]: { abc: { data: '0x', blockNumber: 2 } } }, { abc: 1, def: 1 }, 1, 2)
|
||||
).toEqual(['def'])
|
||||
})
|
||||
it('returns only keys not being fetched', () => {
|
||||
expect(
|
||||
outdatedListeningKeys(
|
||||
{
|
||||
[1]: { abc: { data: '0x', blockNumber: 2 }, def: { fetchingBlockNumber: 2 } }
|
||||
},
|
||||
{ abc: 1, def: 1 },
|
||||
1,
|
||||
2
|
||||
)
|
||||
).toEqual([])
|
||||
})
|
||||
it('returns keys being fetched for old blocks', () => {
|
||||
expect(
|
||||
outdatedListeningKeys(
|
||||
{ [1]: { abc: { data: '0x', blockNumber: 2 }, def: { fetchingBlockNumber: 1 } } },
|
||||
{ abc: 1, def: 1 },
|
||||
1,
|
||||
2
|
||||
)
|
||||
).toEqual(['def'])
|
||||
})
|
||||
it('respects blocks per fetch', () => {
|
||||
expect(
|
||||
outdatedListeningKeys(
|
||||
{ [1]: { abc: { data: '0x', blockNumber: 2 }, def: { data: '0x', fetchingBlockNumber: 1 } } },
|
||||
{ abc: 2, def: 2 },
|
||||
1,
|
||||
3
|
||||
)
|
||||
).toEqual(['def'])
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -7,48 +7,118 @@ import useDebounce from '../../hooks/useDebounce'
|
||||
import chunkArray from '../../utils/chunkArray'
|
||||
import { useBlockNumber } from '../application/hooks'
|
||||
import { AppDispatch, AppState } from '../index'
|
||||
import { parseCallKey, updateMulticallResults } from './actions'
|
||||
import {
|
||||
errorFetchingMulticallResults,
|
||||
fetchingMulticallResults,
|
||||
parseCallKey,
|
||||
updateMulticallResults
|
||||
} from './actions'
|
||||
|
||||
// chunk calls so we do not exceed the gas limit
|
||||
const CALL_CHUNK_SIZE = 250
|
||||
const CALL_CHUNK_SIZE = 500
|
||||
|
||||
/**
|
||||
* From the current all listeners state, return each call key mapped to the
|
||||
* minimum number of blocks per fetch. This is how often each key must be fetched.
|
||||
* @param allListeners the all listeners state
|
||||
* @param chainId the current chain id
|
||||
*/
|
||||
export function activeListeningKeys(
|
||||
allListeners: AppState['multicall']['callListeners'],
|
||||
chainId?: number
|
||||
): { [callKey: string]: number } {
|
||||
if (!allListeners || !chainId) return {}
|
||||
const listeners = allListeners[chainId]
|
||||
if (!listeners) return {}
|
||||
|
||||
return Object.keys(listeners).reduce<{ [callKey: string]: number }>((memo, callKey) => {
|
||||
const keyListeners = listeners[callKey]
|
||||
|
||||
memo[callKey] = Object.keys(keyListeners)
|
||||
.filter(key => {
|
||||
const blocksPerFetch = parseInt(key)
|
||||
if (blocksPerFetch <= 0) return false
|
||||
return keyListeners[blocksPerFetch] > 0
|
||||
})
|
||||
.reduce((previousMin, current) => {
|
||||
return Math.min(previousMin, parseInt(current))
|
||||
}, Infinity)
|
||||
return memo
|
||||
}, {})
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the keys that need to be refetched
|
||||
* @param callResults current call result state
|
||||
* @param listeningKeys each call key mapped to how old the data can be in blocks
|
||||
* @param chainId the current chain id
|
||||
* @param latestBlockNumber the latest block number
|
||||
*/
|
||||
export function outdatedListeningKeys(
|
||||
callResults: AppState['multicall']['callResults'],
|
||||
listeningKeys: { [callKey: string]: number },
|
||||
chainId: number | undefined,
|
||||
latestBlockNumber: number | undefined
|
||||
): string[] {
|
||||
if (!chainId || !latestBlockNumber) return []
|
||||
const results = callResults[chainId]
|
||||
// no results at all, load everything
|
||||
if (!results) return Object.keys(listeningKeys)
|
||||
|
||||
return Object.keys(listeningKeys).filter(callKey => {
|
||||
const blocksPerFetch = listeningKeys[callKey]
|
||||
|
||||
const data = callResults[chainId][callKey]
|
||||
// no data, must fetch
|
||||
if (!data) return true
|
||||
|
||||
const minDataBlockNumber = latestBlockNumber - (blocksPerFetch - 1)
|
||||
|
||||
// already fetching it for a recent enough block, don't refetch it
|
||||
if (data.fetchingBlockNumber && data.fetchingBlockNumber >= minDataBlockNumber) return false
|
||||
|
||||
// if data is newer than minDataBlockNumber, don't fetch it
|
||||
return !(data.blockNumber && data.blockNumber >= minDataBlockNumber)
|
||||
})
|
||||
}
|
||||
|
||||
export default function Updater() {
|
||||
const dispatch = useDispatch<AppDispatch>()
|
||||
const state = useSelector<AppState, AppState['multicall']>(state => state.multicall)
|
||||
// wait for listeners to settle before triggering updates
|
||||
const debouncedListeners = useDebounce(state.callListeners, 100)
|
||||
const latestBlockNumber = useBlockNumber()
|
||||
const { chainId } = useActiveWeb3React()
|
||||
const multicallContract = useMulticallContract()
|
||||
|
||||
const listeningKeys = useMemo(() => {
|
||||
if (!chainId || !state.callListeners[chainId]) return []
|
||||
return Object.keys(state.callListeners[chainId]).filter(callKey => state.callListeners[chainId][callKey] > 0)
|
||||
}, [state.callListeners, chainId])
|
||||
|
||||
const debouncedResults = useDebounce(state.callResults, 20)
|
||||
const debouncedListeningKeys = useDebounce(listeningKeys, 20)
|
||||
const listeningKeys: { [callKey: string]: number } = useMemo(() => {
|
||||
return activeListeningKeys(debouncedListeners, chainId)
|
||||
}, [debouncedListeners, chainId])
|
||||
|
||||
const unserializedOutdatedCallKeys = useMemo(() => {
|
||||
if (!chainId || !debouncedResults[chainId]) return debouncedListeningKeys
|
||||
if (!latestBlockNumber) return []
|
||||
|
||||
return debouncedListeningKeys.filter(key => {
|
||||
const data = debouncedResults[chainId][key]
|
||||
return !data || data.blockNumber < latestBlockNumber
|
||||
})
|
||||
}, [chainId, debouncedResults, debouncedListeningKeys, latestBlockNumber])
|
||||
return outdatedListeningKeys(state.callResults, listeningKeys, chainId, latestBlockNumber)
|
||||
}, [chainId, state.callResults, listeningKeys, latestBlockNumber])
|
||||
|
||||
const serializedOutdatedCallKeys = useMemo(() => JSON.stringify(unserializedOutdatedCallKeys.sort()), [
|
||||
unserializedOutdatedCallKeys
|
||||
])
|
||||
|
||||
useEffect(() => {
|
||||
if (!latestBlockNumber || !chainId || !multicallContract) return
|
||||
|
||||
const outdatedCallKeys: string[] = JSON.parse(serializedOutdatedCallKeys)
|
||||
if (!multicallContract || !chainId || outdatedCallKeys.length === 0) return
|
||||
if (outdatedCallKeys.length === 0) return
|
||||
const calls = outdatedCallKeys.map(key => parseCallKey(key))
|
||||
|
||||
const chunkedCalls = chunkArray(calls, CALL_CHUNK_SIZE)
|
||||
|
||||
console.debug('Firing off chunked calls', chunkedCalls)
|
||||
dispatch(
|
||||
fetchingMulticallResults({
|
||||
calls,
|
||||
chainId,
|
||||
fetchingBlockNumber: latestBlockNumber
|
||||
})
|
||||
)
|
||||
|
||||
chunkedCalls.forEach((chunk, index) =>
|
||||
multicallContract
|
||||
@@ -73,9 +143,16 @@ export default function Updater() {
|
||||
})
|
||||
.catch((error: any) => {
|
||||
console.error('Failed to fetch multicall chunk', chunk, chainId, error)
|
||||
dispatch(
|
||||
errorFetchingMulticallResults({
|
||||
calls: chunk,
|
||||
chainId,
|
||||
fetchingBlockNumber: latestBlockNumber
|
||||
})
|
||||
)
|
||||
})
|
||||
)
|
||||
}, [chainId, multicallContract, dispatch, serializedOutdatedCallKeys])
|
||||
}, [chainId, multicallContract, dispatch, serializedOutdatedCallKeys, latestBlockNumber])
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -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])
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ export function useETHBalances(uncheckedAddresses?: (string | undefined)[]): { [
|
||||
return useMemo(
|
||||
() =>
|
||||
addresses.reduce<{ [address: string]: JSBI | undefined }>((memo, address, i) => {
|
||||
const value = results?.[i]?.[0]
|
||||
const value = results?.[i]?.result?.[0]
|
||||
if (value) memo[address] = JSBI.BigInt(value.toString())
|
||||
return memo
|
||||
}, {}),
|
||||
@@ -61,7 +61,7 @@ export function useTokenBalances(
|
||||
() =>
|
||||
address && validatedTokens.length > 0
|
||||
? validatedTokens.reduce<{ [tokenAddress: string]: TokenAmount | undefined }>((memo, token, i) => {
|
||||
const value = balances?.[i]?.[0]
|
||||
const value = balances?.[i]?.result?.[0]
|
||||
const amount = value ? JSBI.BigInt(value.toString()) : undefined
|
||||
if (amount) {
|
||||
memo[token.address] = new TokenAmount(token, amount)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user