Compare commits

...

38 Commits

Author SHA1 Message Date
Moody Salem
97deebad37 temporary workaround for tokens that have transfer methods that succeed but do not transfer tokens 2020-06-06 08:41:31 -04:00
Noah Zinsmeister
e667615449 support proposed new FoT-capable methods (#866)
* support new FoT-capable methods

short-circuit modal footer rendering if no trade

improve clarity of tx modal flow

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

* delete the bytes32 overload

* console log statement

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

* fix to work with bytes32 symbols/names

* only include name/symbol

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

* print all the details of the liquidity

* show working approve/migrate buttons

* testnet v1 factory addresses

* split code up into two pages

* getting closer to styled

* compute min amount out of eth and token

* compute min amount out of eth and token

* add a back button to the list page

* Improve empty states

* Improve the state management

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

* style fix, pending transaction hook fix

* add forwarding to netlify.toml

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

* make ternaries more accurate

* handle first liquidity provider situation

* Style tweaks for migrate

* merge

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

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

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

pin some pairs
2020-06-01 16:55:31 -04:00
Moody Salem
aadf43efc3 chore(v1): add ipfs links for v1 to the v1 release 2020-06-01 11:20:05 -04:00
Noah Zinsmeister
227f729ecd add AST and EBASE 2020-06-01 10:53:40 -04:00
Moody Salem
a5b15e37f6 fix(application state): fix block number updates after changing networks 2020-05-31 15:33:31 -04:00
Moody Salem
2408b2966e fix(misc): fix styling on the having trouble link 2020-05-31 00:24:03 -04:00
Moody Salem
dc391d1bea fix(misc): fix the blinking ethereum logo on ios safari and temporarily remove the overflow settings to fix double scrolling situation 2020-05-31 00:15:17 -04:00
Moody Salem
e2d0514344 chore(cleanup): separate the components for 3 kinds of links we use 2020-05-30 23:33:16 -04:00
Moody Salem
98d25dd2af perf(cleanup): remove the material ui dependency for the input range 2020-05-30 21:22:59 -04:00
Moody Salem
f289dec684 fix(multicall reducer): add test and fix update multicall results 2020-05-30 03:10:49 -04:00
Moody Salem
73d3df05f2 perf(network connector): use an inline network connector until improved connector is pushed upstream 2020-05-30 02:03:02 -04:00
Moody Salem
83554f44f8 perf(multicall): add unit tests and fix a bug (#845)
* start with the migrate page

* Add a bunch of tests and bump up the call size

* Show a link to the old portal, disable the WIP page

* Fix lint error
2020-05-29 20:07:18 -04:00
Moody Salem
320b2e384b chore(constant): update token list 2020-05-29 15:20:23 -04:00
Moody Salem
9492e7375a chore(multicall/migrate): move some v1 stuff around for migrate 2020-05-29 15:08:07 -04:00
Moody Salem
8a6a10be9d chore(multicall): lint error 2020-05-29 13:25:55 -04:00
Moody Salem
5e486fca7f perf(multicall): improve fetching code to allow for fetching immutable data like token symbols/names/decimals 2020-05-29 13:23:49 -04:00
Moody Salem
87d24c404b fix(multicall): v1 pair lookup 2020-05-29 11:52:20 -04:00
Moody Salem
d4011f73d1 fix(multicall): return loading states from the multicall hooks #842 2020-05-29 11:48:33 -04:00
Moody Salem
6fc3157977 chore(strict): strict connectors directory 2020-05-29 11:05:54 -04:00
97 changed files with 3896 additions and 4394 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -13,7 +13,6 @@
"@ethersproject/strings": "^5.0.0-beta.136",
"@ethersproject/units": "^5.0.0-beta.132",
"@ethersproject/wallet": "^5.0.0-beta.141",
"@material-ui/core": "^4.9.5",
"@popperjs/core": "^2.4.0",
"@reach/dialog": "^0.10.3",
"@reach/portal": "^0.10.3",
@@ -34,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
View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

View File

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

Before

Width:  |  Height:  |  Size: 5.2 KiB

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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()
}
}
}

View 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
View File

@@ -0,0 +1 @@
declare module 'formatic'

View File

@@ -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]
})

View File

@@ -0,0 +1,4 @@
{
"extends": "../../tsconfig.strict.json",
"include": ["**/*"]
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -7,6 +7,7 @@ export default [
new Token(ChainId.MAINNET, '0xD46bA6D942050d489DBd938a2C909A5d5039A161', 9, 'AMPL', 'Ampleforth'),
new Token(ChainId.MAINNET, '0xcD62b1C403fa761BAadFC74C525ce2B51780b184', 18, 'ANJ', 'Aragon Network Juror'),
new Token(ChainId.MAINNET, '0x960b236A07cf122663c4303350609A66A7B288C0', 18, 'ANT', 'Aragon Network Token'),
new Token(ChainId.MAINNET, '0x27054b13b1B798B345b591a4d22e6562d47eA75a', 4, 'AST', 'AirSwap Token'),
new Token(ChainId.MAINNET, '0xBA11D00c5f74255f56a5E366F4F77f5A186d7f55', 18, 'BAND', 'BandToken'),
new Token(ChainId.MAINNET, '0x0D8775F648430679A709E98d2b0Cb6250d2887EF', 18, 'BAT', 'Basic Attention Token'),
new Token(ChainId.MAINNET, '0x107c4504cd79C5d2696Ea0030a8dD4e92601B82e', 18, 'BLT', 'Bloom Token'),
@@ -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
View 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 }

View 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"
}
]

View File

@@ -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"
}
]

View File

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

View File

@@ -1,8 +1,10 @@
import { Token, TokenAmount, Pair } from '@uniswap/sdk'
import { useMemo } from 'react'
import { abi as IUniswapV2PairABI } from '@uniswap/v2-core/build/IUniswapV2Pair.json'
import { Interface } from '@ethersproject/abi'
import { usePairContract } from '../hooks/useContract'
import { useSingleCallResult } from '../state/multicall/hooks'
import { useSingleCallResult, useMultipleContractSingleData } from '../state/multicall/hooks'
/*
* if loading, return undefined
@@ -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])
}

View File

@@ -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
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -2,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)
}

View File

@@ -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])
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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>
)
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,12 +1,19 @@
import { createAction } from '@reduxjs/toolkit'
import { isAddress } from '../../utils'
export interface Call {
address: string
callData: string
}
const ADDRESS_REGEX = /^0x[a-fA-F0-9]{40}$/
const LOWER_HEX_REGEX = /^0x[a-f0-9]*$/
export function toCallKey(call: Call): string {
if (!ADDRESS_REGEX.test(call.address)) {
throw new Error(`Invalid address: ${call.address}`)
}
if (!LOWER_HEX_REGEX.test(call.callData)) {
throw new Error(`Invalid hex: ${call.callData}`)
}
return `${call.address}-${call.callData}`
}
@@ -15,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

View File

@@ -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])
}

View 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'
}
}
}
})
})
})
})

View File

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

View 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'])
})
})
})

View File

@@ -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
}

View File

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

View File

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

View File

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

View File

@@ -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)

View File

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

View File

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

View File

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

3390
yarn.lock

File diff suppressed because it is too large Load Diff