Compare commits

..

120 Commits

Author SHA1 Message Date
Zach Pomerantz
9470c49d11 fix: fix loading delays for cached data (#3549)
* fix: loading transition

* fix: add check to usePoll
2022-03-18 11:03:26 -07:00
Ian Lapham
e1abd81a1d fix: add syncing state to trade loading (#3545)
* add syncing state to trade loading

* remove log statement

* update loading state

* update useLast trade logic

* nit fixes
2022-03-18 10:11:16 -07:00
Zach Pomerantz
7d9657867d fix: polling lag (#3543) 2022-03-18 08:52:55 -07:00
Zach Pomerantz
7cc52abb96 fix: cache computed pool addresses (#3537) 2022-03-18 08:00:35 -07:00
Crowdin Bot
5ac41417b0 chore(i18n): synchronize translations from crowdin [skip ci] 2022-03-18 00:15:22 +00:00
Zach Pomerantz
2c74c5f2df chore: include usdc in swap info (#3539)
* chore: refactor useComputeSwapInfo to include usdc

* chore: use passed usdc if able

* fix: fixture
2022-03-17 16:51:41 -07:00
Ian Lapham
cbc2ff668e disable swap button if trade not populated (#3542) 2022-03-17 16:26:37 -07:00
Zach Pomerantz
a73f59b4ff fix: only debounce input amounts (#3540) 2022-03-17 15:42:32 -07:00
Zach Pomerantz
7a75626c31 chore: bump to v0.0.25-beta (#3538) 2022-03-17 14:17:58 -07:00
Jordan Frankfurt
a0e14bef10 fix(vote): allow 0-vote proposals to render (#3536) 2022-03-17 15:37:36 -05:00
Yadong Zhang
9b5a53b2e8 fix: typeError: Cannot read properties of undefined (reading 'split') (#3518) 2022-03-17 14:30:13 -04:00
Crowdin Bot
50fdb36b6f chore(i18n): synchronize translations from crowdin [skip ci] 2022-03-17 04:06:49 +00:00
Ian Lapham
b993902c73 remove donation popup (#3532) 2022-03-16 20:42:06 -07:00
Zach Pomerantz
828bf540ba feat: usePoll (#3530)
* feat: usePoll

* chore: comments

* fix: cleanup

* fix: review updates

* fix: lint nits
2022-03-16 14:54:17 -07:00
Zach Pomerantz
7c88a5a008 chore: bump to v0.0.24-beta (#3524) 2022-03-15 22:31:41 -07:00
Zach Pomerantz
360c5e2c96 fix: fallback to eth_accounts in eip1193 bridge (#3516) 2022-03-15 09:42:05 -07:00
Ian Lapham
72678ee667 Update token list (#3520) 2022-03-15 10:53:20 -04:00
Zach Pomerantz
4dd74f2144 fix: token img overflow (#3517) 2022-03-15 08:01:32 -05:00
Zach Pomerantz
e45c104135 fix: computed token img (#3519) 2022-03-15 08:00:46 -05:00
Crowdin Bot
98fcaacd9b chore(i18n): synchronize translations from crowdin [skip ci] 2022-03-15 09:06:57 +00:00
Crowdin Bot
93551579e4 chore(i18n): synchronize translations from crowdin [skip ci] 2022-03-14 20:11:46 +00:00
Zach Pomerantz
8a9388ed81 feat: make Expando element (#3515)
* feat: make Expando element

* fix: cleanup

* fix: simplify margin

* fix: summary height

* fix: special case gap transition
2022-03-14 12:03:40 -07:00
Ian Lapham
884bf41da7 fix: add check for undefined slot0 and liquidity call results (#3513)
* add check for undefined slot0 and liquidity call results

* update null check

* update code order
2022-03-14 14:02:03 -04:00
Zach Pomerantz
5b686aea97 fix: exhaustive dep (#3514) 2022-03-14 10:03:19 -07:00
Zach Pomerantz
c4a456a085 chore: simplify Price in its own component (#3508)
* chore: simplify Price in its own component

* fix: restore reversal

* fix: output usdc naming
2022-03-14 09:54:04 -07:00
Zach Pomerantz
4b9098a7bf fix: token button overflow (#3504)
Removes the overflow property from TokenButton, which prevents the vertical scrollbar from rendering.
2022-03-14 09:42:38 -07:00
Zach Pomerantz
71a246f25c fix: expando icon direction (#3507) 2022-03-14 09:42:24 -07:00
Zach Pomerantz
8de048bc84 fix: input amount lag (#3503)
Propagates the exact amount to the trade currency amount without waiting for the trade to be computed. This allows things like insufficient balance or approval to be reflected in the UI while typing, instead of having a lag.
2022-03-13 22:45:14 -07:00
Zach Pomerantz
163e2d5560 fix: override Rule width (#3502)
* fix: override Rule width

* fix: include max-width
2022-03-11 17:26:02 -06:00
Zach Pomerantz
0edb0fe5e2 fix: memoize on-chain results (#3493)
* fix: memo-ize onchain results

* fix: typeof omission
2022-03-11 14:56:05 -08:00
Zach Pomerantz
496408b3db fix: token list fetching performance (#3480)
* fix: token list fetching

* fix: simplify naming
2022-03-11 11:57:49 -08:00
Zach Pomerantz
78b6f5c72d fix: destructure currencies for ref equality (#3498) 2022-03-11 11:25:17 -08:00
Zach Pomerantz
f9fb71a803 fix(perf): cache pools to avoid cost of instantiation (#3499) 2022-03-11 08:20:16 -08:00
Crowdin Bot
59d0046411 chore(i18n): synchronize translations from crowdin [skip ci] 2022-03-11 03:07:34 +00:00
Zach Pomerantz
b4e0234d07 fix: avoid summary wrap (#3497) 2022-03-10 15:59:45 -08:00
Zach Pomerantz
4a8dbda0b8 fix: memoize nested components (#3483) 2022-03-10 15:48:45 -08:00
Crowdin Bot
0cbb24c614 chore(i18n): synchronize translations from crowdin [skip ci] 2022-03-10 23:06:32 +00:00
Ian Lapham
a9dba258ff fix: update wrap fail state callback and input values (#3495)
* update wrap fail state callback and input values

* add error check for wrap status

* update error catching logic

* update error catching
2022-03-10 17:07:52 -05:00
Zach Pomerantz
fa163cb938 feat: price impact update (#3496)
* feat: green text on neg price impact

* chore: propagate all of usdc price impact

* chore: pass price impact to summary details

* chore: propagate slippage and impact warnings

* feat: update warnings on summary dialog

* chore: rm todo
2022-03-10 13:07:37 -08:00
Curly Brackets
b52273932a fix(ui): Auto Router icon not appearing on safari/ios (#3444)
* fix(ui): Auto Router icon not appearing on safari/ios

Closes #3138

* refactor: remove auto-router svg that are no longer in use
2022-03-10 14:37:32 -05:00
Crowdin Bot
9ad8f80e4e chore(i18n): synchronize translations from crowdin [skip ci] 2022-03-10 18:07:32 +00:00
Zach Pomerantz
69bc598dea feat: display "no results found" message (#3467)
* feat: display when no results

* chore: use body not subhead
2022-03-10 09:53:56 -08:00
Ian Lapham
7feba045fc fix: add formatting to USDC values (#3478)
* add formatting to USDC values

* update formatting of USDC amounts to en

* apply same formatting to summary
2022-03-10 12:38:59 -05:00
Zach Pomerantz
04cee0a07d chore: bump to v0.0.23-beta (#3492) 2022-03-10 09:02:17 -08:00
Zach Pomerantz
ea73260e56 fix: accomodate longer locales (#3468) 2022-03-10 08:57:36 -08:00
Zach Pomerantz
b4bd2973a9 fix: update input when prefixing 0 to decimal (#3487) 2022-03-10 08:56:06 -08:00
Zach Pomerantz
a071b8adb0 fix: summary line break (#3486) 2022-03-10 08:55:55 -08:00
Zach Pomerantz
610acb0191 fix: font family overrides (#3485) 2022-03-10 08:55:40 -08:00
Zach Pomerantz
63bad8f890 fix: summary expando height (#3484) 2022-03-10 08:54:42 -08:00
Zach Pomerantz
32f955693f fix: restructure web3 to memoize (#3472) 2022-03-09 15:08:04 -08:00
Zach Pomerantz
96c66a5846 fix: incorrectly memoized hooks (#3471)
* fix: incorrectly memoized hooks

* fix: finish memoizing useUSDCPrice
2022-03-09 12:33:35 -08:00
Sara Reynolds
8c269a6d39 feat: add optimism and arbitrum to gas estimate support (#3434)
* add optimism and arbitrum to gas estimate support

* update sor version

* move to dependencies
2022-03-09 14:52:19 -05:00
Justin Domingue
36f111fa6f chore: upgrade to redux-toolkit 1.8 (#3464) 2022-03-09 12:19:24 -05:00
Ian Lapham
e569dc2152 fix: update default tokens on unsupported networks (#3470)
* update default tokens on unsupported networks

* update supported network with hook

* update defaults

* fix on default input token
2022-03-08 20:35:23 -05:00
Crowdin Bot
1aa042c5ef chore(i18n): synchronize translations from crowdin [skip ci] 2022-03-08 21:07:02 +00:00
Zach Pomerantz
1450315b98 chore: bump to v0.0.21-beta (#3466) 2022-03-08 09:54:59 -08:00
Zach Pomerantz
aefbb3d812 feat: price impact warning (#3437)
* chore: mv usdc price impact to hook

* fix: popover padding

* feat: warn on high price impact from toolbar

* fix: display price impact on warning too

* chore: rename useUSDCValue params

* fix: conform uses of price impact color
2022-03-08 09:53:40 -08:00
Moody Salem
c3f12398cd fix: reverts the defaulting behavior of 1 eth to usdc (#3465) 2022-03-08 11:31:52 -05:00
Zach Pomerantz
2272f2a01a chore: specify a module for CRAv4 (#3462) 2022-03-08 08:19:56 -08:00
Zach Pomerantz
fb71078ea2 fix: fully specify locales (#3461) 2022-03-08 08:19:31 -08:00
Zach Pomerantz
1c7c93191e fix: default input to native currency (#3456)
* fix: default to native

* chore: fix import
2022-03-08 08:19:23 -08:00
Zach Pomerantz
0713f730b3 fix: widget sizing (#3455) 2022-03-08 08:19:12 -08:00
Crowdin Bot
5f7a18b411 chore(i18n): synchronize translations from crowdin [skip ci] 2022-03-08 16:07:25 +00:00
Curly Brackets
020c8d181a fix(ui): display wrapped currency symbol with current chain (#3441) 2022-03-08 09:06:31 -06:00
Zach Pomerantz
ab3f024031 fix: display loaded input amount (#3451) 2022-03-08 08:57:51 -06:00
Ian Lapham
d989c61de5 fix: update type on wrapped native currency (#3454)
* update wrapped native currency type

* fix build error on base check

* update type on mapping to permit undefined values

* undo unneeded changes

* update filter check
2022-03-07 21:04:21 -05:00
Zach Pomerantz
5dd8059734 fix: routing tooltip styles (#3459) 2022-03-07 15:08:01 -08:00
Zach Pomerantz
b50e5511ea chore: update loading colors (#3438)
* chore: update loading colors

* fix: loading opacity of 0.6
2022-03-07 09:40:15 -08:00
Crowdin Bot
1efe5e9cd5 chore(i18n): synchronize translations from crowdin [skip ci] 2022-03-06 15:06:35 +00:00
Zach Pomerantz
2944dc4d0b chore: bump to v0.0.21-beta (#3439) 2022-03-04 14:30:25 -08:00
Jordan Frankfurt
29ae755f2a add new token image to widget (#3433) 2022-03-04 11:14:31 -06:00
Zach Pomerantz
27b831b301 chore: convert inline spinner to svg (#3432)
* chore: convert inline spinner to svg

* feat: inline spinner outline
2022-03-04 08:59:09 -08:00
Ian Lapham
6d9d38819e feat: update loading icon (#3428)
* update loading icon

* color update, update file for spinner

* update loader styling size

* update file naming for spinner
2022-03-04 11:07:02 -05:00
Zach Pomerantz
2de29129ed feat: export types (#3411)
* feat: export used types

* fix: clean defaulting names

* chore: rename to useSyncTokenDefaults
2022-03-03 14:21:03 -08:00
Crowdin Bot
52af0e506b chore(i18n): synchronize translations from crowdin [skip ci] 2022-03-03 22:07:03 +00:00
Noah Zinsmeister
4d69c946bf fix: Improve Vote page (#3429)
* introduce useQuorum

introduce LATEST_GOVERNOR_INDEX

* use CurrencyAmounts and fix % logic

* gate useQuorum to mainnet, just to be safe

* comment
2022-03-03 16:27:59 -05:00
Zach Pomerantz
542bf0bf66 feat: user select (#3410)
* feat: make data user-selectable

* fix: consider the whole node for focus

* fix: back out lineheight typing

* fix: straggling occurences

* chore: comment on root user-select
2022-03-03 11:09:12 -08:00
Zach Pomerantz
a4fbfae4ba fix: set appropriate types to nowrap (#3392) 2022-03-03 10:39:44 -08:00
Crowdin Bot
b2288258f2 chore(i18n): synchronize translations from crowdin [skip ci] 2022-03-03 18:11:42 +00:00
Ian Lapham
8703013b2d feat: use permit when valid on approvals (#3354)
* use permit when valid on approvals

* fix broken check for permit sig

* update conditionals

* update text

* remove unneeded else

* move permit and approve logic to combined hook

* update comment

* split txn and approval state, code clean

* organize disable conditions

* small changes

* update conditional check
2022-03-03 12:28:36 -05:00
Crowdin Bot
4f6173675d chore(i18n): synchronize translations from crowdin [skip ci] 2022-03-03 09:11:25 +00:00
Crowdin Bot
2469eb58b9 chore(i18n): synchronize translations from crowdin [skip ci] 2022-03-03 07:14:57 +00:00
Crowdin Bot
e0a8ac2408 chore(i18n): synchronize translations from crowdin [skip ci] 2022-03-02 23:26:37 +00:00
Ian Lapham
0a736b5e62 feat: create donation banner (#3403)
* create donation banner

* update styling and copy

* only show banner on swap page

* dont use hard coded z index

* style updates
2022-03-02 17:48:12 -05:00
Jordan Frankfurt
b44eb8877c chore(widgets): export theme stuff from widget (#3409) 2022-03-02 14:20:55 -08:00
Zach Pomerantz
92e61fa34b fix: token balance reloading (#3408)
* fix: layout token balance on token select

* fix: do not clear block on window re-focus
2022-03-02 13:39:18 -08:00
Zach Pomerantz
ef62fd33b2 fix: error dialog transition (#3405) 2022-03-02 13:37:57 -08:00
Jordan Frankfurt
96a42f66d4 fix(widgets): disable interactions and don't show summary screen on unsupported networks (#3376)
* disable interactions and don't show summary screen on unsupported networks

* clear input values on unsupported network

* pr feedback

* pr feedback

* simpler active trade clearing

* rename some things
2022-03-02 13:45:31 -06:00
Crowdin Bot
c446f20d2f chore(i18n): synchronize translations from crowdin [skip ci] 2022-03-02 19:11:16 +00:00
Jordan Frankfurt
5a1ef8fb7d feat(widgets): support wrapping native assets (#3301)
* feat(widgets): support wrapping native assets

* integrate wrap with swapInfo, start a useWrapCallback hook

* add loading state

* add pending state to (un)wrap transactions

* final cleanup

* janky merge conflict fix--disregard! this will change

* fixed

* 💢

* pr feedback

* z's pr feedback

* pr feedback

* zzmp pr feedback

* zzmp pr feedback
2022-03-02 12:36:35 -06:00
Zach Pomerantz
2863971640 fix: routing tooltip sizing (#3391) 2022-03-02 08:54:15 -08:00
Crowdin Bot
dcaf10ec29 chore(i18n): synchronize translations from crowdin [skip ci] 2022-03-02 07:31:50 +00:00
Zach Pomerantz
bca5113569 fix: connect wallet button (#3324)
* fix: make dialog provider an isolate

* fix: use button for connect wallet

* chore: document esoteric changes
2022-03-01 14:32:54 -08:00
David Walsh
6779c1a024 Fix #3117 - Open networks menu upon hover (#3378) 2022-03-01 14:45:22 -05:00
Ori Pomerantz
f79ef12494 fix(icons): Updated one of the Optimism images (#3357) 2022-03-01 14:37:18 -05:00
dependabot[bot]
7bcda46934 chore(deps): bump url-parse from 1.5.3 to 1.5.10 (#3371)
Bumps [url-parse](https://github.com/unshiftio/url-parse) from 1.5.3 to 1.5.10.
- [Release notes](https://github.com/unshiftio/url-parse/releases)
- [Commits](https://github.com/unshiftio/url-parse/compare/1.5.3...1.5.10)

---
updated-dependencies:
- dependency-name: url-parse
  dependency-type: indirect
...

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-03-01 14:35:54 -05:00
dependabot[bot]
f4ba24cfd5 chore(deps): bump nanoid from 3.1.28 to 3.2.0 (#3173)
Bumps [nanoid](https://github.com/ai/nanoid) from 3.1.28 to 3.2.0.
- [Release notes](https://github.com/ai/nanoid/releases)
- [Changelog](https://github.com/ai/nanoid/blob/main/CHANGELOG.md)
- [Commits](https://github.com/ai/nanoid/compare/3.1.28...3.2.0)

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-03-01 14:35:01 -05:00
Zach Pomerantz
59c6ab16dd fix: exports (#3379)
* fix: cjs in cra

* chore: wrap cjs for mjs

* chore: export cjs and esm

* chore: export chunks

* fix: correctly export locales

* chore: note exports compat

* fix: use cjs for common deps

* chore: bump to v0.0.20-beta

* chore: rm outdated comment
2022-03-01 11:32:55 -08:00
Ian Lapham
db17dcbf2c list update (#3389) 2022-03-01 14:05:13 -05:00
Moody Salem
1835de7f5f fix: the vote page could not render proposals without signature data
fixes https://github.com/Uniswap/interface/issues/3380
2022-02-28 18:16:24 -05:00
Crowdin Bot
00f158209c chore(i18n): synchronize translations from crowdin [skip ci] 2022-02-28 19:11:43 +00:00
Zach Pomerantz
2108ceedd5 chore: reduce token list block period (#3353) 2022-02-28 10:47:36 -08:00
Zach Pomerantz
ad080470da fix: use explicit value in Plural tag (#3363)
one is non-functional (I don't know why), but _1 seems to work 🤷
2022-02-28 10:41:15 -08:00
Moody Salem
fc34912b53 feat(swap): default 1 native to usdc on the swap page (#3347)
* feat(swap): default 1 eth to usdc on the swap page

* fix unit tests

* fix tests

* fix the issue better

* use the token list logo

* fix integration tests for swap and add one for eth/usdc

* address comments
2022-02-28 13:30:37 -05:00
jaclyn
c25d2b894c fix: update walletlink-connector to 6.2.13 (#3315)
* fix: update walletlink-connector to 6.2.12

* fix: update walletlink-connector to 6.2.13

Co-authored-by: Jaclyn Chan <jaclyn.chan@coinbase.com>
2022-02-28 11:13:36 -05:00
Moody Salem
83c99b8c04 chore: remove unused exports and unused integration test build env variable 2022-02-28 11:03:05 -05:00
Zach Pomerantz
ccdf1e7575 fix: convert to commonjs (#3373)
* chore: use fully specified path for non-main imports

* chore: convert to commonjs

- Pin @web3-react to exact cjs versions

* chore: bump to v0.0.18-beta

* chore: fix fonts.css comment

* fix: @ethersproject/providers version skew

* fix: @web3-react state typings
2022-02-28 07:52:22 -08:00
Crowdin Bot
c9faafee5e chore(i18n): synchronize translations from crowdin [skip ci] 2022-02-25 08:08:05 +00:00
Zach Pomerantz
26a44fb51b chore: bump to v0.0.15-beta (#3358) 2022-02-24 11:36:00 -08:00
Zach Pomerantz
1e16ac8449 fix: wrap Eip1193Bridge to fix outstanding bugs (#3355)
* fix: wrap Eip1193Bridge to fix outstanding bugs

* revert: widget height change
2022-02-24 11:20:09 -08:00
Crowdin Bot
5b5e76573d chore(i18n): synchronize translations from crowdin [skip ci] 2022-02-24 09:06:47 +00:00
Crowdin Bot
27cdbd0d5f chore(i18n): synchronize translations from crowdin [skip ci] 2022-02-23 23:06:39 +00:00
Zach Pomerantz
b2a30b9bf1 fix: style nits (#3352)
* fix: prefix output usdc with $

* fix: space in output estimate warning

* fix: reset slippage warnings

* fix: display link icon for etherscan
2022-02-23 14:56:49 -08:00
Zach Pomerantz
dfad7b89ab chore: nextjs compatibility (#3351)
* fix: esm/cjs exports

* fix: guard visibility state WebAPI

* fix: nextjs styled-components imports

* fix: add ethers' optional deps

* fix: document access in useHasFocus

* fix: suffix babel-plugin-macros config for commonjs

* chore: rollup sourcemaps

* chore: export fonts separately

* chore: mv redux to peer dep

* fix: run tests off cjs config

* fix: run cosmos off cjs config
2022-02-23 13:12:07 -08:00
Zach Pomerantz
4fe35ea42e fix: max on WebKit (#3349)
* chore: add walletconnect to cosmos

* fix: onClickMax for TokenInput

* chore: add setImmediate
2022-02-23 12:50:22 -08:00
Zach Pomerantz
0d852b6165 fix: apply user-select to widget, not to constituents (#3345) 2022-02-23 12:49:59 -08:00
Zach Pomerantz
8ac3b836bd fix: slippage input size (#3350) 2022-02-23 12:25:17 -08:00
Crowdin Bot
12bc5957b4 chore(i18n): synchronize translations from crowdin [skip ci] 2022-02-23 20:06:40 +00:00
Ian Lapham
a33187c33b feat: routing tooltip for widget (#3259)
* start file updates for routing tooltip

* start tooltip UI

* fix styling

* remove use of px and add header

* UI updates;

* update styles

* update file structure

* update routing components and type

* small code fixes

* fix broken sizing bug

* nit fixes
2022-02-23 14:22:21 -05:00
Zach Pomerantz
248bc07cf1 fix: lazily instantiate supported router providers (#3348)
* fix: iterate over enum values

* fix: lazily instantiate router providers
2022-02-23 10:46:29 -08:00
170 changed files with 5940 additions and 3004 deletions

View File

@@ -38,10 +38,10 @@ jobs:
run: yarn install --frozen-lockfile
- run: yarn cypress install
- run: yarn build
env:
CI: false
REACT_APP_NETWORK_URL: 'https://mainnet.infura.io/v3/4bf032f2d38a4ed6bb975b80d6340847'
REACT_APP_SERVICE_WORKER: false
- run: yarn test:e2e

1
.gitignore vendored
View File

@@ -19,6 +19,7 @@
# builds
/build
/cosmos-export
/dist
/dts

View File

@@ -4,7 +4,7 @@
],
"webpack": {
"configPath": "react-scripts/config/webpack.config",
"overridePath": "cosmos.override.js"
"overridePath": "cosmos.override.cjs"
},
"port": 5001
}
}

View File

@@ -2,16 +2,27 @@ describe('Swap', () => {
beforeEach(() => {
cy.visit('/swap')
})
it('starts with ETH selected by default', () => {
cy.get('#swap-currency-input .token-amount-input').should('have.value', '')
cy.get('#swap-currency-input .token-symbol-container').should('contain.text', 'ETH')
cy.get('#swap-currency-output .token-amount-input').should('not.have.value')
cy.get('#swap-currency-output .token-symbol-container').should('contain.text', 'Select a token')
})
it('can enter an amount into input', () => {
cy.get('#swap-currency-input .token-amount-input').type('0.001', { delay: 200 }).should('have.value', '0.001')
cy.get('#swap-currency-input .token-amount-input')
.clear()
.type('0.001', { delay: 200 })
.should('have.value', '0.001')
})
it('zero swap amount', () => {
cy.get('#swap-currency-input .token-amount-input').type('0.0', { delay: 200 }).should('have.value', '0.0')
cy.get('#swap-currency-input .token-amount-input').clear().type('0.0', { delay: 200 }).should('have.value', '0.0')
})
it('invalid swap amount', () => {
cy.get('#swap-currency-input .token-amount-input').type('\\', { delay: 200 }).should('have.value', '')
cy.get('#swap-currency-input .token-amount-input').clear().type('\\', { delay: 200 }).should('have.value', '')
})
it('can enter an amount into output', () => {

View File

@@ -1,15 +1,30 @@
{
"name": "@uniswap/widgets",
"version": "0.0.14-beta",
"version": "0.0.25-beta",
"description": "Uniswap Interface",
"homepage": ".",
"main": "dist/widgets.js",
"module": "dist/widgets.esm.js",
"types": "dist/widgets.d.ts",
"files": [
"lib",
"dist"
],
"type": "module",
"types": "dist/index.d.ts",
"main": "dist/cjs/index.cjs",
"module": "dist/index.js",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js",
"require": "./dist/cjs/index.cjs"
},
"./locales/*": {
"import": "./dist/locales/*.js",
"require": "./dist/cjs/locales/*.cjs"
},
"./fonts.css": {
"import": "./dist/fonts.css",
"require": "./dist/fonts.css"
}
},
"devDependencies": {
"@babel/plugin-transform-runtime": "^7.17.0",
"@babel/preset-env": "^7.16.11",
@@ -21,6 +36,9 @@
"@graphql-codegen/typescript-operations": "^1.18.2",
"@graphql-codegen/typescript-rtk-query": "^1.1.1",
"@lingui/cli": "^3.9.0",
"@lingui/core": "^3.9.0",
"@lingui/macro": "^3.9.0",
"@lingui/react": "^3.9.0",
"@metamask/jazzicon": "^2.0.0",
"@reach/dialog": "^0.10.3",
"@reach/portal": "^0.10.3",
@@ -71,13 +89,8 @@
"@uniswap/v2-periphery": "^1.1.0-beta.0",
"@uniswap/v3-core": "1.0.0",
"@uniswap/v3-periphery": "^1.1.1",
"web3-react-abstract-connector": "npm:@web3-react/abstract-connector@^6.0.7",
"web3-react-fortmatic-connector": "npm:@web3-react/fortmatic-connector@^6.0.9",
"web3-react-injected-connector": "npm:@web3-react/injected-connector@^6.0.7",
"web3-react-portis-connector": "npm:@web3-react/portis-connector@^6.0.9",
"web3-react-types": "npm:@web3-react/types@^6.0.7",
"web3-react-walletconnect-connector": "npm:@web3-react/walletconnect-connector@^7.0.2-alpha.0",
"web3-react-walletlink-connector": "npm:@web3-react/walletlink-connector@^6.2.11",
"@web3-react/metamask": "8.0.13-beta.0",
"@web3-react/walletconnect": "8.0.18-beta.0",
"array.prototype.flat": "^1.2.4",
"array.prototype.flatmap": "^1.2.4",
"babel-plugin-macros": "^3.1.0",
@@ -108,15 +121,18 @@
"react-ga": "^2.5.7",
"react-is": "^17.0.2",
"react-markdown": "^4.3.1",
"react-redux": "^7.2.2",
"react-router-dom": "^5.0.0",
"react-scripts": "^4.0.3",
"react-spring": "^8.0.27",
"react-use-gesture": "^6.0.14",
"redux": "^4.1.2",
"redux-localstorage-simple": "^2.3.1",
"rollup": "^2.63.0",
"rollup-plugin-copy": "^3.4.0",
"rollup-plugin-delete": "^2.0.0",
"rollup-plugin-dts": "^4.1.0",
"rollup-plugin-multi-input": "^1.3.1",
"rollup-plugin-node-externals": "^3.1.2",
"rollup-plugin-scss": "^3.0.0",
"rollup-plugin-typescript2": "^0.31.1",
@@ -130,6 +146,13 @@
"use-resize-observer": "^8.0.0",
"wcag-contrast": "^3.0.0",
"web-vitals": "^2.1.0",
"web3-react-abstract-connector": "npm:@web3-react/abstract-connector@^6.0.7",
"web3-react-fortmatic-connector": "npm:@web3-react/fortmatic-connector@^6.0.9",
"web3-react-injected-connector": "npm:@web3-react/injected-connector@^6.0.7",
"web3-react-portis-connector": "npm:@web3-react/portis-connector@^6.0.9",
"web3-react-types": "npm:@web3-react/types@^6.0.7",
"web3-react-walletconnect-connector": "npm:@web3-react/walletconnect-connector@^7.0.2-alpha.0",
"web3-react-walletlink-connector": "npm:@web3-react/walletlink-connector@^6.2.13",
"workbox-core": "^6.1.0",
"workbox-precaching": "^6.1.0",
"workbox-routing": "^6.1.0"
@@ -150,7 +173,7 @@
"prepublishOnly": "yarn widgets:build",
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test --env=./custom-test-env.js",
"test": "react-scripts test --env=./custom-test-env.cjs",
"test:e2e": "start-server-and-test 'serve build -l 3000' http://localhost:3000 'cypress run --record'",
"widgets:start": "cosmos",
"widgets:build": "rollup --config --failAfterWarnings --configPlugin typescript2"
@@ -180,26 +203,27 @@
"@ethersproject/contracts": "^5.4.1",
"@ethersproject/experimental": "^5.4.0",
"@ethersproject/hash": "^5.4.0",
"@ethersproject/providers": "5.4.0",
"@ethersproject/providers": "^5.4.0",
"@ethersproject/solidity": "^5.4.0",
"@ethersproject/strings": "^5.4.0",
"@ethersproject/units": "^5.4.0",
"@ethersproject/wallet": "^5.4.0",
"@fontsource/ibm-plex-mono": "^4.5.1",
"@fontsource/inter": "^4.5.1",
"@lingui/core": "^3.9.0",
"@lingui/macro": "^3.9.0",
"@lingui/react": "^3.9.0",
"@popperjs/core": "^2.4.4",
"@reduxjs/toolkit": "^1.6.1",
"@uniswap/redux-multicall": "^1.0.0",
"@uniswap/router-sdk": "^1.0.3",
"@uniswap/sdk-core": "^3.0.1",
"@uniswap/smart-order-router": "^2.5.10",
"@uniswap/smart-order-router": "^2.5.20",
"@uniswap/token-lists": "^1.0.0-beta.27",
"@uniswap/v2-sdk": "^3.0.1",
"@uniswap/v3-sdk": "^3.8.2",
"web3-react-core": "npm:@web3-react/core@^6.0.9",
"@web3-react/core": "8.0.17-beta.0",
"@web3-react/eip1193": "8.0.12-beta.0",
"@web3-react/empty": "8.0.10-beta.0",
"@web3-react/types": "8.0.10-beta.0",
"@web3-react/url": "8.0.12-beta.0",
"ajv": "^6.12.3",
"cids": "^1.0.0",
"immer": "^9.0.6",
@@ -214,25 +238,26 @@
"popper-max-size-modifier": "^0.2.0",
"react-feather": "^2.0.8",
"react-popper": "^2.2.3",
"react-redux": "^7.2.2",
"react-virtualized-auto-sizer": "^1.0.2",
"react-window": "^1.8.5",
"rebass": "^4.0.7",
"redux": "^4.1.2",
"setimmediate": "^1.0.5",
"styled-components": "^5.3.0",
"tiny-invariant": "^1.2.0",
"wcag-contrast": "^3.0.0",
"@web3-react/core": "8.0.16-alpha.0",
"@web3-react/eip1193": "8.0.16-alpha.0",
"@web3-react/empty": "8.0.17-alpha.0",
"@web3-react/metamask": "8.0.16-alpha.0",
"@web3-react/types": "8.0.16-alpha.0",
"@web3-react/url": "8.0.17-alpha.0",
"web3-react-core": "npm:@web3-react/core@^6.0.9",
"wicg-inert": "^3.1.1"
},
"peerDependencies": {
"@babel/runtime": "^7.17.0",
"react": "^17.0.1",
"react-dom": "^17.0.1"
"react-dom": "^17.0.1",
"react-redux": "^7.2.2",
"redux": "^4.1.2"
},
"optionalDependencies": {
"bufferutil": "^4.0.6",
"encoding": "^0.1.13",
"utf-8-validate": "^5.0.8"
}
}

View File

@@ -17,6 +17,8 @@ import { RollupWarning } from 'rollup'
import copy from 'rollup-plugin-copy'
import del from 'rollup-plugin-delete'
import dts from 'rollup-plugin-dts'
// @ts-ignore // missing types
import multi from 'rollup-plugin-multi-input'
import externals from 'rollup-plugin-node-externals'
import sass from 'rollup-plugin-scss'
import { CompilerOptions } from 'typescript'
@@ -24,6 +26,8 @@ import { CompilerOptions } from 'typescript'
const REPLACEMENTS = {
'process.env.REACT_APP_IS_WIDGET': true,
'process.env.REACT_APP_LOCALES': '"./locales"',
// esm requires fully-specified paths:
'react/jsx-runtime': 'react/jsx-runtime.js',
}
const EXTENSIONS = ['.js', '.jsx', '.ts', '.tsx']
@@ -45,54 +49,85 @@ const aliases = Object.entries({ ...paths }).flatMap(([find, replacements]) => {
const plugins = [
// Dependency resolution
externals({ exclude: ['constants'], deps: true, peerDeps: true }), // marks builtins, dependencies, and peerDependencies external
resolve({ extensions: EXTENSIONS }), // resolves third-party modules within node_modules/
alias({ entries: aliases }), // resolves paths aliased through the tsconfig (babel does not use tsconfig path resolution)
// Source code transformation
replace({ ...REPLACEMENTS, preventAssignment: true }),
json(), // imports json; doing so type-checking allows the json to be type-checked
json(), // imports json as ES6; doing so enables type-checking and module resolution
]
const check = {
input: 'src/lib/index.tsx',
output: { file: 'dist/widgets.tsc' },
output: { file: 'dist/widgets.tsc', inlineDynamicImports: true },
external: isAsset,
plugins: [...plugins, typescript({ tsconfig: TS_CONFIG })],
plugins: [
externals({ exclude: ['constants'], deps: true, peerDeps: true }), // marks builtins, dependencies, and peerDependencies external
...plugins,
typescript({ tsconfig: TS_CONFIG }),
],
onwarn: squelchTranspilationWarnings, // this pipeline is only for typechecking and generating definitions
}
const type = {
input: 'dist/dts/lib/index.d.ts',
output: { file: 'dist/widgets.d.ts' },
output: { file: 'dist/index.d.ts' },
external: isAsset,
plugins: [
externals({ exclude: ['constants'], deps: true, peerDeps: true }),
dts({ compilerOptions: { baseUrl: 'dist/dts' } }),
process.env.ROLLUP_WATCH ? undefined : del({ hook: 'buildEnd', targets: ['dist/widgets.tsc', 'dist/dts'] }),
],
}
/**
* This exports scheme works for nextjs and for CRA5.
*
* It will also work for CRA4 if you use direct imports:
* instead of `import { SwapWidget } from '@uniswap/widgets'`,
* `import { SwapWidget } from '@uniswap/widgets/dist/index.js'`.
* I do not know why CRA4 does not seem to use exports for resolution.
*
* Note that chunks are enabled. This is so the tokenlist spec can be loaded async,
* to improve first load time (due to ajv). Locales are also in separate chunks.
*
* Lastly, note that JSON and lingui are bundled into the library, as neither are fully
* supported/compatible with ES Modules. Both _could_ be bundled only with esm, but this
* yields a less complex pipeline.
*/
const transpile = {
input: 'src/lib/index.tsx',
output: [
{
file: 'dist/widgets.js',
format: 'cjs',
dir: 'dist',
format: 'esm',
sourcemap: false,
},
{
file: 'dist/widgets.esm.js',
format: 'esm',
dir: 'dist/cjs',
entryFileNames: '[name].cjs',
chunkFileNames: '[name]-[hash].cjs',
format: 'cjs',
sourcemap: false,
},
],
plugins: [
externals({
exclude: [
'constants',
/@lingui\/(core|react)/, // @lingui incorrectly exports esm, so it must be bundled in
/\.json$/, // esm does not support JSON loading, so it must be bundled in
],
deps: true,
peerDeps: true,
}),
...plugins,
// Source code transformation
url({ include: ASSET_EXTENSIONS.map((extname) => '**/*' + extname) }), // imports assets as data URIs
url({ include: ASSET_EXTENSIONS.map((extname) => '**/*' + extname), limit: Infinity }), // imports assets as data URIs
svgr({ exportType: 'named', svgo: false }), // imports svgs as React components
sass({ insert: true }), // imports inlined sass styles
sass({ output: 'dist/fonts.css' }), // generates fonts.css
commonjs(), // transforms cjs dependencies into tree-shakeable ES modules
babel({
@@ -104,16 +139,30 @@ const transpile = {
'@babel/plugin-transform-runtime', // embeds the babel runtime for library distribution
],
}),
copy({
copyOnce: true,
targets: [{ src: 'src/locales/*.js', dest: 'dist/locales' }],
}),
],
onwarn: squelchTypeWarnings, // this pipeline is only for transpilation
}
const config = [check, type, transpile]
const locales = {
input: 'src/locales/*.js',
output: [
{
dir: 'dist',
format: 'esm',
sourcemap: false,
},
],
plugins: [
copy({
copyOnce: true,
targets: [{ src: 'src/locales/*.js', dest: 'dist/cjs/locales', rename: (name) => `${name}.cjs` }],
}),
commonjs(),
multi(),
],
}
const config = [check, type, transpile, locales]
export default config
function squelchTranspilationWarnings(warning: RollupWarning, warn: (warning: RollupWarning) => void) {

Binary file not shown.

After

Width:  |  Height:  |  Size: 121 KiB

View File

@@ -1,3 +1,3 @@
<svg width="100%" height="35" viewBox="800 0 300 200" xmlns="http://www.w3.org/2000/svg">
<line x1="0" x2="2000" y1="100" y2="100" stroke="currentColor" stroke-width="20" stroke-linecap="round" stroke-dasharray="1, 45"/>
<svg width="100%" height="35" viewBox="850 0 300 200" xmlns="http://www.w3.org/2000/svg">
<line x1="0" x2="3000" y1="100" y2="100" stroke="currentColor" stroke-width="20" stroke-linecap="round" stroke-dasharray="1, 45"/>
</svg>

Before

Width:  |  Height:  |  Size: 233 B

After

Width:  |  Height:  |  Size: 233 B

View File

@@ -1,16 +1,5 @@
<svg width="170" height="168" viewBox="0 0 170 168" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0)">
<path opacity="0.6" d="M85.05 168C132.022 168 170.1 130.105 170.1 83.3593C170.1 36.6135 0 36.6135 0 83.3593C0 130.105 38.0782 168 85.05 168Z" fill="#FF505F"/>
<path opacity="0.6" d="M85.05 168C132.022 168 170.1 130.105 170.1 83.3593C170.1 36.6135 0 36.6135 0 83.3593C0 130.105 38.0782 168 85.05 168Z" fill="#FF0320"/>
<path d="M85.05 0C132.022 0 170.1 37.8949 170.1 84.6407C170.1 131.386 0 131.386 0 84.6407C0 37.8949 38.0782 0 85.05 0Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M144.665 64.0394L112.444 12.3742L89.0263 78.9477L144.665 64.0394Z" fill="#FF4E65"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M143.777 64.215L112.444 12.3742L165.349 58.4347L143.777 64.215Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M144.551 63.613L142.479 124.467L88.912 78.5213L144.551 63.613Z" fill="#D0001A"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M143.663 63.7886L142.479 124.467L165.235 58.0083L143.663 63.7886Z" fill="#FF697B"/>
</g>
<defs>
<clipPath id="clip0">
<rect width="170" height="168" fill="white"/>
</clipPath>
</defs>
<svg width="500" height="500" viewBox="0 0 500 500" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="250" cy="250" r="250" fill="#FF0420"/>
<path d="M177.133 316.446C162.247 316.446 150.051 312.943 140.544 305.938C131.162 298.808 126.471 288.676 126.471 275.541C126.471 272.789 126.784 269.411 127.409 265.408C129.036 256.402 131.35 245.581 134.352 232.947C142.858 198.547 164.812 181.347 200.213 181.347C209.845 181.347 218.476 182.973 226.107 186.225C233.738 189.352 239.742 194.106 244.12 200.486C248.498 206.74 250.688 214.246 250.688 223.002C250.688 225.629 250.375 228.944 249.749 232.947C247.873 244.08 245.621 254.901 242.994 265.408C238.616 282.546 231.048 295.368 220.29 303.874C209.532 312.255 195.147 316.446 177.133 316.446ZM179.76 289.426C186.766 289.426 192.707 287.362 197.586 283.234C202.59 279.106 206.155 272.789 208.281 264.283C211.158 252.524 213.348 242.266 214.849 233.51C215.349 230.883 215.599 228.194 215.599 225.441C215.599 214.058 209.657 208.366 197.774 208.366C190.768 208.366 184.764 210.43 179.76 214.558C174.882 218.687 171.379 225.004 169.253 233.51C167.001 241.891 164.749 252.149 162.498 264.283C161.997 266.784 161.747 269.411 161.747 272.163C161.747 283.672 167.752 289.426 179.76 289.426Z" fill="white"/>
<path d="M259.303 314.57C257.927 314.57 256.863 314.132 256.113 313.256C255.487 312.255 255.3 311.13 255.55 309.879L281.444 187.914C281.694 186.538 282.382 185.412 283.508 184.536C284.634 183.661 285.822 183.223 287.073 183.223H336.985C350.87 183.223 362.003 186.1 370.384 191.854C378.891 197.609 383.144 205.927 383.144 216.81C383.144 219.937 382.769 223.19 382.018 226.567C378.891 240.953 372.574 251.586 363.067 258.466C353.685 265.346 340.8 268.786 324.413 268.786H299.082L290.451 309.879C290.2 311.255 289.512 312.38 288.387 313.256C287.261 314.132 286.072 314.57 284.822 314.57H259.303ZM325.727 242.892C330.98 242.892 335.546 241.453 339.424 238.576C343.427 235.699 346.054 231.571 347.305 226.192C347.68 224.065 347.868 222.189 347.868 220.563C347.868 216.935 346.805 214.183 344.678 212.307C342.551 210.305 338.924 209.305 333.795 209.305H311.278L304.148 242.892H325.727Z" fill="white"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

@@ -51,6 +51,16 @@ const FlyoutHeader = styled.div`
font-weight: 400;
`
const FlyoutMenu = styled.div`
position: absolute;
top: 54px;
width: 272px;
z-index: 99;
padding-top: 10px;
@media screen and (min-width: ${MEDIA_WIDTHS.upToSmall}px) {
top: 40px;
}
`
const FlyoutMenuContents = styled.div`
align-items: flex-start;
background-color: ${({ theme }) => theme.bg0};
box-shadow: 0px 0px 1px rgba(0, 0, 0, 0.01), 0px 4px 8px rgba(0, 0, 0, 0.04), 0px 16px 24px rgba(0, 0, 0, 0.04),
@@ -61,16 +71,9 @@ const FlyoutMenu = styled.div`
font-size: 16px;
overflow: auto;
padding: 16px;
position: absolute;
top: 64px;
width: 272px;
z-index: 99;
& > *:not(:last-child) {
margin-bottom: 12px;
}
@media screen and (min-width: ${MEDIA_WIDTHS.upToSmall}px) {
top: 50px;
}
`
const FlyoutRow = styled.div<{ active: boolean }>`
align-items: center;
@@ -305,21 +308,23 @@ export default function NetworkSelector() {
}
return (
<SelectorWrapper ref={node as any}>
<SelectorControls onClick={toggle} interactive>
<SelectorWrapper ref={node as any} onMouseEnter={toggle} onMouseLeave={toggle}>
<SelectorControls interactive>
<SelectorLogo interactive src={info.logoUrl} />
<SelectorLabel>{info.label}</SelectorLabel>
<StyledChevronDown />
</SelectorControls>
{open && (
<FlyoutMenu onMouseLeave={toggle}>
<FlyoutHeader>
<Trans>Select a network</Trans>
</FlyoutHeader>
<Row onSelectChain={handleChainSwitch} targetChain={SupportedChainId.MAINNET} />
<Row onSelectChain={handleChainSwitch} targetChain={SupportedChainId.POLYGON} />
<Row onSelectChain={handleChainSwitch} targetChain={SupportedChainId.OPTIMISM} />
<Row onSelectChain={handleChainSwitch} targetChain={SupportedChainId.ARBITRUM_ONE} />
<FlyoutMenu>
<FlyoutMenuContents>
<FlyoutHeader>
<Trans>Select a network</Trans>
</FlyoutHeader>
<Row onSelectChain={handleChainSwitch} targetChain={SupportedChainId.MAINNET} />
<Row onSelectChain={handleChainSwitch} targetChain={SupportedChainId.POLYGON} />
<Row onSelectChain={handleChainSwitch} targetChain={SupportedChainId.OPTIMISM} />
<Row onSelectChain={handleChainSwitch} targetChain={SupportedChainId.ARBITRUM_ONE} />
</FlyoutMenuContents>
</FlyoutMenu>
)}
</SelectorWrapper>

View File

@@ -14,11 +14,12 @@ const MobilePopupWrapper = styled.div<{ height: string | number }>`
max-width: 100%;
height: ${({ height }) => height};
margin: ${({ height }) => (height ? '0 auto;' : 0)};
margin-bottom: ${({ height }) => (height ? '20px' : 0)}};
margin-bottom: ${({ height }) => (height ? '20px' : 0)};
display: none;
${({ theme }) => theme.mediaWidth.upToSmall`
display: block;
padding-top: 20px;
`};
`

View File

@@ -19,7 +19,7 @@ import { PositionDetails } from 'types/position'
import { formatTickPrice } from 'utils/formatTickPrice'
import { unwrappedToken } from 'utils/unwrappedToken'
import { DAI, USDC, USDT, WBTC, WRAPPED_NATIVE_CURRENCY } from '../../constants/tokens'
import { DAI, USDC_MAINNET, USDT, WBTC, WRAPPED_NATIVE_CURRENCY } from '../../constants/tokens'
const LinkRow = styled(Link)`
align-items: center;
@@ -145,7 +145,7 @@ export function getPriceOrderingFromPositionForUI(position?: Position): {
const token1 = position.amount1.currency
// if token0 is a dollar-stable asset, set it as the quote token
const stables = [DAI, USDC, USDT]
const stables = [DAI, USDC_MAINNET, USDT]
if (stables.some((stable) => stable.equals(token0))) {
return {
priceLower: position.token0PriceUpper.invert(),
@@ -157,7 +157,7 @@ export function getPriceOrderingFromPositionForUI(position?: Position): {
// if token1 is an ETH-/BTC-stable asset, set it as the base token
const bases = [...Object.values(WRAPPED_NATIVE_CURRENCY), WBTC]
if (bases.some((base) => base.equals(token1))) {
if (bases.some((base) => base && base.equals(token1))) {
return {
priceLower: position.token0PriceUpper.invert(),
priceUpper: position.token0PriceLower.invert(),

View File

@@ -1,7 +1,7 @@
import { Protocol } from '@uniswap/router-sdk'
import { Currency, Percent } from '@uniswap/sdk-core'
import { FeeAmount } from '@uniswap/v3-sdk'
import { DAI, USDC, WBTC } from 'constants/tokens'
import { DAI, USDC_MAINNET, WBTC } from 'constants/tokens'
import { render } from 'test-utils'
import RoutingDiagram, { RoutingDiagramEntry } from './RoutingDiagram'
@@ -10,16 +10,16 @@ const percent = (strings: TemplateStringsArray) => new Percent(parseInt(strings[
const singleRoute: RoutingDiagramEntry = {
percent: percent`100`,
path: [[USDC, DAI, FeeAmount.LOW]],
path: [[USDC_MAINNET, DAI, FeeAmount.LOW]],
protocol: Protocol.V3,
}
const multiRoute: RoutingDiagramEntry[] = [
{ percent: percent`75`, path: [[USDC, DAI, FeeAmount.LOWEST]], protocol: Protocol.V2 },
{ percent: percent`75`, path: [[USDC_MAINNET, DAI, FeeAmount.LOWEST]], protocol: Protocol.V2 },
{
percent: percent`25`,
path: [
[USDC, WBTC, FeeAmount.MEDIUM],
[USDC_MAINNET, WBTC, FeeAmount.MEDIUM],
[WBTC, DAI, FeeAmount.HIGH],
],
protocol: Protocol.V3,
@@ -47,16 +47,16 @@ jest.mock('hooks/useTokenInfoFromActiveList', () => ({
}))
it('renders when no routes are provided', () => {
const { asFragment } = render(<RoutingDiagram currencyIn={DAI} currencyOut={USDC} routes={[]} />)
const { asFragment } = render(<RoutingDiagram currencyIn={DAI} currencyOut={USDC_MAINNET} routes={[]} />)
expect(asFragment()).toMatchSnapshot()
})
it('renders single route', () => {
const { asFragment } = render(<RoutingDiagram currencyIn={USDC} currencyOut={DAI} routes={[singleRoute]} />)
const { asFragment } = render(<RoutingDiagram currencyIn={USDC_MAINNET} currencyOut={DAI} routes={[singleRoute]} />)
expect(asFragment()).toMatchSnapshot()
})
it('renders multi route', () => {
const { asFragment } = render(<RoutingDiagram currencyIn={USDC} currencyOut={DAI} routes={multiRoute} />)
const { asFragment } = render(<RoutingDiagram currencyIn={USDC_MAINNET} currencyOut={DAI} routes={multiRoute} />)
expect(asFragment()).toMatchSnapshot()
})

View File

@@ -1,12 +1,12 @@
import { Trans } from '@lingui/macro'
import { Protocol } from '@uniswap/router-sdk'
import { Currency, Percent } from '@uniswap/sdk-core'
import { Currency } from '@uniswap/sdk-core'
import { FeeAmount } from '@uniswap/v3-sdk'
import Badge from 'components/Badge'
import CurrencyLogo from 'components/CurrencyLogo'
import DoubleCurrencyLogo from 'components/DoubleLogo'
import Row, { AutoRow } from 'components/Row'
import { useTokenInfoFromActiveList } from 'hooks/useTokenInfoFromActiveList'
import { RoutingDiagramEntry } from 'lib/components/Swap/RoutingDiagram/utils'
import { Box } from 'rebass'
import styled from 'styled-components/macro'
import { ThemedText, Z_INDEX } from 'theme'
@@ -14,12 +14,6 @@ import { ThemedText, Z_INDEX } from 'theme'
import { ReactComponent as DotLine } from '../../assets/svg/dot_line.svg'
import { MouseoverTooltip } from '../Tooltip'
export interface RoutingDiagramEntry {
percent: Percent
path: [Currency, Currency, FeeAmount][]
protocol: Protocol
}
const Wrapper = styled(Box)`
align-items: center;
width: 100%;

View File

@@ -48,7 +48,7 @@ export function ImportToken(props: ImportProps) {
<RowBetween>
{onBack ? <ArrowLeft style={{ cursor: 'pointer' }} onClick={onBack} /> : <div />}
<ThemedText.MediumHeader>
<Plural value={tokens.length} one="Import token" other="Import tokens" />
<Plural value={tokens.length} _1="Import token" other="Import tokens" />
</ThemedText.MediumHeader>
{onDismiss ? <CloseIcon onClick={onDismiss} /> : <div />}
</RowBetween>

View File

@@ -0,0 +1,35 @@
import { useRef } from 'react'
let uniqueId = 0
const getUniqueId = () => uniqueId++
export default function AutoRouterIcon({ className, id }: { className?: string; id?: string }) {
const componentIdRef = useRef(id ?? getUniqueId())
const componentId = `AutoRouterIconGradient${componentIdRef.current}`
return (
<svg
width="23"
height="20"
viewBox="0 0 23 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className={className}
>
<defs>
<linearGradient id={componentId} x1="0" y1="0" x2="1" y2="0" gradientTransform="rotate(95)">
<stop id="stop1" offset="0" stopColor="#2274E2" />
<stop id="stop1" offset="0.5" stopColor="#2274E2" />
<stop id="stop2" offset="1" stopColor="#3FB672" />
</linearGradient>
</defs>
<path
d="M16 16C10 16 9 10 5 10M16 16C16 17.6569 17.3431 19 19 19C20.6569 19 22 17.6569 22 16C22 14.3431 20.6569 13 19 13C17.3431 13 16 14.3431 16 16ZM5 10C9 10 10 4 16 4M5 10H1.5M16 4C16 5.65685 17.3431 7 19 7C20.6569 7 22 5.65685 22 4C22 2.34315 20.6569 1 19 1C17.3431 1 16 2.34315 16 4Z"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
stroke={`url(#${componentId})`}
/>
</svg>
)
}

View File

@@ -3,8 +3,8 @@ import useAutoRouterSupported from 'hooks/useAutoRouterSupported'
import styled from 'styled-components/macro'
import { ThemedText } from 'theme'
import { ReactComponent as AutoRouterIcon } from '../../assets/svg/auto_router.svg'
import { ReactComponent as StaticRouterIcon } from '../../assets/svg/static_route.svg'
import AutoRouterIcon from './AutoRouterIcon'
const StyledAutoRouterIcon = styled(AutoRouterIcon)`
height: 16px;

View File

@@ -1,15 +1,14 @@
import { Trans } from '@lingui/macro'
import { Trade } from '@uniswap/router-sdk'
import { Currency, Percent, TradeType } from '@uniswap/sdk-core'
import { Pair } from '@uniswap/v2-sdk'
import { Currency, TradeType } from '@uniswap/sdk-core'
import AnimatedDropdown from 'components/AnimatedDropdown'
import { AutoColumn } from 'components/Column'
import { LoadingRows } from 'components/Loader/styled'
import RoutingDiagram, { RoutingDiagramEntry } from 'components/RoutingDiagram/RoutingDiagram'
import RoutingDiagram from 'components/RoutingDiagram/RoutingDiagram'
import { AutoRow, RowBetween } from 'components/Row'
import { SUPPORTED_GAS_ESTIMATE_CHAIN_IDS } from 'constants/chains'
import useActiveWeb3React from 'hooks/useActiveWeb3React'
import useAutoRouterSupported from 'hooks/useAutoRouterSupported'
import { getTokenPath } from 'lib/components/Swap/RoutingDiagram/utils'
import { memo, useState } from 'react'
import { Plus } from 'react-feather'
import { InterfaceTrade } from 'state/routing/types'
@@ -39,8 +38,6 @@ const OpenCloseIcon = styled(Plus)<{ open?: boolean }>`
}
`
const V2_DEFAULT_FEE_TIER = 3000
interface SwapRouteProps extends React.HTMLAttributes<HTMLDivElement> {
trade: InterfaceTrade<Currency, Currency, TradeType>
syncing: boolean
@@ -109,35 +106,3 @@ export default memo(function SwapRoute({ trade, syncing, fixedOpen = false, ...r
</Wrapper>
)
})
function getTokenPath(trade: Trade<Currency, Currency, TradeType>): RoutingDiagramEntry[] {
return trade.swaps.map(({ route: { path: tokenPath, pools, protocol }, inputAmount, outputAmount }) => {
const portion =
trade.tradeType === TradeType.EXACT_INPUT
? inputAmount.divide(trade.inputAmount)
: outputAmount.divide(trade.outputAmount)
const percent = new Percent(portion.numerator, portion.denominator)
const path: RoutingDiagramEntry['path'] = []
for (let i = 0; i < pools.length; i++) {
const nextPool = pools[i]
const tokenIn = tokenPath[i]
const tokenOut = tokenPath[i + 1]
const entry: RoutingDiagramEntry['path'][0] = [
tokenIn,
tokenOut,
nextPool instanceof Pair ? V2_DEFAULT_FEE_TIER : nextPool.fee,
]
path.push(entry)
}
return {
percent,
path,
protocol,
}
})
}

View File

@@ -39,7 +39,12 @@ export const ALL_SUPPORTED_CHAIN_IDS: SupportedChainId[] = Object.values(Support
(id) => typeof id === 'number'
) as SupportedChainId[]
export const SUPPORTED_GAS_ESTIMATE_CHAIN_IDS = [SupportedChainId.MAINNET, SupportedChainId.POLYGON]
export const SUPPORTED_GAS_ESTIMATE_CHAIN_IDS = [
SupportedChainId.MAINNET,
SupportedChainId.POLYGON,
SupportedChainId.OPTIMISM,
SupportedChainId.ARBITRUM_ONE,
]
/**
* All the chain IDs that are running the Ethereum protocol.

View File

@@ -12,6 +12,8 @@ export const COMMON_CONTRACT_NAMES: Record<number, { [address: string]: string }
[TIMELOCK_ADDRESS[SupportedChainId.MAINNET]]: 'Timelock',
[GOVERNANCE_ALPHA_V0_ADDRESSES[SupportedChainId.MAINNET]]: 'Governance (V0)',
[GOVERNANCE_ALPHA_V1_ADDRESSES[SupportedChainId.MAINNET]]: 'Governance',
'0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e': 'ENS Registry',
'0x4976fb03C32e5B8cfe2b6cCB31c09Ba78EBaBa41': 'ENS Public Resolver',
},
}
@@ -21,3 +23,5 @@ export const DEFAULT_AVERAGE_BLOCK_TIME_IN_SECS = 13
export const AVERAGE_BLOCK_TIME_IN_SECS: { [chainId: number]: number } = {
1: DEFAULT_AVERAGE_BLOCK_TIME_IN_SECS,
}
export const LATEST_GOVERNOR_INDEX = 2

View File

@@ -18,8 +18,8 @@ import {
sETH2,
SWISE,
TRIBE,
USDC,
USDC_ARBITRUM,
USDC_MAINNET,
USDC_OPTIMISM,
USDC_POLYGON,
USDT,
@@ -44,13 +44,21 @@ type ChainCurrencyList = {
}
const WRAPPED_NATIVE_CURRENCIES_ONLY: ChainTokenList = Object.fromEntries(
Object.entries(WRAPPED_NATIVE_CURRENCY).map(([key, value]) => [key, [value]])
Object.entries(WRAPPED_NATIVE_CURRENCY)
.map(([key, value]) => [key, [value]])
.filter(Boolean)
)
// used to construct intermediary pairs for trading
export const BASES_TO_CHECK_TRADES_AGAINST: ChainTokenList = {
...WRAPPED_NATIVE_CURRENCIES_ONLY,
[SupportedChainId.MAINNET]: [...WRAPPED_NATIVE_CURRENCIES_ONLY[SupportedChainId.MAINNET], DAI, USDC, USDT, WBTC],
[SupportedChainId.MAINNET]: [
...WRAPPED_NATIVE_CURRENCIES_ONLY[SupportedChainId.MAINNET],
DAI,
USDC_MAINNET,
USDT,
WBTC,
],
[SupportedChainId.OPTIMISM]: [
...WRAPPED_NATIVE_CURRENCIES_ONLY[SupportedChainId.OPTIMISM],
DAI_OPTIMISM,
@@ -90,7 +98,7 @@ export const ADDITIONAL_BASES: { [chainId: number]: { [tokenAddress: string]: To
*/
export const CUSTOM_BASES: { [chainId: number]: { [tokenAddress: string]: Token[] } } = {
[SupportedChainId.MAINNET]: {
[AMPL.address]: [DAI, WRAPPED_NATIVE_CURRENCY[SupportedChainId.MAINNET]],
[AMPL.address]: [DAI, WRAPPED_NATIVE_CURRENCY[SupportedChainId.MAINNET] as Token],
},
}
@@ -101,32 +109,38 @@ export const COMMON_BASES: ChainCurrencyList = {
[SupportedChainId.MAINNET]: [
nativeOnChain(SupportedChainId.MAINNET),
DAI,
USDC,
USDC_MAINNET,
USDT,
WBTC,
WRAPPED_NATIVE_CURRENCY[SupportedChainId.MAINNET],
WRAPPED_NATIVE_CURRENCY[SupportedChainId.MAINNET] as Token,
],
[SupportedChainId.ROPSTEN]: [
nativeOnChain(SupportedChainId.ROPSTEN),
WRAPPED_NATIVE_CURRENCY[SupportedChainId.ROPSTEN],
WRAPPED_NATIVE_CURRENCY[SupportedChainId.ROPSTEN] as Token,
],
[SupportedChainId.RINKEBY]: [
nativeOnChain(SupportedChainId.RINKEBY),
WRAPPED_NATIVE_CURRENCY[SupportedChainId.RINKEBY],
WRAPPED_NATIVE_CURRENCY[SupportedChainId.RINKEBY] as Token,
],
[SupportedChainId.GOERLI]: [
nativeOnChain(SupportedChainId.GOERLI),
WRAPPED_NATIVE_CURRENCY[SupportedChainId.GOERLI] as Token,
],
[SupportedChainId.KOVAN]: [
nativeOnChain(SupportedChainId.KOVAN),
WRAPPED_NATIVE_CURRENCY[SupportedChainId.KOVAN] as Token,
],
[SupportedChainId.GOERLI]: [nativeOnChain(SupportedChainId.GOERLI), WRAPPED_NATIVE_CURRENCY[SupportedChainId.GOERLI]],
[SupportedChainId.KOVAN]: [nativeOnChain(SupportedChainId.KOVAN), WRAPPED_NATIVE_CURRENCY[SupportedChainId.KOVAN]],
[SupportedChainId.ARBITRUM_ONE]: [
nativeOnChain(SupportedChainId.ARBITRUM_ONE),
DAI_ARBITRUM_ONE,
USDC_ARBITRUM,
USDT_ARBITRUM_ONE,
WBTC_ARBITRUM_ONE,
WRAPPED_NATIVE_CURRENCY[SupportedChainId.ARBITRUM_ONE],
WRAPPED_NATIVE_CURRENCY[SupportedChainId.ARBITRUM_ONE] as Token,
],
[SupportedChainId.ARBITRUM_RINKEBY]: [
nativeOnChain(SupportedChainId.ARBITRUM_RINKEBY),
WRAPPED_NATIVE_CURRENCY[SupportedChainId.ARBITRUM_RINKEBY],
WRAPPED_NATIVE_CURRENCY[SupportedChainId.ARBITRUM_RINKEBY] as Token,
],
[SupportedChainId.OPTIMISM]: [
nativeOnChain(SupportedChainId.OPTIMISM),
@@ -146,7 +160,7 @@ export const COMMON_BASES: ChainCurrencyList = {
],
[SupportedChainId.POLYGON_MUMBAI]: [
nativeOnChain(SupportedChainId.POLYGON_MUMBAI),
WRAPPED_NATIVE_CURRENCY[SupportedChainId.POLYGON_MUMBAI],
WRAPPED_NATIVE_CURRENCY[SupportedChainId.POLYGON_MUMBAI] as Token,
WETH_POLYGON_MUMBAI,
],
}
@@ -154,7 +168,13 @@ export const COMMON_BASES: ChainCurrencyList = {
// used to construct the list of all pairs we consider by default in the frontend
export const BASES_TO_TRACK_LIQUIDITY_FOR: ChainTokenList = {
...WRAPPED_NATIVE_CURRENCIES_ONLY,
[SupportedChainId.MAINNET]: [...WRAPPED_NATIVE_CURRENCIES_ONLY[SupportedChainId.MAINNET], DAI, USDC, USDT, WBTC],
[SupportedChainId.MAINNET]: [
...WRAPPED_NATIVE_CURRENCIES_ONLY[SupportedChainId.MAINNET],
DAI,
USDC_MAINNET,
USDT,
WBTC,
],
}
export const PINNED_PAIRS: { readonly [chainId: number]: [Token, Token][] } = {
[SupportedChainId.MAINNET]: [
@@ -168,7 +188,7 @@ export const PINNED_PAIRS: { readonly [chainId: number]: [Token, Token][] } = {
'Compound USD Coin'
),
],
[USDC, USDT],
[USDC_MAINNET, USDT],
[DAI, USDT],
],
}

View File

@@ -1101,6 +1101,20 @@
"name": "BMEX",
"symbol": "BMEX",
"decimals": 18
},
{
"chainId": 1,
"address": "0x322A46E88fa3C78F9c9E3DBb0254b61664a06109",
"name": "Ukraine DAO",
"symbol": "Ukraine",
"decimals": 18
},
{
"chainId": 1,
"address": "0x8290D7a64F25e6b5002d98367E8367c1b532b534",
"name": "oneUNI",
"symbol": "oneUNI",
"decimals": 18
}
]
}

View File

@@ -1,8 +1,24 @@
import { Currency, Ether, NativeCurrency, Token, WETH9 } from '@uniswap/sdk-core'
import {
USDC_ARBITRUM,
USDC_ARBITRUM_RINKEBY,
USDC_GÖRLI,
USDC_KOVAN,
USDC_MAINNET,
USDC_OPTIMISM,
USDC_OPTIMISTIC_KOVAN,
USDC_POLYGON,
USDC_POLYGON_MUMBAI,
USDC_RINKEBY,
USDC_ROPSTEN,
} from '@uniswap/smart-order-router'
import invariant from 'tiny-invariant'
import { UNI_ADDRESS } from './addresses'
import { SupportedChainId } from './chains'
export { USDC_ARBITRUM, USDC_MAINNET, USDC_OPTIMISM, USDC_POLYGON }
export const AMPL = new Token(
SupportedChainId.MAINNET,
'0xD46bA6D942050d489DBd938a2C909A5d5039A161',
@@ -31,27 +47,19 @@ export const DAI_OPTIMISM = new Token(
'DAI',
'Dai stable coin'
)
export const USDC = new Token(
SupportedChainId.MAINNET,
'0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48',
6,
'USDC',
'USD//C'
)
export const USDC_ARBITRUM = new Token(
SupportedChainId.ARBITRUM_ONE,
'0xFF970A61A04b1cA14834A43f5dE4533eBDDB5CC8',
6,
'USDC',
'USD//C'
)
export const USDC_POLYGON = new Token(
SupportedChainId.POLYGON,
'0x2791bca1f2de4661ed88a30c99a7a9449aa84174',
6,
'USDC',
'USD//C'
)
export const USDC: { [chainId in SupportedChainId]: Token } = {
[SupportedChainId.MAINNET]: USDC_MAINNET,
[SupportedChainId.ARBITRUM_ONE]: USDC_ARBITRUM,
[SupportedChainId.OPTIMISM]: USDC_OPTIMISM,
[SupportedChainId.ARBITRUM_RINKEBY]: USDC_ARBITRUM_RINKEBY,
[SupportedChainId.OPTIMISTIC_KOVAN]: USDC_OPTIMISTIC_KOVAN,
[SupportedChainId.POLYGON]: USDC_POLYGON,
[SupportedChainId.POLYGON_MUMBAI]: USDC_POLYGON_MUMBAI,
[SupportedChainId.GOERLI]: USDC_GÖRLI,
[SupportedChainId.RINKEBY]: USDC_RINKEBY,
[SupportedChainId.KOVAN]: USDC_KOVAN,
[SupportedChainId.ROPSTEN]: USDC_ROPSTEN,
}
export const DAI_POLYGON = new Token(
SupportedChainId.POLYGON,
'0x8f3Cf7ad23Cd3CaDbD9735AFf958023239c6A063',
@@ -73,13 +81,6 @@ export const WBTC_POLYGON = new Token(
'WBTC',
'Wrapped BTC'
)
export const USDC_OPTIMISM = new Token(
SupportedChainId.OPTIMISM,
'0x7F5c764cBc14f9669B88837ca1490cCa17c31607',
6,
'USDC',
'USD//C'
)
export const USDT = new Token(
SupportedChainId.MAINNET,
'0xdAC17F958D2ee523a2206206994597C13D831ec7',
@@ -208,8 +209,8 @@ export const UNI: { [chainId: number]: Token } = {
[SupportedChainId.KOVAN]: new Token(SupportedChainId.KOVAN, UNI_ADDRESS[42], 18, 'UNI', 'Uniswap'),
}
export const WRAPPED_NATIVE_CURRENCY: { [chainId: number]: Token } = {
...WETH9,
export const WRAPPED_NATIVE_CURRENCY: { [chainId: number]: Token | undefined } = {
...(WETH9 as Record<SupportedChainId, Token>),
[SupportedChainId.OPTIMISM]: new Token(
SupportedChainId.OPTIMISM,
'0x4200000000000000000000000000000000000006',
@@ -265,7 +266,9 @@ class MaticNativeCurrency extends NativeCurrency {
get wrapped(): Token {
if (!isMatic(this.chainId)) throw new Error('Not matic')
return WRAPPED_NATIVE_CURRENCY[this.chainId]
const wrapped = WRAPPED_NATIVE_CURRENCY[this.chainId]
invariant(wrapped instanceof Token)
return wrapped
}
public constructor(chainId: number) {
@@ -276,7 +279,8 @@ class MaticNativeCurrency extends NativeCurrency {
export class ExtendedEther extends Ether {
public get wrapped(): Token {
if (this.chainId in WRAPPED_NATIVE_CURRENCY) return WRAPPED_NATIVE_CURRENCY[this.chainId]
const wrapped = WRAPPED_NATIVE_CURRENCY[this.chainId]
if (wrapped) return wrapped
throw new Error('Unsupported chain ID')
}
@@ -296,3 +300,19 @@ export function nativeOnChain(chainId: number): NativeCurrency {
: ExtendedEther.onChain(chainId))
)
}
export const TOKEN_SHORTHANDS: { [shorthand: string]: { [chainId in SupportedChainId]?: string } } = {
USDC: {
[SupportedChainId.MAINNET]: USDC_MAINNET.address,
[SupportedChainId.ARBITRUM_ONE]: USDC_ARBITRUM.address,
[SupportedChainId.OPTIMISM]: USDC_OPTIMISM.address,
[SupportedChainId.ARBITRUM_RINKEBY]: USDC_ARBITRUM_RINKEBY.address,
[SupportedChainId.OPTIMISTIC_KOVAN]: USDC_OPTIMISTIC_KOVAN.address,
[SupportedChainId.POLYGON]: USDC_POLYGON.address,
[SupportedChainId.POLYGON_MUMBAI]: USDC_POLYGON_MUMBAI.address,
[SupportedChainId.GOERLI]: USDC_GÖRLI.address,
[SupportedChainId.RINKEBY]: USDC_RINKEBY.address,
[SupportedChainId.KOVAN]: USDC_KOVAN.address,
[SupportedChainId.ROPSTEN]: USDC_ROPSTEN.address,
},
}

View File

@@ -2,7 +2,7 @@ import { Currency, Token } from '@uniswap/sdk-core'
import { CHAIN_INFO } from 'constants/chainInfo'
import { L2_CHAIN_IDS, SupportedChainId, SupportedL2ChainId } from 'constants/chains'
import useActiveWeb3React from 'hooks/useActiveWeb3React'
import { useCurrencyFromMap, useTokenFromMap } from 'lib/hooks/useCurrency'
import { useCurrencyFromMap, useTokenFromMapOrNetwork } from 'lib/hooks/useCurrency'
import { getTokenFilter } from 'lib/hooks/useTokenList/filtering'
import { useMemo } from 'react'
@@ -159,7 +159,7 @@ export function useIsUserAddedToken(currency: Currency | undefined | null): bool
// otherwise returns the token
export function useToken(tokenAddress?: string | null): Token | null | undefined {
const tokens = useAllTokens()
return useTokenFromMap(tokens, tokenAddress)
return useTokenFromMapOrNetwork(tokens, tokenAddress)
}
export function useCurrency(currencyId?: string | null): Currency | null | undefined {

View File

@@ -1,6 +1,6 @@
import { renderHook } from '@testing-library/react-hooks'
import { CurrencyAmount, TradeType } from '@uniswap/sdk-core'
import { DAI, USDC } from 'constants/tokens'
import { DAI, USDC_MAINNET } from 'constants/tokens'
import { TradeState } from 'state/routing/types'
import { useRoutingAPITrade } from '../state/routing/useRoutingAPITrade'
@@ -10,7 +10,7 @@ import { useClientSideV3Trade } from './useClientSideV3Trade'
import useDebounce from './useDebounce'
import useIsWindowVisible from './useIsWindowVisible'
const USDCAmount = CurrencyAmount.fromRawAmount(USDC, '10000')
const USDCAmount = CurrencyAmount.fromRawAmount(USDC_MAINNET, '10000')
const DAIAmount = CurrencyAmount.fromRawAmount(DAI, '10000')
jest.mock('./useAutoRouterSupported')
@@ -126,10 +126,10 @@ describe('#useBestV3Trade ExactOut', () => {
expectRouterMock(TradeState.INVALID)
expectClientSideMock(TradeState.VALID)
const { result } = renderHook(() => useBestTrade(TradeType.EXACT_OUTPUT, DAIAmount, USDC))
const { result } = renderHook(() => useBestTrade(TradeType.EXACT_OUTPUT, DAIAmount, USDC_MAINNET))
expect(mockUseRoutingAPITrade).toHaveBeenCalledWith(TradeType.EXACT_OUTPUT, undefined, USDC)
expect(mockUseClientSideV3Trade).toHaveBeenCalledWith(TradeType.EXACT_OUTPUT, DAIAmount, USDC)
expect(mockUseRoutingAPITrade).toHaveBeenCalledWith(TradeType.EXACT_OUTPUT, undefined, USDC_MAINNET)
expect(mockUseClientSideV3Trade).toHaveBeenCalledWith(TradeType.EXACT_OUTPUT, DAIAmount, USDC_MAINNET)
expect(result.current).toEqual({ state: TradeState.VALID, trade: undefined })
})
@@ -138,17 +138,17 @@ describe('#useBestV3Trade ExactOut', () => {
expectRouterMock(TradeState.NO_ROUTE_FOUND)
expectClientSideMock(TradeState.VALID)
const { result } = renderHook(() => useBestTrade(TradeType.EXACT_OUTPUT, DAIAmount, USDC))
const { result } = renderHook(() => useBestTrade(TradeType.EXACT_OUTPUT, DAIAmount, USDC_MAINNET))
expect(mockUseRoutingAPITrade).toHaveBeenCalledWith(TradeType.EXACT_OUTPUT, undefined, USDC)
expect(mockUseClientSideV3Trade).toHaveBeenCalledWith(TradeType.EXACT_OUTPUT, DAIAmount, USDC)
expect(mockUseRoutingAPITrade).toHaveBeenCalledWith(TradeType.EXACT_OUTPUT, undefined, USDC_MAINNET)
expect(mockUseClientSideV3Trade).toHaveBeenCalledWith(TradeType.EXACT_OUTPUT, DAIAmount, USDC_MAINNET)
expect(result.current).toEqual({ state: TradeState.VALID, trade: undefined })
})
describe('when routing api is in non-error state', () => {
it('does not compute client side v3 trade if routing api is LOADING', () => {
expectRouterMock(TradeState.LOADING)
const { result } = renderHook(() => useBestTrade(TradeType.EXACT_OUTPUT, DAIAmount, USDC))
const { result } = renderHook(() => useBestTrade(TradeType.EXACT_OUTPUT, DAIAmount, USDC_MAINNET))
expect(mockUseClientSideV3Trade).toHaveBeenCalledWith(TradeType.EXACT_OUTPUT, undefined, undefined)
expect(result.current).toEqual({ state: TradeState.LOADING, trade: undefined })
@@ -157,7 +157,7 @@ describe('#useBestV3Trade ExactOut', () => {
it('does not compute client side v3 trade if routing api is VALID', () => {
expectRouterMock(TradeState.VALID)
const { result } = renderHook(() => useBestTrade(TradeType.EXACT_OUTPUT, DAIAmount, USDC))
const { result } = renderHook(() => useBestTrade(TradeType.EXACT_OUTPUT, DAIAmount, USDC_MAINNET))
expect(mockUseClientSideV3Trade).toHaveBeenCalledWith(TradeType.EXACT_OUTPUT, undefined, undefined)
expect(result.current).toEqual({ state: TradeState.VALID, trade: undefined })
@@ -166,7 +166,7 @@ describe('#useBestV3Trade ExactOut', () => {
it('does not compute client side v3 trade if routing api is SYNCING', () => {
expectRouterMock(TradeState.SYNCING)
const { result } = renderHook(() => useBestTrade(TradeType.EXACT_OUTPUT, DAIAmount, USDC))
const { result } = renderHook(() => useBestTrade(TradeType.EXACT_OUTPUT, DAIAmount, USDC_MAINNET))
expect(mockUseClientSideV3Trade).toHaveBeenCalledWith(TradeType.EXACT_OUTPUT, undefined, undefined)
expect(result.current).toEqual({ state: TradeState.SYNCING, trade: undefined })
@@ -178,7 +178,7 @@ describe('#useBestV3Trade ExactOut', () => {
expectRouterMock(TradeState.INVALID)
expectClientSideMock(TradeState.VALID)
renderHook(() => useBestTrade(TradeType.EXACT_OUTPUT, DAIAmount, USDC))
renderHook(() => useBestTrade(TradeType.EXACT_OUTPUT, DAIAmount, USDC_MAINNET))
expect(mockUseClientSideV3Trade).toHaveBeenCalledWith(TradeType.EXACT_OUTPUT, undefined, undefined)
})
@@ -187,9 +187,9 @@ describe('#useBestV3Trade ExactOut', () => {
expectRouterMock(TradeState.NO_ROUTE_FOUND)
expectClientSideMock(TradeState.VALID)
const { result } = renderHook(() => useBestTrade(TradeType.EXACT_OUTPUT, DAIAmount, USDC))
const { result } = renderHook(() => useBestTrade(TradeType.EXACT_OUTPUT, DAIAmount, USDC_MAINNET))
expect(mockUseClientSideV3Trade).toHaveBeenCalledWith(TradeType.EXACT_OUTPUT, DAIAmount, USDC)
expect(mockUseClientSideV3Trade).toHaveBeenCalledWith(TradeType.EXACT_OUTPUT, DAIAmount, USDC_MAINNET)
expect(result.current).toEqual({ state: TradeState.VALID, trade: undefined })
})
})

View File

@@ -36,21 +36,8 @@ export function useBestTrade(
debouncedOtherCurrency
)
const isLoading = amountSpecified !== undefined && debouncedAmount === undefined
// consider trade debouncing when inputs/outputs do not match
const debouncing =
routingAPITrade.trade &&
amountSpecified &&
(tradeType === TradeType.EXACT_INPUT
? !routingAPITrade.trade.inputAmount.equalTo(amountSpecified) ||
!amountSpecified.currency.equals(routingAPITrade.trade.inputAmount.currency) ||
!debouncedOtherCurrency?.equals(routingAPITrade.trade.outputAmount.currency)
: !routingAPITrade.trade.outputAmount.equalTo(amountSpecified) ||
!amountSpecified.currency.equals(routingAPITrade.trade.outputAmount.currency) ||
!debouncedOtherCurrency?.equals(routingAPITrade.trade.inputAmount.currency))
const useFallback = !autoRouterSupported || (!debouncing && routingAPITrade.state === TradeState.NO_ROUTE_FOUND)
const isLoading = routingAPITrade.state === TradeState.LOADING
const useFallback = !autoRouterSupported || routingAPITrade.state === TradeState.NO_ROUTE_FOUND
// only use client side router if routing api trade failed or is not supported
const bestV3Trade = useClientSideV3Trade(
@@ -63,9 +50,8 @@ export function useBestTrade(
return useMemo(
() => ({
...(useFallback ? bestV3Trade : routingAPITrade),
...(debouncing ? { state: TradeState.SYNCING } : {}),
...(isLoading ? { state: TradeState.LOADING } : {}),
}),
[bestV3Trade, debouncing, isLoading, routingAPITrade, useFallback]
[bestV3Trade, isLoading, routingAPITrade, useFallback]
)
}

View File

@@ -1,7 +1,7 @@
import { Token } from '@uniswap/sdk-core'
import { SupportedChainId } from 'constants/chains'
import uriToHttp from 'lib/utils/uriToHttp'
import Vibrant from 'node-vibrant/lib/bundle'
import Vibrant from 'node-vibrant/lib/bundle.js'
import { shade } from 'polished'
import { useLayoutEffect, useState } from 'react'
import { WrappedTokenInfo } from 'state/lists/wrappedTokenInfo'

View File

@@ -1,10 +1,15 @@
import { BigNumber } from '@ethersproject/bignumber'
import { useSingleCallResult } from 'lib/hooks/multicall'
import { useMemo } from 'react'
import { useInterfaceMulticall } from './useContract'
// gets the current timestamp from the blockchain
export default function useCurrentBlockTimestamp(): BigNumber | undefined {
const multicall = useInterfaceMulticall()
return useSingleCallResult(multicall, 'getCurrentBlockTimestamp')?.result?.[0]
const resultStr: string | undefined = useSingleCallResult(
multicall,
'getCurrentBlockTimestamp'
)?.result?.[0]?.toString()
return useMemo(() => (typeof resultStr === 'string' ? BigNumber.from(resultStr) : undefined), [resultStr])
}

View File

@@ -10,7 +10,7 @@ import { useSingleCallResult } from 'lib/hooks/multicall'
import { useMemo, useState } from 'react'
import { SWAP_ROUTER_ADDRESSES, V3_ROUTER_ADDRESS } from '../constants/addresses'
import { DAI, UNI, USDC } from '../constants/tokens'
import { DAI, UNI, USDC_MAINNET } from '../constants/tokens'
import { useEIP2612Contract } from './useContract'
import useIsArgentWallet from './useIsArgentWallet'
@@ -36,7 +36,7 @@ const PERMITTABLE_TOKENS: {
}
} = {
1: {
[USDC.address]: { type: PermitType.AMOUNT, name: 'USD Coin', version: '2' },
[USDC_MAINNET.address]: { type: PermitType.AMOUNT, name: 'USD Coin', version: '2' },
[DAI.address]: { type: PermitType.ALLOWED, name: 'Dai Stablecoin', version: '1' },
[UNI[1].address]: { type: PermitType.AMOUNT, name: 'Uniswap' },
},

View File

@@ -1,5 +1,6 @@
import JSBI from 'jsbi'
import { useSingleCallResult } from 'lib/hooks/multicall'
import { useMemo } from 'react'
import { useContract } from './useContract'
import useENSAddress from './useENSAddress'
@@ -22,5 +23,5 @@ export default function useGasPrice(): JSBI | undefined {
const contract = useContract(address ?? undefined, CHAIN_DATA_ABI, false)
const resultStr = useSingleCallResult(contract, 'latestAnswer').result?.[0]?.toString()
return typeof resultStr === 'string' ? JSBI.BigInt(resultStr) : undefined
return useMemo(() => (typeof resultStr === 'string' ? JSBI.BigInt(resultStr) : undefined), [resultStr])
}

View File

@@ -9,5 +9,5 @@ export default function useIsArgentWallet(): boolean {
const argentWalletDetector = useArgentWalletDetectorContract()
const inputs = useMemo(() => [account ?? undefined], [account])
const call = useSingleCallResult(argentWalletDetector, 'isArgentWallet', inputs, NEVER_RELOAD)
return call?.result?.[0] ?? false
return Boolean(call?.result?.[0])
}

View File

@@ -1,9 +1,11 @@
import { useCallback, useEffect, useState } from 'react'
const VISIBILITY_STATE_SUPPORTED = 'visibilityState' in document
function isVisibilityStateSupported() {
return 'visibilityState' in document
}
function isWindowVisible() {
return !VISIBILITY_STATE_SUPPORTED || document.visibilityState !== 'hidden'
return !isVisibilityStateSupported() || document.visibilityState !== 'hidden'
}
/**
@@ -16,7 +18,7 @@ export default function useIsWindowVisible(): boolean {
}, [setFocused])
useEffect(() => {
if (!VISIBILITY_STATE_SUPPORTED) return undefined
if (!isVisibilityStateSupported()) return undefined
document.addEventListener('visibilitychange', listener)
return () => {

View File

@@ -1,9 +1,10 @@
import { Interface } from '@ethersproject/abi'
import { Currency, Token } from '@uniswap/sdk-core'
import { BigintIsh, Currency, Token } from '@uniswap/sdk-core'
import IUniswapV3PoolStateJson from '@uniswap/v3-core/artifacts/contracts/interfaces/pool/IUniswapV3PoolState.sol/IUniswapV3PoolState.json'
import { computePoolAddress } from '@uniswap/v3-sdk'
import { FeeAmount, Pool } from '@uniswap/v3-sdk'
import useActiveWeb3React from 'hooks/useActiveWeb3React'
import JSBI from 'jsbi'
import { useMultipleContractSingleData } from 'lib/hooks/multicall'
import { useMemo } from 'react'
@@ -14,6 +15,69 @@ const { abi: IUniswapV3PoolStateABI } = IUniswapV3PoolStateJson
const POOL_STATE_INTERFACE = new Interface(IUniswapV3PoolStateABI) as IUniswapV3PoolStateInterface
// Classes are expensive to instantiate, so this caches the recently instantiated pools.
// This avoids re-instantiating pools as the other pools in the same request are loaded.
class PoolCache {
// Evict after 128 entries. Empirically, a swap uses 64 entries.
private static MAX_ENTRIES = 128
// These are FIFOs, using unshift/pop. This makes recent entries faster to find.
private static pools: Pool[] = []
private static addresses: { key: string; address: string }[] = []
static getPoolAddress(factoryAddress: string, tokenA: Token, tokenB: Token, fee: FeeAmount): string {
if (this.addresses.length > this.MAX_ENTRIES) {
this.addresses = this.addresses.slice(0, this.MAX_ENTRIES / 2)
}
const { address: addressA } = tokenA
const { address: addressB } = tokenB
const key = `${factoryAddress}:${addressA}:${addressB}:${fee.toString()}`
const found = this.addresses.find((address) => address.key === key)
if (found) return found.address
const address = {
key,
address: computePoolAddress({
factoryAddress,
tokenA,
tokenB,
fee,
}),
}
this.addresses.unshift(address)
return address.address
}
static getPool(
tokenA: Token,
tokenB: Token,
fee: FeeAmount,
sqrtPriceX96: BigintIsh,
liquidity: BigintIsh,
tick: number
): Pool {
if (this.pools.length > this.MAX_ENTRIES) {
this.pools = this.pools.slice(0, this.MAX_ENTRIES / 2)
}
const found = this.pools.find(
(pool) =>
pool.token0 === tokenA &&
pool.token1 === tokenB &&
pool.fee === fee &&
JSBI.EQ(pool.sqrtRatioX96, sqrtPriceX96) &&
JSBI.EQ(pool.liquidity, liquidity) &&
pool.tickCurrent === tick
)
if (found) return found
const pool = new Pool(tokenA, tokenB, fee, sqrtPriceX96, liquidity, tick)
this.pools.unshift(pool)
return pool
}
}
export enum PoolState {
LOADING,
NOT_EXISTS,
@@ -26,58 +90,57 @@ export function usePools(
): [PoolState, Pool | null][] {
const { chainId } = useActiveWeb3React()
const transformed: ([Token, Token, FeeAmount] | null)[] = useMemo(() => {
return poolKeys.map(([currencyA, currencyB, feeAmount]) => {
if (!chainId || !currencyA || !currencyB || !feeAmount) return null
const poolTokens: ([Token, Token, FeeAmount] | undefined)[] = useMemo(() => {
if (!chainId) return new Array(poolKeys.length)
const tokenA = currencyA?.wrapped
const tokenB = currencyB?.wrapped
if (!tokenA || !tokenB || tokenA.equals(tokenB)) return null
const [token0, token1] = tokenA.sortsBefore(tokenB) ? [tokenA, tokenB] : [tokenB, tokenA]
return [token0, token1, feeAmount]
return poolKeys.map(([currencyA, currencyB, feeAmount]) => {
if (currencyA && currencyB && feeAmount) {
const tokenA = currencyA.wrapped
const tokenB = currencyB.wrapped
if (tokenA.equals(tokenB)) return undefined
return tokenA.sortsBefore(tokenB) ? [tokenA, tokenB, feeAmount] : [tokenB, tokenA, feeAmount]
}
return undefined
})
}, [chainId, poolKeys])
const poolAddresses: (string | undefined)[] = useMemo(() => {
const v3CoreFactoryAddress = chainId && V3_CORE_FACTORY_ADDRESSES[chainId]
if (!v3CoreFactoryAddress) return new Array(poolTokens.length)
return transformed.map((value) => {
if (!v3CoreFactoryAddress || !value) return undefined
return computePoolAddress({
factoryAddress: v3CoreFactoryAddress,
tokenA: value[0],
tokenB: value[1],
fee: value[2],
})
})
}, [chainId, transformed])
return poolTokens.map((value) => value && PoolCache.getPoolAddress(v3CoreFactoryAddress, ...value))
}, [chainId, poolTokens])
const slot0s = useMultipleContractSingleData(poolAddresses, POOL_STATE_INTERFACE, 'slot0')
const liquidities = useMultipleContractSingleData(poolAddresses, POOL_STATE_INTERFACE, 'liquidity')
return useMemo(() => {
return poolKeys.map((_key, index) => {
const [token0, token1, fee] = transformed[index] ?? []
if (!token0 || !token1 || !fee) return [PoolState.INVALID, null]
const tokens = poolTokens[index]
if (!tokens) return [PoolState.INVALID, null]
const [token0, token1, fee] = tokens
if (!slot0s[index]) return [PoolState.INVALID, null]
const { result: slot0, loading: slot0Loading, valid: slot0Valid } = slot0s[index]
if (!liquidities[index]) return [PoolState.INVALID, null]
const { result: liquidity, loading: liquidityLoading, valid: liquidityValid } = liquidities[index]
if (!slot0Valid || !liquidityValid) return [PoolState.INVALID, null]
if (!tokens || !slot0Valid || !liquidityValid) return [PoolState.INVALID, null]
if (slot0Loading || liquidityLoading) return [PoolState.LOADING, null]
if (!slot0 || !liquidity) return [PoolState.NOT_EXISTS, null]
if (!slot0.sqrtPriceX96 || slot0.sqrtPriceX96.eq(0)) return [PoolState.NOT_EXISTS, null]
try {
return [PoolState.EXISTS, new Pool(token0, token1, fee, slot0.sqrtPriceX96, liquidity[0], slot0.tick)]
const pool = PoolCache.getPool(token0, token1, fee, slot0.sqrtPriceX96, liquidity[0], slot0.tick)
return [PoolState.EXISTS, pool]
} catch (error) {
console.error('Error when constructing the pool', error)
return [PoolState.NOT_EXISTS, null]
}
})
}, [liquidities, poolKeys, slot0s, transformed])
}, [liquidities, poolKeys, slot0s, poolTokens])
}
export function usePool(

View File

@@ -1,6 +1,6 @@
import { BigNumber } from '@ethersproject/bignumber'
import { Currency, CurrencyAmount, Token } from '@uniswap/sdk-core'
import { useSingleCallResult } from 'lib/hooks/multicall'
import { useMemo } from 'react'
import { useTokenContract } from './useContract'
@@ -9,7 +9,10 @@ import { useTokenContract } from './useContract'
export function useTotalSupply(token?: Currency): CurrencyAmount<Token> | undefined {
const contract = useTokenContract(token?.isToken ? token.address : undefined, false)
const totalSupply: BigNumber = useSingleCallResult(contract, 'totalSupply')?.result?.[0]
const totalSupplyStr: string | undefined = useSingleCallResult(contract, 'totalSupply')?.result?.[0]?.toString()
return token?.isToken && totalSupply ? CurrencyAmount.fromRawAmount(token, totalSupply.toString()) : undefined
return useMemo(
() => (token?.isToken && totalSupplyStr ? CurrencyAmount.fromRawAmount(token, totalSupplyStr) : undefined),
[token, totalSupplyStr]
)
}

View File

@@ -4,14 +4,14 @@ import tryParseCurrencyAmount from 'lib/utils/tryParseCurrencyAmount'
import { useMemo } from 'react'
import { SupportedChainId } from '../constants/chains'
import { DAI_OPTIMISM, USDC, USDC_ARBITRUM, USDC_POLYGON } from '../constants/tokens'
import { DAI_OPTIMISM, USDC_ARBITRUM, USDC_MAINNET, USDC_POLYGON } from '../constants/tokens'
import { useBestV2Trade } from './useBestV2Trade'
import { useClientSideV3Trade } from './useClientSideV3Trade'
// Stablecoin amounts used when calculating spot price for a given currency.
// The amount is large enough to filter low liquidity pairs.
export const STABLECOIN_AMOUNT_OUT: { [chainId: number]: CurrencyAmount<Token> } = {
[SupportedChainId.MAINNET]: CurrencyAmount.fromRawAmount(USDC, 100_000e6),
[SupportedChainId.MAINNET]: CurrencyAmount.fromRawAmount(USDC_MAINNET, 100_000e6),
[SupportedChainId.ARBITRUM_ONE]: CurrencyAmount.fromRawAmount(USDC_ARBITRUM, 10_000e6),
[SupportedChainId.OPTIMISM]: CurrencyAmount.fromRawAmount(DAI_OPTIMISM, 10_000e18),
[SupportedChainId.POLYGON]: CurrencyAmount.fromRawAmount(USDC_POLYGON, 10_000e6),
@@ -78,17 +78,18 @@ export function useStablecoinAmountFromFiatValue(fiatValue: string | null | unde
const { chainId } = useActiveWeb3React()
const stablecoin = chainId ? STABLECOIN_AMOUNT_OUT[chainId]?.currency : undefined
if (fiatValue === null || fiatValue === undefined || !chainId || !stablecoin) {
return undefined
}
return useMemo(() => {
if (fiatValue === null || fiatValue === undefined || !chainId || !stablecoin) {
return undefined
}
// trim for decimal precision when parsing
const parsedForDecimals = parseFloat(fiatValue).toFixed(stablecoin.decimals).toString()
try {
// parse USD string into CurrencyAmount based on stablecoin decimals
return tryParseCurrencyAmount(parsedForDecimals, stablecoin)
} catch (error) {
return undefined
}
// trim for decimal precision when parsing
const parsedForDecimals = parseFloat(fiatValue).toFixed(stablecoin.decimals).toString()
try {
// parse USD string into CurrencyAmount based on stablecoin decimals
return tryParseCurrencyAmount(parsedForDecimals, stablecoin)
} catch (error) {
return undefined
}
}, [chainId, fiatValue, stablecoin])
}

View File

@@ -73,4 +73,3 @@ ReactDOM.render(
if (process.env.REACT_APP_SERVICE_WORKER !== 'false') {
serviceWorkerRegistration.register()
}
export { INFURA_NETWORK_URLS } from 'constants/infura'

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

@@ -1,10 +1,10 @@
<svg width="23" height="20" viewBox="0 0 23 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<defs>
<linearGradient id="gradient1" x1="0" y1="0" x2="1" y2="0" gradientTransform="rotate(95)">
<linearGradient id="gradient" x1="0" y1="0" x2="1" y2="0" gradientTransform="rotate(95)">
<stop id="stop1" offset="0" stop-color="#2274E2"/>
<stop id="stop1" offset="0.5" stop-color="#2274E2"/>
<stop id="stop2" offset="1" stop-color="#3FB672" />
</linearGradient>
</defs>
<path d="M16 16C10 16 9 10 5 10M16 16C16 17.6569 17.3431 19 19 19C20.6569 19 22 17.6569 22 16C22 14.3431 20.6569 13 19 13C17.3431 13 16 14.3431 16 16ZM5 10C9 10 10 4 16 4M5 10H1.5M16 4C16 5.65685 17.3431 7 19 7C20.6569 7 22 5.65685 22 4C22 2.34315 20.6569 1 19 1C17.3431 1 16 2.34315 16 4Z" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" stroke="url(#gradient1)" />
<path d="M16 16C10 16 9 10 5 10M16 16C16 17.6569 17.3431 19 19 19C20.6569 19 22 17.6569 22 16C22 14.3431 20.6569 13 19 13C17.3431 13 16 14.3431 16 16ZM5 10C9 10 10 4 16 4M5 10H1.5M16 4C16 5.65685 17.3431 7 19 7C20.6569 7 22 5.65685 22 4C22 2.34315 20.6569 1 19 1C17.3431 1 16 2.34315 16 4Z" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" stroke="url(#gradient)" />
</svg>

Before

Width:  |  Height:  |  Size: 782 B

After

Width:  |  Height:  |  Size: 780 B

View File

@@ -0,0 +1,35 @@
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<mask id="mask">
<circle cx="12" cy="12" r="10" fill="black" stroke="black" stroke-width="2" />
<rect width="12" height="12" fill="white" stroke-width="0" />
<circle cx="2" cy="12" r="1" fill="white" stroke-width="0" />
<circle cx="12" cy="2" r="1" fill="white" stroke-width="0" />
</mask>
<circle
cx="12"
cy="12"
r="6"
stroke="none"
/>
<circle
id="track"
cx="12"
cy="12"
r="10"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
fill="none"
/>
<circle
cx="12"
cy="12"
r="10"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
fill="none"
mask="url(#mask)"
/>
</svg>

After

Width:  |  Height:  |  Size: 931 B

View File

@@ -1,12 +1,11 @@
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<mask id="mask">
<rect width="24" height="24" fill="white" stroke-width="0" />
<circle cx="12" cy="12" r="10" stroke="white" stroke-width="2" />
<rect width="12" height="12" fill="black" stroke-width="0" />
<circle cx="2" cy="12" r="1" fill="white" stroke-width="0" />
<circle cx="12" cy="2" r="1" fill="white" stroke-width="0" />
</mask>
<circle
id="circle"
cx="12"
cy="12"
r="10"

Before

Width:  |  Height:  |  Size: 608 B

After

Width:  |  Height:  |  Size: 592 B

View File

@@ -61,14 +61,13 @@ export interface Action {
children: ReactNode
}
export interface ActionButtonProps {
export interface BaseProps {
color?: Color
disabled?: boolean
action?: Action
onClick: () => void
children: ReactNode
}
export type ActionButtonProps = BaseProps & Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, keyof BaseProps>
export default function ActionButton({ color = 'accent', disabled, action, onClick, children }: ActionButtonProps) {
const textColor = useMemo(() => (color === 'accent' && !disabled ? 'onAccent' : 'currentColor'), [color, disabled])
return (

View File

@@ -2,6 +2,7 @@ import { Trans } from '@lingui/macro'
import Row from 'lib/components/Row'
import { Logo } from 'lib/icons'
import styled, { brand, ThemedText } from 'lib/theme'
import { memo } from 'react'
import ExternalLink from './ExternalLink'
@@ -24,7 +25,7 @@ const UniswapA = styled(ExternalLink)`
}
`
export default function BrandedFooter() {
export default memo(function BrandedFooter() {
return (
<Row justify="center">
<UniswapA href={`https://uniswap.org/`}>
@@ -37,4 +38,4 @@ export default function BrandedFooter() {
</UniswapA>
</Row>
)
}
})

View File

@@ -10,6 +10,7 @@ export const BaseButton = styled.button`
cursor: pointer;
font-size: inherit;
font-weight: inherit;
height: inherit;
line-height: inherit;
margin: 0;
padding: 0;

View File

@@ -40,7 +40,10 @@ export function Provider({ value, children }: ProviderProps) {
}
}, [active])
return (
<div ref={ref}>
<div
ref={ref}
style={{ isolation: 'isolate' }} // creates a new stacking context, preventing the dialog from intercepting non-dialog clicks
>
<Context.Provider value={context}>{children}</Context.Provider>
</div>
)

View File

@@ -1,17 +1,14 @@
import { Trans } from '@lingui/macro'
import useScrollbar from 'lib/hooks/useScrollbar'
import { AlertTriangle, Expando, Icon, Info, LargeIcon } from 'lib/icons'
import ActionButton from 'lib/components/ActionButton'
import Column from 'lib/components/Column'
import Expando from 'lib/components/Expando'
import { AlertTriangle, Icon, LargeIcon } from 'lib/icons'
import styled, { Color, ThemedText } from 'lib/theme'
import { ReactNode, useState } from 'react'
import ActionButton from '../ActionButton'
import { IconButton } from '../Button'
import Column from '../Column'
import Row from '../Row'
import Rule from '../Rule'
import { ReactNode, useCallback, useState } from 'react'
const HeaderIcon = styled(LargeIcon)`
flex-grow: 1;
transition: height 0.25s, width 0.25s;
svg {
transition: height 0.25s, width 0.25s;
@@ -34,7 +31,6 @@ export function StatusHeader({ icon: Icon, iconColor, iconSize = 4, children }:
{children}
</Column>
</Column>
<Rule />
</>
)
}
@@ -48,40 +44,6 @@ const ErrorHeader = styled(Column)<{ open: boolean }>`
transition: max-height 0.25s;
}
`
const ErrorColumn = styled(Column)``
const ExpandoColumn = styled(Column)<{ open: boolean }>`
flex-grow: ${({ open }) => (open ? 2 : 0)};
transition: flex-grow 0.25s, gap 0.25s;
${Rule} {
margin-bottom: ${({ open }) => (open ? 0 : 0.75)}em;
transition: margin-bottom 0.25s;
}
${ErrorColumn} {
flex-basis: 0;
flex-grow: ${({ open }) => (open ? 1 : 0)};
overflow-y: hidden;
position: relative;
transition: flex-grow 0.25s;
${Column} {
height: 100%;
padding: ${({ open }) => (open ? '0.5em 0' : 0)};
transition: padding 0.25s;
:after {
background: linear-gradient(#ffffff00, ${({ theme }) => theme.dialog});
bottom: 0;
content: '';
height: 0.75em;
pointer-events: none;
position: absolute;
width: calc(100% - 1em);
}
}
}
`
interface ErrorDialogProps {
header?: ReactNode
@@ -92,8 +54,8 @@ interface ErrorDialogProps {
export default function ErrorDialog({ header, error, action, onClick }: ErrorDialogProps) {
const [open, setOpen] = useState(false)
const [details, setDetails] = useState<HTMLDivElement | null>(null)
const scrollbar = useScrollbar(details)
const onExpand = useCallback(() => setOpen((open) => !open), [])
return (
<Column flex padded gap={0.75} align="stretch" style={{ height: '100%' }}>
<StatusHeader icon={AlertTriangle} iconColor="error" iconSize={open ? 3 : 4}>
@@ -104,27 +66,15 @@ export default function ErrorDialog({ header, error, action, onClick }: ErrorDia
<ThemedText.Body2>{header}</ThemedText.Body2>
</ErrorHeader>
</StatusHeader>
<Row>
<Row gap={0.5}>
<Info color="secondary" />
<ThemedText.Subhead2 color="secondary">
<Trans>Error details</Trans>
</ThemedText.Subhead2>
</Row>
<IconButton color="secondary" onClick={() => setOpen(!open)} icon={Expando} iconProps={{ open }} />
</Row>
<ExpandoColumn flex align="stretch" open={open}>
<Rule />
<ErrorColumn>
<Column gap={0.5} ref={setDetails} css={scrollbar}>
<ThemedText.Code>
{error.name}
{error.message ? `: ${error.message}` : ''}
</ThemedText.Code>
</Column>
</ErrorColumn>
<Column gap={open ? 0 : 0.75} style={{ transition: 'gap 0.25s' }}>
<Expando title={<Trans>Error details</Trans>} open={open} onExpand={onExpand} height={7.5}>
<ThemedText.Code userSelect>
{error.name}
{error.message ? `: ${error.message}` : ''}
</ThemedText.Code>
</Expando>
<ActionButton onClick={onClick}>{action}</ActionButton>
</ExpandoColumn>
</Column>
</Column>
)
}

View File

@@ -1,12 +1,14 @@
import { SupportedChainId } from 'constants/chains'
import useActiveWeb3React from 'lib/hooks/useActiveWeb3React'
import { Link } from 'lib/icons'
import styled, { Color } from 'lib/theme'
import { ReactNode, useMemo } from 'react'
import { ExplorerDataType, getExplorerLink } from 'utils/getExplorerLink'
import ExternalLink from './ExternalLink'
import Row from './Row'
const Link = styled(ExternalLink)<{ color: Color }>`
const StyledExternalLink = styled(ExternalLink)<{ color: Color }>`
color: ${({ theme, color }) => theme[color]};
text-decoration: none;
`
@@ -25,8 +27,10 @@ export default function EtherscanLink({ data, type, color = 'currentColor', chil
[chainId, data, type]
)
return (
<Link href={url} color={color} target="_blank">
{children}
</Link>
<StyledExternalLink href={url} color={color} target="_blank">
<Row gap={0.25}>
{children} <Link />
</Row>
</StyledExternalLink>
)
}

View File

@@ -0,0 +1,66 @@
import { IconButton } from 'lib/components/Button'
import Column from 'lib/components/Column'
import Row from 'lib/components/Row'
import Rule from 'lib/components/Rule'
import useScrollbar from 'lib/hooks/useScrollbar'
import { Expando as ExpandoIcon } from 'lib/icons'
import styled from 'lib/theme'
import { PropsWithChildren, ReactNode, useState } from 'react'
const HeaderColumn = styled(Column)`
transition: gap 0.25s;
`
const ExpandoColumn = styled(Column)<{ height: number; open: boolean }>`
height: ${({ height, open }) => (open ? height : 0)}em;
overflow: hidden;
position: relative;
transition: height 0.25s, padding 0.25s;
:after {
background: linear-gradient(#ffffff00, ${({ theme }) => theme.dialog});
bottom: 0;
content: '';
height: 0.75em;
pointer-events: none;
position: absolute;
width: calc(100% - 1em);
}
`
const InnerColumn = styled(Column)<{ height: number }>`
height: ${({ height }) => height}em;
padding: 0.5em 0;
`
interface ExpandoProps {
title: ReactNode
open: boolean
onExpand: () => void
// The absolute height of the expanded container, in em.
height: number
}
/** A scrollable Expando with an absolute height. */
export default function Expando({ title, open, onExpand, height, children }: PropsWithChildren<ExpandoProps>) {
const [scrollingEl, setScrollingEl] = useState<HTMLDivElement | null>(null)
const scrollbar = useScrollbar(scrollingEl)
return (
<Column>
<HeaderColumn gap={open ? 0.5 : 0.75}>
<Rule />
<Row>
{title}
<IconButton color="secondary" onClick={onExpand} icon={ExpandoIcon} iconProps={{ open }} />
</Row>
<Rule />
</HeaderColumn>
<ExpandoColumn open={open} height={height}>
<InnerColumn flex align="stretch" height={height} ref={setScrollingEl} css={scrollbar}>
{children}
</InnerColumn>
</ExpandoColumn>
</Column>
)
return null
}

View File

@@ -1,4 +1,3 @@
import JSBI from 'jsbi'
import styled, { css } from 'lib/theme'
import { forwardRef, HTMLProps, useCallback, useEffect, useState } from 'react'
@@ -78,12 +77,16 @@ interface EnforcedNumericInputProps extends NumericInputProps {
}
function isNumericallyEqual(a: string, b: string) {
const [aInteger, aDecimal] = a.split('.')
const [bInteger, bDecimal] = b.split('.')
return (
JSBI.equal(JSBI.BigInt(aInteger ?? 0), JSBI.BigInt(bInteger ?? 0)) &&
JSBI.equal(JSBI.BigInt(aDecimal ?? 0), JSBI.BigInt(bDecimal ?? 0))
)
const [aInteger, aDecimal] = toParts(a)
const [bInteger, bDecimal] = toParts(b)
return aInteger === bInteger && aDecimal === bDecimal
function toParts(num: string) {
let [integer, decimal] = num.split('.')
integer = integer?.match(/([1-9]\d*)/)?.[1] || ''
decimal = decimal?.match(/(\d*[1-9])/)?.[1] || ''
return [integer, decimal]
}
}
const NumericInput = forwardRef<HTMLInputElement, EnforcedNumericInputProps>(function NumericInput(

View File

@@ -14,14 +14,16 @@ const PopoverContainer = styled.div<{ show: boolean }>`
border: 1px solid ${({ theme }) => theme.outline};
border-radius: 0.5em;
opacity: ${(props) => (props.show ? 1 : 0)};
padding: 8px;
padding: 10px 12px;
transition: visibility 0.25s linear, opacity 0.25s linear;
visibility: ${(props) => (props.show ? 'visible' : 'hidden')};
z-index: ${Layer.TOOLTIP};
`
const Reference = styled.div`
align-self: flex-start;
display: inline-block;
height: 1em;
`
const Arrow = styled.div`

View File

@@ -6,6 +6,10 @@ const Rule = styled.hr<{ padded?: true; scrollingEdge?: 'top' | 'bottom' }>`
margin: 0 ${({ padded }) => (padded ? '0.75em' : 0)};
margin-bottom: ${({ scrollingEdge }) => (scrollingEdge === 'bottom' ? -1 : 0)}px;
margin-top: ${({ scrollingEdge }) => (scrollingEdge !== 'bottom' ? -1 : 0)}px;
// Integrators will commonly modify hr width - this overrides any modifications within the widget.
max-width: auto;
width: auto;
`
export default Rule

View File

@@ -1,6 +1,6 @@
import { useLingui } from '@lingui/react'
import { useUSDCValue } from 'hooks/useUSDCPrice'
import { loadingOpacityCss } from 'lib/css/loading'
import { Currency, CurrencyAmount } from '@uniswap/sdk-core'
import { loadingTransitionCss } from 'lib/css/loading'
import {
useIsSwapFieldIndependent,
useSwapAmount,
@@ -21,8 +21,8 @@ import Row from '../Row'
import TokenImg from '../TokenImg'
import TokenInput from './TokenInput'
export const LoadingRow = styled(Row)<{ $loading: boolean }>`
${loadingOpacityCss};
export const USDC = styled(Row)`
${loadingTransitionCss};
`
export const Balance = styled(ThemedText.Body2)<{ focused: boolean }>`
@@ -45,21 +45,40 @@ export interface InputProps {
focused: boolean
}
interface UseFormattedFieldAmountArguments {
disabled: boolean
currencyAmount?: CurrencyAmount<Currency>
fieldAmount?: string
}
export function useFormattedFieldAmount({ disabled, currencyAmount, fieldAmount }: UseFormattedFieldAmountArguments) {
return useMemo(() => {
if (disabled) {
return ''
}
if (fieldAmount !== undefined) {
return fieldAmount
}
if (currencyAmount) {
return currencyAmount.toSignificant(6)
}
return ''
}, [disabled, currencyAmount, fieldAmount])
}
export default function Input({ disabled, focused }: InputProps) {
const { i18n } = useLingui()
const {
[Field.INPUT]: { balance, amount: tradeCurrencyAmount, usdc },
trade: { state: tradeState },
currencyBalances: { [Field.INPUT]: balance },
currencyAmounts: { [Field.INPUT]: swapInputCurrencyAmount },
} = useSwapInfo()
const inputUSDC = useUSDCValue(swapInputCurrencyAmount)
const [swapInputAmount, updateSwapInputAmount] = useSwapAmount(Field.INPUT)
const [swapInputCurrency, updateSwapInputCurrency] = useSwapCurrency(Field.INPUT)
const [inputAmount, updateInputAmount] = useSwapAmount(Field.INPUT)
const [inputCurrency, updateInputCurrency] = useSwapCurrency(Field.INPUT)
const inputCurrencyAmount = useSwapCurrencyAmount(Field.INPUT)
// extract eagerly in case of reversal
usePrefetchCurrencyColor(swapInputCurrency)
usePrefetchCurrencyColor(inputCurrency)
const isRouteLoading = tradeState === TradeState.SYNCING || tradeState === TradeState.LOADING
const isDependentField = !useIsSwapFieldIndependent(Field.INPUT)
@@ -69,39 +88,41 @@ export default function Input({ disabled, focused }: InputProps) {
const mockApproved = true
// account for gas needed if using max on native token
const maxAmount = useMemo(() => maxAmountSpend(balance), [balance])
const onMax = useMemo(() => {
if (maxAmount?.greaterThan(0)) {
return () => updateSwapInputAmount(maxAmount.toExact())
}
return
}, [maxAmount, updateSwapInputAmount])
const max = useMemo(() => {
const maxAmount = maxAmountSpend(balance)
return maxAmount?.greaterThan(0) ? maxAmount.toExact() : undefined
}, [balance])
const balanceColor = useMemo(() => {
const insufficientBalance =
balance &&
(inputCurrencyAmount ? inputCurrencyAmount.greaterThan(balance) : swapInputCurrencyAmount?.greaterThan(balance))
(inputCurrencyAmount ? inputCurrencyAmount.greaterThan(balance) : tradeCurrencyAmount?.greaterThan(balance))
return insufficientBalance ? 'error' : undefined
}, [balance, inputCurrencyAmount, swapInputCurrencyAmount])
}, [balance, inputCurrencyAmount, tradeCurrencyAmount])
const amount = useFormattedFieldAmount({
disabled,
currencyAmount: tradeCurrencyAmount,
fieldAmount: inputAmount,
})
return (
<InputColumn gap={0.5} approved={mockApproved}>
<TokenInput
currency={swapInputCurrency}
amount={(swapInputAmount !== undefined ? swapInputAmount : swapInputCurrencyAmount?.toSignificant(6)) ?? ''}
currency={inputCurrency}
amount={amount}
max={max}
disabled={disabled}
onMax={onMax}
onChangeInput={updateSwapInputAmount}
onChangeCurrency={updateSwapInputCurrency}
onChangeInput={updateInputAmount}
onChangeCurrency={updateInputCurrency}
loading={isLoading}
>
<ThemedText.Body2 color="secondary">
<ThemedText.Body2 color="secondary" userSelect>
<Row>
<LoadingRow $loading={isLoading}>{inputUSDC ? `$${inputUSDC.toFixed(2)}` : '-'}</LoadingRow>
<USDC isLoading={isRouteLoading}>{usdc ? `$${formatCurrencyAmount(usdc, 6, 'en', 2)}` : '-'}</USDC>
{balance && (
<Balance color={balanceColor} focused={focused}>
Balance: <span style={{ userSelect: 'text' }}>{formatCurrencyAmount(balance, 4, i18n.locale)}</span>
Balance: <span>{formatCurrencyAmount(balance, 4, i18n.locale)}</span>
</Balance>
)}
</Row>

View File

@@ -1,6 +1,5 @@
import { Trans } from '@lingui/macro'
import { useLingui } from '@lingui/react'
import { useUSDCValue } from 'hooks/useUSDCPrice'
import { atom } from 'jotai'
import { useAtomValue } from 'jotai/utils'
import BrandedFooter from 'lib/components/BrandedFooter'
@@ -8,15 +7,13 @@ import { useIsSwapFieldIndependent, useSwapAmount, useSwapCurrency, useSwapInfo
import useCurrencyColor from 'lib/hooks/useCurrencyColor'
import { Field } from 'lib/state/swap'
import styled, { DynamicThemeProvider, ThemedText } from 'lib/theme'
import { PropsWithChildren, useMemo } from 'react'
import { PropsWithChildren } from 'react'
import { TradeState } from 'state/routing/types'
import { computeFiatValuePriceImpact } from 'utils/computeFiatValuePriceImpact'
import { formatCurrencyAmount } from 'utils/formatCurrencyAmount'
import { getPriceImpactWarning } from 'utils/prices'
import Column from '../Column'
import Row from '../Row'
import { Balance, InputProps, LoadingRow } from './Input'
import { Balance, InputProps, USDC, useFormattedFieldAmount } from './Input'
import TokenInput from './TokenInput'
export const colorAtom = atom<string | undefined>(undefined)
@@ -41,9 +38,9 @@ export default function Output({ disabled, focused, children }: PropsWithChildre
const { i18n } = useLingui()
const {
[Field.OUTPUT]: { balance, amount: outputCurrencyAmount, usdc: outputUSDC },
trade: { state: tradeState },
currencyBalances: { [Field.OUTPUT]: balance },
currencyAmounts: { [Field.INPUT]: inputCurrencyAmount, [Field.OUTPUT]: outputCurrencyAmount },
impact,
} = useSwapInfo()
const [swapOutputAmount, updateSwapOutputAmount] = useSwapAmount(Field.OUTPUT)
@@ -60,23 +57,11 @@ export default function Output({ disabled, focused, children }: PropsWithChildre
// different state true/null/false allow smoother color transition
const hasColor = swapOutputCurrency ? Boolean(color) || null : false
const inputUSDC = useUSDCValue(inputCurrencyAmount)
const outputUSDC = useUSDCValue(outputCurrencyAmount)
const priceImpact = useMemo(() => {
const fiatValuePriceImpact = computeFiatValuePriceImpact(inputUSDC, outputUSDC)
if (!fiatValuePriceImpact) return null
const color = getPriceImpactWarning(fiatValuePriceImpact)
const sign = fiatValuePriceImpact.lessThan(0) ? '+' : ''
const displayedPriceImpact = parseFloat(fiatValuePriceImpact.multiply(-1)?.toSignificant(3))
return (
<ThemedText.Body2 color={color}>
({sign}
{displayedPriceImpact}%)
</ThemedText.Body2>
)
}, [inputUSDC, outputUSDC])
const amount = useFormattedFieldAmount({
disabled,
currencyAmount: outputCurrencyAmount,
fieldAmount: swapOutputAmount,
})
return (
<DynamicThemeProvider color={color}>
@@ -88,20 +73,21 @@ export default function Output({ disabled, focused, children }: PropsWithChildre
</Row>
<TokenInput
currency={swapOutputCurrency}
amount={(swapOutputAmount !== undefined ? swapOutputAmount : outputCurrencyAmount?.toSignificant(6)) ?? ''}
amount={amount}
disabled={disabled}
onChangeInput={updateSwapOutputAmount}
onChangeCurrency={updateSwapOutputCurrency}
loading={isLoading}
>
<ThemedText.Body2 color="secondary">
<ThemedText.Body2 color="secondary" userSelect>
<Row>
<LoadingRow gap={0.5} $loading={isLoading}>
{outputUSDC?.toFixed(2)} {priceImpact}
</LoadingRow>
<USDC gap={0.5} isLoading={isRouteLoading}>
{outputUSDC ? `$${formatCurrencyAmount(outputUSDC, 6, 'en', 2)}` : '-'}{' '}
{impact.display && <ThemedText.Body2 color={impact.warning}>({impact.display})</ThemedText.Body2>}
</USDC>
{balance && (
<Balance focused={focused}>
Balance: <span style={{ userSelect: 'text' }}>{formatCurrencyAmount(balance, 4, i18n.locale)}</span>
Balance: <span>{formatCurrencyAmount(balance, 4, i18n.locale)}</span>
</Balance>
)}
</Row>

View File

@@ -0,0 +1,54 @@
import { useLingui } from '@lingui/react'
import { Trade } from '@uniswap/router-sdk'
import { Currency, CurrencyAmount, TradeType } from '@uniswap/sdk-core'
import Row from 'lib/components/Row'
import { ThemedText } from 'lib/theme'
import formatLocaleNumber from 'lib/utils/formatLocaleNumber'
import { useCallback, useMemo, useState } from 'react'
import { formatCurrencyAmount, formatPrice } from 'utils/formatCurrencyAmount'
import { TextButton } from '../Button'
interface PriceProps {
trade: Trade<Currency, Currency, TradeType>
outputUSDC?: CurrencyAmount<Currency>
}
/** Displays the price of a trade. If outputUSDC is included, also displays the unit price. */
export default function Price({ trade, outputUSDC }: PriceProps) {
const { i18n } = useLingui()
const { inputAmount, outputAmount, executionPrice } = trade
const [base, setBase] = useState<'input' | 'output'>('input')
const onClick = useCallback(() => setBase((base) => (base === 'input' ? 'output' : 'input')), [])
// Compute the usdc price from the output price, so that it aligns with the displayed price.
const { price, usdcPrice } = useMemo(() => {
switch (base) {
case 'input':
return {
price: executionPrice,
usdcPrice: outputUSDC?.multiply(inputAmount.decimalScale).divide(inputAmount),
}
case 'output':
return {
price: executionPrice.invert(),
usdcPrice: outputUSDC?.multiply(outputAmount.decimalScale).divide(outputAmount),
}
}
}, [base, executionPrice, inputAmount, outputAmount, outputUSDC])
return (
<TextButton color="primary" onClick={onClick}>
<ThemedText.Caption>
<Row gap={0.25}>
{formatLocaleNumber({ number: 1, sigFigs: 1, locale: i18n.locale })} {price.baseCurrency.symbol} ={' '}
{formatPrice(price, 6, i18n.locale)} {price.quoteCurrency.symbol}
{usdcPrice && (
<ThemedText.Caption color="secondary">(${formatCurrencyAmount(usdcPrice, 6, 'en', 2)})</ThemedText.Caption>
)}
</Row>
</ThemedText.Caption>
</TextButton>
)
}

View File

@@ -0,0 +1,139 @@
import { Plural, Trans } from '@lingui/macro'
import { Currency, TradeType } from '@uniswap/sdk-core'
import { FeeAmount } from '@uniswap/v3-sdk'
import { ReactComponent as DotLine } from 'assets/svg/dot_line.svg'
import Column from 'lib/components/Column'
import Row from 'lib/components/Row'
import Rule from 'lib/components/Rule'
import TokenImg from 'lib/components/TokenImg'
import { AutoRouter } from 'lib/icons'
import styled, { Layer, ThemedText } from 'lib/theme'
import { useMemo } from 'react'
import { InterfaceTrade } from 'state/routing/types'
import { getTokenPath, RoutingDiagramEntry } from './utils'
const StyledAutoRouterLabel = styled(ThemedText.ButtonSmall)`
@supports (-webkit-background-clip: text) and (-webkit-text-fill-color: transparent) {
background-image: linear-gradient(90deg, #2172e5 0%, #54e521 163.16%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
`
function Header({ routes }: { routes: RoutingDiagramEntry[] }) {
return (
<Row justify="space-between" gap={1}>
<ThemedText.Subhead2>
<Row gap={0.25}>
<AutoRouter />
<StyledAutoRouterLabel color="primary" lineHeight={'16px'}>
<Trans>Auto Router</Trans>
</StyledAutoRouterLabel>
</Row>
</ThemedText.Subhead2>
<ThemedText.Body2>
<Plural value={routes.length} _1="Best route via 1 hop" other="Best route via # hops" />
</ThemedText.Body2>
</Row>
)
}
const Dots = styled(DotLine)`
color: ${({ theme }) => theme.outline};
position: absolute;
z-index: ${Layer.UNDERLAYER};
`
const RouteRow = styled(Row)`
flex-wrap: nowrap;
`
const RouteNode = styled(Row)`
background-color: ${({ theme }) => theme.interactive};
border-radius: ${({ theme }) => `${(theme.borderRadius ?? 1) * 0.5}em`};
margin-left: 1.625em;
padding: 0.25em 0.375em;
width: max-content;
`
const RouteBadge = styled.div`
background-color: ${({ theme }) => theme.module};
border-radius: ${({ theme }) => `${(theme.borderRadius ?? 1) * 0.25}em`};
padding: 0.125em;
`
function RouteDetail({ route }: { route: RoutingDiagramEntry }) {
const protocol = route.protocol.toUpperCase()
return (
<RouteNode>
<Row gap={0.375}>
<ThemedText.Caption>{route.percent.toSignificant(2)}%</ThemedText.Caption>
<RouteBadge>
<ThemedText.Badge color="secondary">{protocol}</ThemedText.Badge>
</RouteBadge>
</Row>
</RouteNode>
)
}
const RoutePool = styled(RouteNode)`
margin: 0 0.75em;
`
function Pool({
originCurrency,
targetCurrency,
feeAmount,
}: {
originCurrency: Currency
targetCurrency: Currency
feeAmount: FeeAmount
}) {
return (
<RoutePool>
<ThemedText.Caption>
<Row gap={0.25}>
<TokenImg token={originCurrency} />
<TokenImg token={targetCurrency} style={{ marginLeft: '-0.65em' }} />
{feeAmount / 10_000}%
</Row>
</ThemedText.Caption>
</RoutePool>
)
}
function Route({ route }: { route: RoutingDiagramEntry }) {
const [originCurrency] = route.path[0]
const [, targetCurrency] = route.path[route.path.length - 1]
return (
<Row align="center" style={{ gridTemplateColumns: '1em 1fr 1em' }}>
<TokenImg token={originCurrency} />
<RouteRow flex style={{ position: 'relative' }}>
<Dots />
<RouteDetail route={route} />
<RouteRow justify="space-evenly" flex>
{route.path.map(([originCurrency, targetCurrency, feeAmount], index) => (
<Pool key={index} originCurrency={originCurrency} targetCurrency={targetCurrency} feeAmount={feeAmount} />
))}
</RouteRow>
</RouteRow>
<TokenImg token={targetCurrency} />
</Row>
)
}
export default function RoutingDiagram({ trade }: { trade: InterfaceTrade<Currency, Currency, TradeType> }) {
const routes: RoutingDiagramEntry[] = useMemo(() => getTokenPath(trade), [trade])
return (
<Column gap={0.75}>
<Header routes={routes} />
<Rule />
{routes.map((route, index) => (
<Route key={index} route={route} />
))}
</Column>
)
}

View File

@@ -0,0 +1,43 @@
import { Protocol } from '@uniswap/router-sdk'
import { Currency, Percent, TradeType } from '@uniswap/sdk-core'
import { Pair } from '@uniswap/v2-sdk'
import { FeeAmount } from '@uniswap/v3-sdk'
import { InterfaceTrade } from 'state/routing/types'
export interface RoutingDiagramEntry {
percent: Percent
path: [Currency, Currency, FeeAmount][]
protocol: Protocol
}
const V2_DEFAULT_FEE_TIER = 3000
/**
* Loops through all routes on a trade and returns an array of diagram entries.
*/
export function getTokenPath(trade: InterfaceTrade<Currency, Currency, TradeType>): RoutingDiagramEntry[] {
return trade.swaps.map(({ route: { path: tokenPath, pools, protocol }, inputAmount, outputAmount }) => {
const portion =
trade.tradeType === TradeType.EXACT_INPUT
? inputAmount.divide(trade.inputAmount)
: outputAmount.divide(trade.outputAmount)
const percent = new Percent(portion.numerator, portion.denominator)
const path: RoutingDiagramEntry['path'] = []
for (let i = 0; i < pools.length; i++) {
const nextPool = pools[i]
const tokenIn = tokenPath[i]
const tokenOut = tokenPath[i + 1]
const entry: RoutingDiagramEntry['path'][0] = [
tokenIn,
tokenOut,
nextPool instanceof Pair ? V2_DEFAULT_FEE_TIER : nextPool.fee,
]
path.push(entry)
}
return {
percent,
path,
protocol,
}
})
}

View File

@@ -2,11 +2,11 @@ import { Trans } from '@lingui/macro'
import { useAtom } from 'jotai'
import Popover from 'lib/components/Popover'
import { useTooltip } from 'lib/components/Tooltip'
import { getSlippageWarning, toPercent } from 'lib/hooks/useAllowedSlippage'
import { getSlippageWarning, toPercent } from 'lib/hooks/useSlippage'
import { AlertTriangle, Check, Icon, LargeIcon, XOctagon } from 'lib/icons'
import { autoSlippageAtom, maxSlippageAtom } from 'lib/state/settings'
import styled, { ThemedText } from 'lib/theme'
import { forwardRef, memo, ReactNode, useCallback, useMemo, useRef, useState } from 'react'
import { forwardRef, memo, ReactNode, useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { BaseButton, TextButton } from '../../Button'
import Column from '../../Column'
@@ -88,7 +88,6 @@ export default function MaxSlippageSelect() {
const [autoSlippage, setAutoSlippage] = useAtom(autoSlippageAtom)
const [maxSlippage, setMaxSlippage] = useAtom(maxSlippageAtom)
const maxSlippageInput = useMemo(() => maxSlippage?.toString() || '', [maxSlippage])
const [warning, setWarning] = useState<'warning' | 'error' | undefined>(getSlippageWarning(toPercent(maxSlippage)))
const option = useRef<HTMLButtonElement>(null)
const showTooltip = useTooltip(option.current)
@@ -96,20 +95,27 @@ export default function MaxSlippageSelect() {
const input = useRef<HTMLInputElement>(null)
const focus = useCallback(() => input.current?.focus(), [input])
const [warning, setWarning] = useState<'warning' | 'error' | undefined>(getSlippageWarning(toPercent(maxSlippage)))
useEffect(() => {
setWarning(getSlippageWarning(toPercent(maxSlippage)))
}, [maxSlippage])
const onInputSelect = useCallback(() => {
focus()
const percent = toPercent(maxSlippage)
const warning = getSlippageWarning(percent)
setAutoSlippage(!percent || warning === 'error')
}, [focus, maxSlippage, setAutoSlippage])
const processValue = useCallback(
(value: number | undefined) => {
const percent = toPercent(value)
const warning = getSlippageWarning(percent)
setWarning(warning)
setMaxSlippage(value)
setAutoSlippage(!percent || warning === 'error')
},
[setAutoSlippage, setMaxSlippage]
)
const onInputSelect = useCallback(() => {
focus()
processValue(maxSlippage)
}, [focus, maxSlippage, processValue])
return (
<Column gap={0.75}>
@@ -130,7 +136,7 @@ export default function MaxSlippageSelect() {
>
<Row color={warning === 'error' ? 'error' : undefined}>
<DecimalInput
size={Math.max(maxSlippageInput.length, 3)}
size={Math.max(maxSlippageInput.length, 4)}
value={maxSlippageInput}
onChange={(input) => processValue(+input)}
placeholder={placeholder}

View File

@@ -1,9 +1,10 @@
import { Trans } from '@lingui/macro'
import ErrorDialog, { StatusHeader } from 'lib/components/Error/ErrorDialog'
import EtherscanLink from 'lib/components/EtherscanLink'
import SwapSummary from 'lib/components/Swap/Summary'
import useInterval from 'lib/hooks/useInterval'
import { CheckCircle, Clock, Spinner } from 'lib/icons'
import { SwapTransactionInfo, Transaction } from 'lib/state/transactions'
import { SwapTransactionInfo, Transaction, TransactionType, WrapTransactionInfo } from 'lib/state/transactions'
import styled, { ThemedText } from 'lib/theme'
import ms from 'ms.macro'
import { useCallback, useMemo, useState } from 'react'
@@ -12,7 +13,6 @@ import { ExplorerDataType } from 'utils/getExplorerLink'
import ActionButton from '../../ActionButton'
import Column from '../../Column'
import Row from '../../Row'
import Summary from '../Summary'
const errorMessage = (
<Trans>
@@ -26,7 +26,9 @@ const TransactionRow = styled(Row)`
flex-direction: row-reverse;
`
function ElapsedTime({ tx }: { tx: Transaction<SwapTransactionInfo> }) {
type PendingTransaction = Transaction<SwapTransactionInfo | WrapTransactionInfo>
function ElapsedTime({ tx }: { tx: PendingTransaction }) {
const [elapsedMs, setElapsedMs] = useState(0)
useInterval(() => setElapsedMs(Date.now() - tx.addedTime), tx.receipt ? null : ms`1s`)
@@ -54,7 +56,7 @@ function ElapsedTime({ tx }: { tx: Transaction<SwapTransactionInfo> }) {
}
interface TransactionStatusProps {
tx: Transaction<SwapTransactionInfo>
tx: PendingTransaction
onClose: () => void
}
@@ -63,14 +65,24 @@ function TransactionStatus({ tx, onClose }: TransactionStatusProps) {
return tx.receipt?.status ? CheckCircle : Spinner
}, [tx.receipt?.status])
const heading = useMemo(() => {
return tx.receipt?.status ? <Trans>Transaction submitted</Trans> : <Trans>Transaction pending</Trans>
}, [tx.receipt?.status])
if (tx.info.type === TransactionType.SWAP) {
return tx.receipt?.status ? <Trans>Swap confirmed</Trans> : <Trans>Swap pending</Trans>
} else if (tx.info.type === TransactionType.WRAP) {
if (tx.info.unwrapped) {
return tx.receipt?.status ? <Trans>Unwrap confirmed</Trans> : <Trans>Unwrap pending</Trans>
}
return tx.receipt?.status ? <Trans>Wrap confirmed</Trans> : <Trans>Wrap pending</Trans>
}
return tx.receipt?.status ? <Trans>Transaction confirmed</Trans> : <Trans>Transaction pending</Trans>
}, [tx.info, tx.receipt?.status])
return (
<Column flex padded gap={0.75} align="stretch" style={{ height: '100%' }}>
<StatusHeader icon={Icon} iconColor={tx.receipt?.status ? 'success' : undefined}>
<ThemedText.Subhead1>{heading}</ThemedText.Subhead1>
<Summary input={tx.info.inputCurrencyAmount} output={tx.info.outputCurrencyAmount} />
{tx.info.type === TransactionType.SWAP ? (
<SwapSummary input={tx.info.inputCurrencyAmount} output={tx.info.outputCurrencyAmount} />
) : null}
</StatusHeader>
<TransactionRow flex>
<ThemedText.ButtonSmall>

View File

@@ -22,8 +22,11 @@ const UNI = (function () {
function Fixture() {
const setState = useUpdateAtom(swapAtom)
const {
allowedSlippage,
[Field.INPUT]: { usdc: inputUSDC },
[Field.OUTPUT]: { usdc: outputUSDC },
trade: { trade },
slippage,
impact,
} = useSwapInfo()
useEffect(() => {
@@ -37,7 +40,14 @@ function Fixture() {
return trade ? (
<Modal color="dialog">
<SummaryDialog onConfirm={() => void 0} trade={trade} allowedSlippage={allowedSlippage} />
<SummaryDialog
onConfirm={() => void 0}
trade={trade}
slippage={slippage}
inputUSDC={inputUSDC}
outputUSDC={outputUSDC}
impact={impact}
/>
</Modal>
) : null
}

View File

@@ -3,15 +3,14 @@ import { useLingui } from '@lingui/react'
import { Trade } from '@uniswap/router-sdk'
import { Currency, Percent, TradeType } from '@uniswap/sdk-core'
import { useAtomValue } from 'jotai/utils'
import { getSlippageWarning } from 'lib/hooks/useAllowedSlippage'
import Column from 'lib/components/Column'
import Row from 'lib/components/Row'
import { feeOptionsAtom } from 'lib/state/swap'
import styled, { Color, ThemedText } from 'lib/theme'
import { useMemo } from 'react'
import { currencyId } from 'utils/currencyId'
import { formatCurrencyAmount } from 'utils/formatCurrencyAmount'
import { computeRealizedLPFeeAmount, computeRealizedPriceImpact, getPriceImpactWarning } from 'utils/prices'
import Row from '../../Row'
import { computeRealizedLPFeeAmount } from 'utils/prices'
const Value = styled.span<{ color?: Color }>`
color: ${({ color, theme }) => color && theme[color]};
@@ -26,7 +25,7 @@ interface DetailProps {
function Detail({ label, value, color }: DetailProps) {
return (
<ThemedText.Caption>
<ThemedText.Caption userSelect>
<Row gap={2}>
<span>{label}</span>
<Value color={color}>{value}</Value>
@@ -37,16 +36,16 @@ function Detail({ label, value, color }: DetailProps) {
interface DetailsProps {
trade: Trade<Currency, Currency, TradeType>
allowedSlippage: Percent
slippage: { auto: boolean; allowed: Percent; warning?: Color }
priceImpact: { priceImpact?: string; warning?: Color }
}
export default function Details({ trade, allowedSlippage }: DetailsProps) {
export default function Details({ trade, slippage, priceImpact }: DetailsProps) {
const { inputAmount, outputAmount } = trade
const inputCurrency = inputAmount.currency
const outputCurrency = outputAmount.currency
const integrator = window.location.hostname
const feeOptions = useAtomValue(feeOptionsAtom)
const priceImpact = useMemo(() => computeRealizedPriceImpact(trade), [trade])
const lpFeeAmount = useMemo(() => computeRealizedLPFeeAmount(trade), [trade])
const { i18n } = useLingui()
@@ -62,7 +61,9 @@ export default function Details({ trade, allowedSlippage }: DetailsProps) {
}
}
rows.push([t`Price impact`, `${priceImpact.toFixed(2)}%`, getPriceImpactWarning(priceImpact)])
if (priceImpact.priceImpact) {
rows.push([t`Price impact`, priceImpact.priceImpact, priceImpact.warning])
}
if (lpFeeAmount) {
const parsedLpFee = formatCurrencyAmount(lpFeeAmount, 6, i18n.locale)
@@ -70,16 +71,16 @@ export default function Details({ trade, allowedSlippage }: DetailsProps) {
}
if (trade.tradeType === TradeType.EXACT_OUTPUT) {
const localizedMaxSent = formatCurrencyAmount(trade.maximumAmountIn(allowedSlippage), 6, i18n.locale)
const localizedMaxSent = formatCurrencyAmount(trade.maximumAmountIn(slippage.allowed), 6, i18n.locale)
rows.push([t`Maximum sent`, `${localizedMaxSent} ${inputCurrency.symbol}`])
}
if (trade.tradeType === TradeType.EXACT_INPUT) {
const localizedMaxSent = formatCurrencyAmount(trade.minimumAmountOut(allowedSlippage), 6, i18n.locale)
const localizedMaxSent = formatCurrencyAmount(trade.minimumAmountOut(slippage.allowed), 6, i18n.locale)
rows.push([t`Minimum received`, `${localizedMaxSent} ${outputCurrency.symbol}`])
}
rows.push([t`Slippage tolerance`, `${allowedSlippage.toFixed(2)}%`, getSlippageWarning(allowedSlippage)])
rows.push([t`Slippage tolerance`, `${slippage.allowed.toFixed(2)}%`, slippage.warning])
return rows
}, [
@@ -87,7 +88,7 @@ export default function Details({ trade, allowedSlippage }: DetailsProps) {
priceImpact,
lpFeeAmount,
trade,
allowedSlippage,
slippage,
outputAmount,
i18n.locale,
integrator,
@@ -96,10 +97,10 @@ export default function Details({ trade, allowedSlippage }: DetailsProps) {
])
return (
<>
<Column gap={0.5}>
{details.map(([label, detail, color]) => (
<Detail key={label} label={label} value={detail} color={color} />
))}
</>
</Column>
)
}

View File

@@ -1,54 +1,37 @@
import { useLingui } from '@lingui/react'
import { Currency, CurrencyAmount } from '@uniswap/sdk-core'
import { useUSDCValue } from 'hooks/useUSDCPrice'
import { PriceImpact } from 'lib/hooks/useUSDCPriceImpact'
import { ArrowRight } from 'lib/icons'
import styled from 'lib/theme'
import { ThemedText } from 'lib/theme'
import { useMemo } from 'react'
import { computeFiatValuePriceImpact } from 'utils/computeFiatValuePriceImpact'
import { PropsWithChildren } from 'react'
import { formatCurrencyAmount } from 'utils/formatCurrencyAmount'
import Column from '../../Column'
import Row from '../../Row'
import TokenImg from '../../TokenImg'
const Percent = styled.span<{ gain: boolean }>`
color: ${({ gain, theme }) => (gain ? theme.success : theme.error)};
`
interface TokenValueProps {
input: CurrencyAmount<Currency>
usdc?: boolean
change?: number
usdc?: CurrencyAmount<Currency>
}
function TokenValue({ input, usdc, change }: TokenValueProps) {
function TokenValue({ input, usdc, children }: PropsWithChildren<TokenValueProps>) {
const { i18n } = useLingui()
const percent = useMemo(() => {
if (change) {
const percent = change.toPrecision(3)
return change > 0 ? `(+${percent}%)` : `(${percent}%)`
}
return undefined
}, [change])
const usdcAmount = useUSDCValue(input)
return (
<Column justify="flex-start">
<Row gap={0.375} justify="flex-start">
<TokenImg token={input.currency} />
<ThemedText.Body2>
<ThemedText.Body2 userSelect>
{formatCurrencyAmount(input, 6, i18n.locale)} {input.currency.symbol}
</ThemedText.Body2>
</Row>
{usdc && usdcAmount && (
<Row justify="flex-start">
<ThemedText.Caption color="secondary">
${formatCurrencyAmount(usdcAmount, 2, i18n.locale)}
{change && <Percent gain={change > 0}> {percent}</Percent>}
</ThemedText.Caption>
</Row>
{usdc && (
<ThemedText.Caption color="secondary" userSelect>
<Row justify="flex-start" gap={0.25}>
${formatCurrencyAmount(usdc, 6, 'en', 2)}
{children}
</Row>
</ThemedText.Caption>
)}
</Column>
)
@@ -57,23 +40,19 @@ function TokenValue({ input, usdc, change }: TokenValueProps) {
interface SummaryProps {
input: CurrencyAmount<Currency>
output: CurrencyAmount<Currency>
usdc?: boolean
inputUSDC?: CurrencyAmount<Currency>
outputUSDC?: CurrencyAmount<Currency>
priceImpact?: PriceImpact
}
export default function Summary({ input, output, usdc }: SummaryProps) {
const inputUSDCValue = useUSDCValue(input)
const outputUSDCValue = useUSDCValue(output)
const priceImpact = useMemo(() => {
const computedChange = computeFiatValuePriceImpact(inputUSDCValue, outputUSDCValue)
return computedChange ? parseFloat(computedChange.multiply(-1)?.toSignificant(3)) : undefined
}, [inputUSDCValue, outputUSDCValue])
export default function Summary({ input, output, inputUSDC, outputUSDC, priceImpact }: SummaryProps) {
return (
<Row gap={usdc ? 1 : 0.25}>
<TokenValue input={input} usdc={usdc} />
<Row gap={priceImpact ? 1 : 0.25}>
<TokenValue input={input} usdc={inputUSDC} />
<ArrowRight />
<TokenValue input={output} usdc={usdc} change={priceImpact} />
<TokenValue input={output} usdc={outputUSDC}>
{priceImpact && <ThemedText.Caption color={priceImpact.warning}>({priceImpact.display})</ThemedText.Caption>}
</TokenValue>
</Row>
)
}

View File

@@ -1,121 +1,116 @@
import { Trans } from '@lingui/macro'
import { useLingui } from '@lingui/react'
import { Trade } from '@uniswap/router-sdk'
import { Currency, Percent, TradeType } from '@uniswap/sdk-core'
import { IconButton } from 'lib/components/Button'
import { useSwapTradeType } from 'lib/hooks/swap'
import { getSlippageWarning } from 'lib/hooks/useAllowedSlippage'
import useScrollbar from 'lib/hooks/useScrollbar'
import { AlertTriangle, BarChart, Expando, Info } from 'lib/icons'
import styled, { ThemedText } from 'lib/theme'
import formatLocaleNumber from 'lib/utils/formatLocaleNumber'
import { useMemo, useState } from 'react'
import { formatCurrencyAmount, formatPrice } from 'utils/formatCurrencyAmount'
import { computeRealizedPriceImpact, getPriceImpactWarning } from 'utils/prices'
import { Currency, CurrencyAmount, TradeType } from '@uniswap/sdk-core'
import ActionButton, { Action } from 'lib/components/ActionButton'
import Column from 'lib/components/Column'
import { Header } from 'lib/components/Dialog'
import Expando from 'lib/components/Expando'
import Row from 'lib/components/Row'
import { Slippage } from 'lib/hooks/useSlippage'
import { PriceImpact } from 'lib/hooks/useUSDCPriceImpact'
import { AlertTriangle, BarChart, Info } from 'lib/icons'
import styled, { Color, ThemedText } from 'lib/theme'
import { useCallback, useMemo, useState } from 'react'
import { formatCurrencyAmount } from 'utils/formatCurrencyAmount'
import { tradeMeaningfullyDiffers } from 'utils/tradeMeaningFullyDiffer'
import ActionButton, { Action } from '../../ActionButton'
import Column from '../../Column'
import { Header } from '../../Dialog'
import Row from '../../Row'
import Rule from '../../Rule'
import Price from '../Price'
import Details from './Details'
import Summary from './Summary'
export default Summary
const SummaryColumn = styled(Column)``
const ExpandoColumn = styled(Column)``
const DetailsColumn = styled(Column)``
const Estimate = styled(ThemedText.Caption)``
const Content = styled(Column)``
const Heading = styled(Column)``
const Footing = styled(Column)``
const Body = styled(Column)<{ open: boolean }>`
height: calc(100% - 2.5em);
${SummaryColumn} {
flex-grow: ${({ open }) => (open ? 0 : 1)};
${Content}, ${Heading} {
flex-grow: 1;
transition: flex-grow 0.25s;
}
${ExpandoColumn} {
flex-grow: ${({ open }) => (open ? 1 : 0)};
transition: flex-grow 0.25s;
${DetailsColumn} {
flex-basis: ${({ open }) => (open ? 7.5 : 0)}em;
overflow-y: hidden;
position: relative;
transition: flex-basis 0.25s;
${Column} {
height: 7.5em;
grid-template-rows: repeat(auto-fill, 1em);
padding: ${({ open }) => (open ? '0.5em 0' : 0)};
transition: padding 0.25s;
:after {
background: linear-gradient(#ffffff00, ${({ theme }) => theme.dialog});
bottom: 0;
content: '';
height: 0.75em;
pointer-events: none;
position: absolute;
width: calc(100% - 1em);
}
}
}
${Estimate} {
max-height: ${({ open }) => (open ? 0 : 56 / 12)}em; // 2 * line-height + padding
min-height: 0;
overflow-y: hidden;
padding: ${({ open }) => (open ? 0 : '1em 0')};
transition: ${({ open }) =>
open
? 'max-height 0.1s ease-out, padding 0.25s ease-out'
: 'flex-grow 0.25s ease-out, max-height 0.1s ease-in, padding 0.25s ease-out'};
}
${Footing} {
margin-bottom: ${({ open }) => (open ? '-0.75em' : undefined)};
max-height: ${({ open }) => (open ? 0 : '3em')};
opacity: ${({ open }) => (open ? 0 : 1)};
transition: max-height 0.25s, margin-bottom 0.25s, opacity 0.15s 0.1s;
visibility: ${({ open }) => (open ? 'hidden' : undefined)};
}
`
interface SummaryDialogProps {
trade: Trade<Currency, Currency, TradeType>
allowedSlippage: Percent
onConfirm: () => void
function Subhead({ priceImpact, slippage }: { priceImpact: { warning?: Color }; slippage: Slippage }) {
return (
<Row gap={0.5}>
{priceImpact.warning || slippage.warning ? (
<AlertTriangle color={priceImpact.warning || slippage.warning} />
) : (
<Info color="secondary" />
)}
<ThemedText.Subhead2 color={priceImpact.warning || slippage.warning || 'secondary'}>
{priceImpact.warning ? (
<Trans>High price impact</Trans>
) : slippage.warning ? (
<Trans>High slippage</Trans>
) : (
<Trans>Swap details</Trans>
)}
</ThemedText.Subhead2>
</Row>
)
}
export function SummaryDialog({ trade, allowedSlippage, onConfirm }: SummaryDialogProps) {
const { inputAmount, outputAmount, executionPrice } = trade
const inputCurrency = inputAmount.currency
const outputCurrency = outputAmount.currency
const priceImpact = useMemo(() => computeRealizedPriceImpact(trade), [trade])
const tradeType = useSwapTradeType()
function Estimate({ trade, slippage }: { trade: Trade<Currency, Currency, TradeType>; slippage: Slippage }) {
const { i18n } = useLingui()
const text = useMemo(() => {
switch (trade.tradeType) {
case TradeType.EXACT_INPUT:
return (
<Trans>
Output is estimated. You will receive at least{' '}
{formatCurrencyAmount(trade.minimumAmountOut(slippage.allowed), 6, i18n.locale)}{' '}
{trade.outputAmount.currency.symbol} or the transaction will revert.
</Trans>
)
case TradeType.EXACT_OUTPUT:
return (
<Trans>
Output is estimated. You will send at most{' '}
{formatCurrencyAmount(trade.maximumAmountIn(slippage.allowed), 6, i18n.locale)}{' '}
{trade.inputAmount.currency.symbol} or the transaction will revert.
</Trans>
)
}
}, [i18n.locale, slippage.allowed, trade])
return <ThemedText.Caption color="secondary">{text}</ThemedText.Caption>
}
const [open, setOpen] = useState(false)
const [details, setDetails] = useState<HTMLDivElement | null>(null)
const scrollbar = useScrollbar(details)
const warning = useMemo(() => {
return getPriceImpactWarning(priceImpact) || getSlippageWarning(allowedSlippage)
}, [allowedSlippage, priceImpact])
function ConfirmButton({
trade,
highPriceImpact,
onConfirm,
}: {
trade: Trade<Currency, Currency, TradeType>
highPriceImpact: boolean
onConfirm: () => void
}) {
const [ackPriceImpact, setAckPriceImpact] = useState(false)
const [confirmedTrade, setConfirmedTrade] = useState(trade)
const [ackTrade, setAckTrade] = useState(trade)
const doesTradeDiffer = useMemo(
() => Boolean(trade && confirmedTrade && tradeMeaningfullyDiffers(trade, confirmedTrade)),
[confirmedTrade, trade]
() => Boolean(trade && ackTrade && tradeMeaningfullyDiffers(trade, ackTrade)),
[ackTrade, trade]
)
const action = useMemo((): Action | undefined => {
if (doesTradeDiffer) {
return {
message: <Trans>Price updated</Trans>,
icon: BarChart,
onClick: () => setConfirmedTrade(trade),
onClick: () => setAckTrade(trade),
children: <Trans>Accept</Trans>,
}
} else if (getPriceImpactWarning(priceImpact) === 'error' && !ackPriceImpact) {
} else if (highPriceImpact && !ackPriceImpact) {
return {
message: <Trans>High price impact</Trans>,
onClick: () => setAckPriceImpact(true),
@@ -123,60 +118,58 @@ export function SummaryDialog({ trade, allowedSlippage, onConfirm }: SummaryDial
}
}
return
}, [ackPriceImpact, doesTradeDiffer, priceImpact, trade])
}, [ackPriceImpact, doesTradeDiffer, highPriceImpact, trade])
if (!(inputAmount && outputAmount && inputCurrency && outputCurrency)) {
return null
}
return (
<ActionButton onClick={onConfirm} action={action}>
<Trans>Confirm swap</Trans>
</ActionButton>
)
}
interface SummaryDialogProps {
trade: Trade<Currency, Currency, TradeType>
slippage: Slippage
inputUSDC?: CurrencyAmount<Currency>
outputUSDC?: CurrencyAmount<Currency>
impact: PriceImpact
onConfirm: () => void
}
export function SummaryDialog({ trade, slippage, inputUSDC, outputUSDC, impact, onConfirm }: SummaryDialogProps) {
const { inputAmount, outputAmount } = trade
const [open, setOpen] = useState(false)
const onExpand = useCallback(() => setOpen((open) => !open), [])
return (
<>
<Header title={<Trans>Swap summary</Trans>} ruled />
<Body flex align="stretch" gap={0.75} padded open={open}>
<SummaryColumn gap={0.75} flex justify="center">
<Summary input={inputAmount} output={outputAmount} usdc={true} />
<ThemedText.Caption>
{formatLocaleNumber({ number: 1, sigFigs: 1, locale: i18n.locale })} {inputCurrency.symbol} ={' '}
{formatPrice(executionPrice, 6, i18n.locale)} {outputCurrency.symbol}
</ThemedText.Caption>
</SummaryColumn>
<Rule />
<Row>
<Row gap={0.5}>
{warning ? <AlertTriangle color={warning} /> : <Info color="secondary" />}
<ThemedText.Subhead2 color="secondary">
<Trans>Swap details</Trans>
</ThemedText.Subhead2>
</Row>
<IconButton color="secondary" onClick={() => setOpen(!open)} icon={Expando} iconProps={{ open }} />
</Row>
<ExpandoColumn flex align="stretch">
<Rule />
<DetailsColumn>
<Column gap={0.5} ref={setDetails} css={scrollbar}>
<Details trade={trade} allowedSlippage={allowedSlippage} />
</Column>
</DetailsColumn>
<Estimate color="secondary">
<Trans>Output is estimated.</Trans>
{tradeType === TradeType.EXACT_INPUT && (
<Trans>
You will receive at least{' '}
{formatCurrencyAmount(trade.minimumAmountOut(allowedSlippage), 6, i18n.locale)} {outputCurrency.symbol}{' '}
or the transaction will revert.
</Trans>
)}
{tradeType === TradeType.EXACT_OUTPUT && (
<Trans>
You will send at most {formatCurrencyAmount(trade.maximumAmountIn(allowedSlippage), 6, i18n.locale)}{' '}
{inputCurrency.symbol} or the transaction will revert.
</Trans>
)}
</Estimate>
<ActionButton onClick={onConfirm} action={action}>
<Trans>Confirm swap</Trans>
</ActionButton>
</ExpandoColumn>
<Body flex align="stretch" padded gap={0.75} open={open}>
<Heading gap={0.75} flex justify="center">
<Summary
input={inputAmount}
output={outputAmount}
inputUSDC={inputUSDC}
outputUSDC={outputUSDC}
priceImpact={impact}
/>
<Price trade={trade} />
</Heading>
<Column gap={open ? 0 : 0.75} style={{ transition: 'gap 0.25s' }}>
<Expando
title={<Subhead priceImpact={impact} slippage={slippage} />}
open={open}
onExpand={onExpand}
height={7.25}
>
<Details trade={trade} slippage={slippage} priceImpact={impact} />
</Expando>
<Footing>
<Estimate trade={trade} slippage={slippage} />
</Footing>
<ConfirmButton trade={trade} highPriceImpact={impact.warning === 'error'} onConfirm={onConfirm} />
</Column>
</Body>
</>
)

View File

@@ -1,5 +1,5 @@
import { tokens } from '@uniswap/default-token-list'
import { DAI, USDC } from 'constants/tokens'
import { DAI, USDC_MAINNET } from 'constants/tokens'
import { useUpdateAtom } from 'jotai/utils'
import { useEffect } from 'react'
import { useSelect, useValue } from 'react-cosmos/fixture'
@@ -37,40 +37,36 @@ function Fixture() {
defaultValue: FEE_RECIPIENT_OPTIONS[1],
})
const optionsToAddressMap: Record<string, string> = {
none: '',
const optionsToAddressMap: Record<string, string | undefined> = {
None: undefined,
Native: 'NATIVE',
DAI: DAI.address,
USDC: USDC.address,
USDC: USDC_MAINNET.address,
}
const addressOptions = Object.keys(optionsToAddressMap)
const [defaultInput] = useSelect('defaultInputAddress', {
options: addressOptions,
defaultValue: addressOptions[2],
})
const inputOptions = ['', '0', '100', '-1']
const [defaultInputAmount] = useSelect('defaultInputAmount', {
options: inputOptions,
defaultValue: inputOptions[2],
})
const [defaultOutput] = useSelect('defaultOutputAddress', {
const [defaultInputToken] = useSelect('defaultInputToken', {
options: addressOptions,
defaultValue: addressOptions[1],
})
const [defaultOutputAmount] = useSelect('defaultOutputAmount', {
options: inputOptions,
defaultValue: inputOptions[0],
const [defaultInputAmount] = useValue('defaultInputAmount', { defaultValue: 1 })
const [defaultOutputToken] = useSelect('defaultOutputToken', {
options: addressOptions,
defaultValue: addressOptions[2],
})
const [defaultOutputAmount] = useValue('defaultOutputAmount', { defaultValue: 0 })
return (
<Swap
convenienceFee={convenienceFee}
convenienceFeeRecipient={convenienceFeeRecipient}
defaultInputAddress={optionsToAddressMap[defaultInput]}
defaultInputTokenAddress={optionsToAddressMap[defaultInputToken]}
defaultInputAmount={defaultInputAmount}
defaultOutputAddress={optionsToAddressMap[defaultOutput]}
defaultOutputTokenAddress={optionsToAddressMap[defaultOutputToken]}
defaultOutputAmount={defaultOutputAmount}
tokenList={tokens}
onConnectWallet={() => console.log('onConnectWallet')} // this handler is included as a test of functionality, but only logs
/>
)
}

View File

@@ -1,30 +1,30 @@
import { Trans } from '@lingui/macro'
import { Token } from '@uniswap/sdk-core'
import { useERC20PermitFromTrade } from 'hooks/useERC20Permit'
import { useUpdateAtom } from 'jotai/utils'
import { useAtomValue, useUpdateAtom } from 'jotai/utils'
import { WrapErrorText } from 'lib/components/Swap/WrapErrorText'
import { useSwapCurrencyAmount, useSwapInfo, useSwapTradeType } from 'lib/hooks/swap'
import useSwapApproval, {
ApprovalState,
import {
ApproveOrPermitState,
useApproveOrPermit,
useSwapApprovalOptimizedTrade,
useSwapRouterAddress,
} from 'lib/hooks/swap/useSwapApproval'
import { useSwapCallback } from 'lib/hooks/swap/useSwapCallback'
import { useAddTransaction } from 'lib/hooks/transactions'
import { usePendingApproval } from 'lib/hooks/transactions'
import useWrapCallback, { WrapError, WrapType } from 'lib/hooks/swap/useWrapCallback'
import { useAddTransaction, usePendingApproval } from 'lib/hooks/transactions'
import useActiveWeb3React from 'lib/hooks/useActiveWeb3React'
import useTransactionDeadline from 'lib/hooks/useTransactionDeadline'
import { Link, Spinner } from 'lib/icons'
import { displayTxHashAtom, Field } from 'lib/state/swap'
import { Spinner } from 'lib/icons'
import { displayTxHashAtom, feeOptionsAtom, Field } from 'lib/state/swap'
import { TransactionType } from 'lib/state/transactions'
import { useTheme } from 'lib/theme'
import { useCallback, useEffect, useMemo, useState } from 'react'
import { memo, useCallback, useEffect, useMemo, useState } from 'react'
import invariant from 'tiny-invariant'
import { ExplorerDataType } from 'utils/getExplorerLink'
import ActionButton, { ActionButtonProps } from '../ActionButton'
import Dialog from '../Dialog'
import EtherscanLink from '../EtherscanLink'
import Row from '../Row'
import { SummaryDialog } from './Summary'
interface SwapButtonProps {
@@ -35,19 +35,24 @@ function useIsPendingApproval(token?: Token, spender?: string): boolean {
return Boolean(usePendingApproval(token, spender))
}
export default function SwapButton({ disabled }: SwapButtonProps) {
export default memo(function SwapButton({ disabled }: SwapButtonProps) {
const { account, chainId } = useActiveWeb3React()
const { tokenColorExtraction } = useTheme()
const {
[Field.INPUT]: {
currency: inputCurrency,
amount: inputTradeCurrencyAmount,
balance: inputCurrencyBalance,
usdc: inputUSDC,
},
[Field.OUTPUT]: { amount: outputTradeCurrencyAmount, usdc: outputUSDC },
trade,
allowedSlippage,
currencies: { [Field.INPUT]: inputCurrency },
currencyBalances: { [Field.INPUT]: inputCurrencyBalance },
currencyAmounts: { [Field.INPUT]: inputCurrencyAmount, [Field.OUTPUT]: outputCurrencyAmount },
feeOptions,
slippage,
impact,
} = useSwapInfo()
const feeOptions = useAtomValue(feeOptionsAtom)
const tradeType = useSwapTradeType()
@@ -56,86 +61,113 @@ export default function SwapButton({ disabled }: SwapButtonProps) {
setActiveTrade((activeTrade) => activeTrade && trade.trade)
}, [trade])
// clear active trade on chain change
useEffect(() => {
setActiveTrade(undefined)
}, [chainId])
// TODO(zzmp): Return an optimized trade directly from useSwapInfo.
const optimizedTrade =
// Use trade.trade if there is no swap optimized trade. This occurs if approvals are still pending.
useSwapApprovalOptimizedTrade(trade.trade, allowedSlippage, useIsPendingApproval) || trade.trade
useSwapApprovalOptimizedTrade(trade.trade, slippage.allowed, useIsPendingApproval) || trade.trade
const approvalCurrencyAmount = useSwapCurrencyAmount(Field.INPUT)
const [approval, getApproval] = useSwapApproval(
const { approvalState, signatureData, handleApproveOrPermit } = useApproveOrPermit(
optimizedTrade,
allowedSlippage,
slippage.allowed,
useIsPendingApproval,
approvalCurrencyAmount
)
const approvalHash = usePendingApproval(
inputCurrency?.isToken ? inputCurrency : undefined,
useSwapRouterAddress(optimizedTrade)
)
const addTransaction = useAddTransaction()
const addApprovalTransaction = useCallback(() => {
getApproval().then((transaction) => {
const onApprove = useCallback(() => {
handleApproveOrPermit().then((transaction) => {
if (transaction) {
addTransaction({ type: TransactionType.APPROVAL, ...transaction })
}
})
}, [addTransaction, getApproval])
}, [addTransaction, handleApproveOrPermit])
const { type: wrapType, callback: wrapCallback, error: wrapError, loading: wrapLoading } = useWrapCallback()
const disableSwap = useMemo(
() =>
disabled ||
!optimizedTrade ||
!chainId ||
wrapLoading ||
(wrapType !== WrapType.NOT_APPLICABLE && wrapError) ||
approvalState === ApproveOrPermitState.PENDING_SIGNATURE ||
!(inputTradeCurrencyAmount && inputCurrencyBalance) ||
inputCurrencyBalance.lessThan(inputTradeCurrencyAmount),
[
disabled,
optimizedTrade,
chainId,
wrapLoading,
wrapType,
wrapError,
approvalState,
inputTradeCurrencyAmount,
inputCurrencyBalance,
]
)
const actionProps = useMemo((): Partial<ActionButtonProps> | undefined => {
if (!disabled && chainId) {
if (approval === ApprovalState.NOT_APPROVED) {
const currency = inputCurrency || approvalCurrencyAmount?.currency
invariant(currency)
return {
action: {
message: <Trans>Approve {currency.symbol} first</Trans>,
onClick: addApprovalTransaction,
children: <Trans>Approve</Trans>,
},
}
} else if (approval === ApprovalState.PENDING) {
return {
disabled: true,
action: {
message: (
<EtherscanLink type={ExplorerDataType.TRANSACTION} data={approvalHash}>
<Row gap={0.25}>
<Trans>
Approval pending <Link />
</Trans>
</Row>
</EtherscanLink>
if (disableSwap) {
return { disabled: true }
}
if (
wrapType === WrapType.NOT_APPLICABLE &&
(approvalState === ApproveOrPermitState.REQUIRES_APPROVAL ||
approvalState === ApproveOrPermitState.REQUIRES_SIGNATURE)
) {
const currency = inputCurrency || approvalCurrencyAmount?.currency
invariant(currency)
return {
action: {
message:
approvalState === ApproveOrPermitState.REQUIRES_SIGNATURE ? (
<Trans>Allow {currency.symbol} first</Trans>
) : (
<Trans>Approve {currency.symbol} first</Trans>
),
icon: Spinner,
onClick: addApprovalTransaction,
children: <Trans>Approve</Trans>,
},
}
} else if (inputCurrencyAmount && inputCurrencyBalance && !inputCurrencyBalance.lessThan(inputCurrencyAmount)) {
return {}
onClick: onApprove,
children:
approvalState === ApproveOrPermitState.REQUIRES_SIGNATURE ? <Trans>Allow</Trans> : <Trans>Approve</Trans>,
},
}
}
return { disabled: true }
}, [
addApprovalTransaction,
approval,
approvalCurrencyAmount?.currency,
approvalHash,
chainId,
disabled,
inputCurrency,
inputCurrencyAmount,
inputCurrencyBalance,
])
if (approvalState === ApproveOrPermitState.PENDING_APPROVAL) {
return {
disabled: true,
action: {
message: (
<EtherscanLink type={ExplorerDataType.TRANSACTION} data={approvalHash}>
<Trans>Approval pending</Trans>
</EtherscanLink>
),
icon: Spinner,
onClick: () => void 0, // @TODO: should not require an onclick
children: <Trans>Approve</Trans>,
},
}
}
return {}
}, [approvalCurrencyAmount?.currency, approvalHash, approvalState, disableSwap, inputCurrency, onApprove, wrapType])
const deadline = useTransactionDeadline()
const { signatureData } = useERC20PermitFromTrade(optimizedTrade, allowedSlippage, deadline)
// the callback to execute the swap
const { callback: swapCallback } = useSwapCallback({
trade: optimizedTrade,
allowedSlippage,
allowedSlippage: slippage.allowed,
recipientAddressOrName: account ?? null,
signatureData,
deadline,
@@ -149,13 +181,13 @@ export default function SwapButton({ disabled }: SwapButtonProps) {
swapCallback?.()
.then((response) => {
setDisplayTxHash(response.hash)
invariant(inputCurrencyAmount && outputCurrencyAmount)
invariant(inputTradeCurrencyAmount && outputTradeCurrencyAmount)
addTransaction({
response,
type: TransactionType.SWAP,
tradeType,
inputCurrencyAmount,
outputCurrencyAmount,
inputCurrencyAmount: inputTradeCurrencyAmount,
outputCurrencyAmount: outputTradeCurrencyAmount,
})
})
.catch((error) => {
@@ -165,22 +197,64 @@ export default function SwapButton({ disabled }: SwapButtonProps) {
.finally(() => {
setActiveTrade(undefined)
})
}, [addTransaction, inputCurrencyAmount, outputCurrencyAmount, setDisplayTxHash, swapCallback, tradeType])
}, [addTransaction, inputTradeCurrencyAmount, outputTradeCurrencyAmount, setDisplayTxHash, swapCallback, tradeType])
const ButtonText = useCallback(() => {
if ((wrapType === WrapType.WRAP || wrapType === WrapType.UNWRAP) && wrapError !== WrapError.NO_ERROR) {
return <WrapErrorText wrapError={wrapError} />
}
switch (wrapType) {
case WrapType.UNWRAP:
return <Trans>Unwrap</Trans>
case WrapType.WRAP:
return <Trans>Wrap</Trans>
case WrapType.NOT_APPLICABLE:
default:
return <Trans>Review swap</Trans>
}
}, [wrapError, wrapType])
const handleDialogClose = useCallback(() => {
setActiveTrade(undefined)
}, [])
const handleActionButtonClick = useCallback(async () => {
if (wrapType === WrapType.NOT_APPLICABLE) {
setActiveTrade(trade.trade)
} else {
const transaction = await wrapCallback()
addTransaction({
response: transaction,
type: TransactionType.WRAP,
unwrapped: wrapType === WrapType.UNWRAP,
currencyAmountRaw: transaction.value?.toString() ?? '0',
chainId,
})
setDisplayTxHash(transaction.hash)
}
}, [addTransaction, chainId, setDisplayTxHash, trade.trade, wrapCallback, wrapType])
return (
<>
<ActionButton
color={tokenColorExtraction ? 'interactive' : 'accent'}
onClick={() => setActiveTrade(trade.trade)}
onClick={handleActionButtonClick}
{...actionProps}
>
<Trans>Review swap</Trans>
<ButtonText />
</ActionButton>
{activeTrade && (
<Dialog color="dialog" onClose={() => setActiveTrade(undefined)}>
<SummaryDialog trade={activeTrade} allowedSlippage={allowedSlippage} onConfirm={onConfirm} />
<Dialog color="dialog" onClose={handleDialogClose}>
<SummaryDialog
trade={activeTrade}
slippage={slippage}
inputUSDC={inputUSDC}
outputUSDC={outputUSDC}
impact={impact}
onConfirm={onConfirm}
/>
</Dialog>
)}
</>
)
}
})

View File

@@ -1,6 +1,7 @@
import { BigNumber } from '@ethersproject/bignumber'
import { DefaultAddress, SwapProps } from 'lib/components/Swap'
import { IntegrationError } from 'lib/errors'
import { FeeOptions } from 'lib/hooks/swap/useSyncConvenienceFee'
import { DefaultAddress, TokenDefaults } from 'lib/hooks/swap/useSyncTokenDefaults'
import { PropsWithChildren, useEffect } from 'react'
import { isAddress } from '../../../utils'
@@ -15,7 +16,7 @@ function isAddressOrAddressMap(addressOrMap: DefaultAddress): boolean {
return false
}
type ValidatorProps = PropsWithChildren<SwapProps>
type ValidatorProps = PropsWithChildren<TokenDefaults & FeeOptions>
export default function SwapPropValidator(props: ValidatorProps) {
const { convenienceFee, convenienceFeeRecipient } = props
@@ -47,7 +48,7 @@ export default function SwapPropValidator(props: ValidatorProps) {
}
}, [convenienceFee, convenienceFeeRecipient])
const { defaultInputAddress, defaultInputAmount, defaultOutputAddress, defaultOutputAmount } = props
const { defaultInputTokenAddress, defaultInputAmount, defaultOutputTokenAddress, defaultOutputAmount } = props
useEffect(() => {
if (defaultOutputAmount && defaultInputAmount) {
throw new IntegrationError('defaultInputAmount and defaultOutputAmount may not both be defined.')
@@ -60,17 +61,25 @@ export default function SwapPropValidator(props: ValidatorProps) {
`defaultOutputAmount must be a positive number. (You set it to ${defaultOutputAmount})`
)
}
if (defaultInputAddress && !isAddressOrAddressMap(defaultInputAddress) && defaultInputAddress !== 'NATIVE') {
if (
defaultInputTokenAddress &&
!isAddressOrAddressMap(defaultInputTokenAddress) &&
defaultInputTokenAddress !== 'NATIVE'
) {
throw new IntegrationError(
`defaultInputAddress(es) must be a valid address or "NATIVE". (You set it to ${defaultInputAddress}`
`defaultInputTokenAddress(es) must be a valid address or "NATIVE". (You set it to ${defaultInputTokenAddress}`
)
}
if (defaultOutputAddress && !isAddressOrAddressMap(defaultOutputAddress) && defaultOutputAddress !== 'NATIVE') {
if (
defaultOutputTokenAddress &&
!isAddressOrAddressMap(defaultOutputTokenAddress) &&
defaultOutputTokenAddress !== 'NATIVE'
) {
throw new IntegrationError(
`defaultOutputAddress(es) must be a valid address or "NATIVE". (You set it to ${defaultOutputAddress}`
`defaultOutputTokenAddress(es) must be a valid address or "NATIVE". (You set it to ${defaultOutputTokenAddress}`
)
}
}, [defaultInputAddress, defaultInputAmount, defaultOutputAddress, defaultOutputAmount])
}, [defaultInputTokenAddress, defaultInputAmount, defaultOutputTokenAddress, defaultOutputAmount])
return <>{props.children}</>
}

View File

@@ -1,8 +1,10 @@
import 'setimmediate'
import { Trans } from '@lingui/macro'
import { Currency } from '@uniswap/sdk-core'
import { loadingOpacityCss } from 'lib/css/loading'
import { loadingTransitionCss } from 'lib/css/loading'
import styled, { keyframes, ThemedText } from 'lib/theme'
import { FocusEvent, ReactNode, useCallback, useRef, useState } from 'react'
import { ReactNode, useCallback, useEffect, useMemo, useRef, useState } from 'react'
import Button from '../Button'
import Column from '../Column'
@@ -14,7 +16,7 @@ const TokenInputRow = styled(Row)`
grid-template-columns: 1fr;
`
const ValueInput = styled(DecimalInput)<{ $loading: boolean }>`
const ValueInput = styled(DecimalInput)`
color: ${({ theme }) => theme.primary};
height: 1em;
@@ -26,7 +28,7 @@ const ValueInput = styled(DecimalInput)<{ $loading: boolean }>`
color: ${({ theme }) => theme.onHover(theme.secondary)};
}
${loadingOpacityCss}
${loadingTransitionCss}
`
const delayedFadeIn = keyframes`
@@ -50,8 +52,8 @@ const MaxButton = styled(Button)`
interface TokenInputProps {
currency?: Currency
amount: string
max?: string
disabled?: boolean
onMax?: () => void
onChangeInput: (input: string) => void
onChangeCurrency: (currency: Currency) => void
loading?: boolean
@@ -61,47 +63,59 @@ interface TokenInputProps {
export default function TokenInput({
currency,
amount,
max,
disabled,
onMax,
onChangeInput,
onChangeCurrency,
loading,
children,
}: TokenInputProps) {
const max = useRef<HTMLButtonElement>(null)
const [showMax, setShowMax] = useState(false)
const onFocus = useCallback(() => setShowMax(Boolean(onMax)), [onMax])
const onBlur = useCallback((e: FocusEvent) => {
if (e.relatedTarget !== max.current && e.relatedTarget !== input.current) {
setShowMax(false)
}
}, [])
const input = useRef<HTMLInputElement>(null)
const onSelect = useCallback(
(currency: Currency) => {
onChangeCurrency(currency)
setTimeout(() => input.current?.focus(), 0)
setImmediate(() => input.current?.focus())
},
[onChangeCurrency]
)
const maxButton = useRef<HTMLButtonElement>(null)
const hasMax = useMemo(() => Boolean(max && max !== amount), [max, amount])
const [showMax, setShowMax] = useState<boolean>(hasMax)
useEffect(() => setShowMax((hasMax && input.current?.contains(document.activeElement)) ?? false), [hasMax])
const onBlur = useCallback((e) => {
// Filters out clicks on input or maxButton, because onBlur fires before onClickMax.
if (!input.current?.contains(e.relatedTarget) && !maxButton.current?.contains(e.relatedTarget)) {
setShowMax(false)
}
}, [])
const onClickMax = useCallback(() => {
onChangeInput(max || '')
setShowMax(false)
setImmediate(() => {
input.current?.focus()
// Brings the start of the input into view. NB: This only works for clicks, not eg keyboard interactions.
input.current?.setSelectionRange(0, null)
})
}, [max, onChangeInput])
return (
<Column gap={0.25}>
<TokenInputRow gap={0.5} onBlur={onBlur}>
<ThemedText.H2>
<ValueInput
value={amount}
onFocus={onFocus}
onFocus={() => setShowMax(hasMax)}
onChange={onChangeInput}
disabled={disabled || !currency}
$loading={Boolean(loading)}
isLoading={Boolean(loading)}
ref={input}
></ValueInput>
</ThemedText.H2>
{showMax && (
<MaxButton onClick={onMax} ref={max}>
<ThemedText.ButtonMedium>
<MaxButton onClick={onClickMax} ref={maxButton}>
{/* Without a tab index, Safari would not populate the FocusEvent.relatedTarget needed by onBlur. */}
<ThemedText.ButtonMedium tabIndex={-1}>
<Trans>Max</Trans>
</ThemedText.ButtonMedium>
</MaxButton>

View File

@@ -1,14 +1,23 @@
import { Trans } from '@lingui/macro'
import { Currency, TradeType } from '@uniswap/sdk-core'
import useUSDCPrice from 'hooks/useUSDCPrice'
import { AlertTriangle, Icon, Info, Spinner } from 'lib/icons'
import { ThemedText } from 'lib/theme'
import { ReactNode, useMemo, useState } from 'react'
import { Currency, CurrencyAmount, TradeType } from '@uniswap/sdk-core'
import Column from 'lib/components/Column'
import Rule from 'lib/components/Rule'
import Tooltip from 'lib/components/Tooltip'
import { loadingCss } from 'lib/css/loading'
import { WrapType } from 'lib/hooks/swap/useWrapCallback'
import { PriceImpact } from 'lib/hooks/useUSDCPriceImpact'
import { AlertTriangle, Icon, Info, InlineSpinner } from 'lib/icons'
import styled, { ThemedText } from 'lib/theme'
import { ReactNode, useCallback } from 'react'
import { InterfaceTrade } from 'state/routing/types'
import { TextButton } from '../../Button'
import Row from '../../Row'
import RoutingTooltip from './RoutingTooltip'
import Price from '../Price'
import RoutingDiagram from '../RoutingDiagram'
const Loading = styled.span`
color: ${({ theme }) => theme.secondary};
${loadingCss};
`
interface CaptionProps {
icon?: Icon
@@ -27,55 +36,72 @@ function Caption({ icon: Icon = AlertTriangle, caption }: CaptionProps) {
export function ConnectWallet() {
return <Caption caption={<Trans>Connect wallet to swap</Trans>} />
}
export function UnsupportedNetwork() {
return <Caption caption={<Trans>Unsupported network - switch to another to trade.</Trans>} />
}
export function InsufficientBalance({ currency }: { currency: Currency }) {
return <Caption caption={<Trans>Insufficient {currency?.symbol} balance</Trans>} />
}
export function InsufficientLiquidity() {
return <Caption caption={<Trans>Insufficient liquidity in the pool for your trade</Trans>} />
}
export function Empty() {
return <Caption icon={Info} caption={<Trans>Enter an amount</Trans>} />
}
export function LoadingTrade() {
return <Caption icon={Spinner} caption={<Trans>Fetching best price</Trans>} />
return (
<Caption
icon={InlineSpinner}
caption={
<Loading>
<Trans>Fetching best price</Trans>
</Loading>
}
/>
)
}
export function Trade({ trade }: { trade: InterfaceTrade<Currency, Currency, TradeType> }) {
const [flip, setFlip] = useState(true)
const { inputAmount, outputAmount, executionPrice } = trade
const fiatValueInput = useUSDCPrice(inputAmount.currency)
const fiatValueOutput = useUSDCPrice(outputAmount.currency)
export function WrapCurrency({ loading, wrapType }: { loading: boolean; wrapType: WrapType.UNWRAP | WrapType.WRAP }) {
const WrapText = useCallback(() => {
if (wrapType === WrapType.WRAP) {
return loading ? <Trans>Wrapping native currency.</Trans> : <Trans>Wrap native currency.</Trans>
}
return loading ? <Trans>Unwrapping native currency.</Trans> : <Trans>Unwrap native currency.</Trans>
}, [loading, wrapType])
const ratio = useMemo(() => {
const [a, b] = flip ? [outputAmount, inputAmount] : [inputAmount, outputAmount]
const priceString = (!flip ? executionPrice : executionPrice?.invert())?.toSignificant(6)
const ratio = `1 ${a.currency.symbol} = ${priceString} ${b.currency.symbol}`
const usdc = !flip
? fiatValueInput
? ` ($${fiatValueInput.toSignificant(6)})`
: null
: fiatValueOutput
? ` ($${fiatValueOutput.toSignificant(6)})`
: null
return (
<Row gap={0.25} style={{ userSelect: 'text' }}>
{ratio}
{usdc && <ThemedText.Caption color="secondary">{usdc}</ThemedText.Caption>}
</Row>
)
}, [executionPrice, fiatValueInput, fiatValueOutput, flip, inputAmount, outputAmount])
return <Caption icon={Info} caption={<WrapText />} />
}
export function Trade({
trade,
outputUSDC,
impact,
}: {
trade: InterfaceTrade<Currency, Currency, TradeType>
outputUSDC?: CurrencyAmount<Currency>
impact: PriceImpact
}) {
return (
<>
<RoutingTooltip />
<TextButton color="primary" onClick={() => setFlip(!flip)}>
{ratio}
</TextButton>
<Tooltip placement="bottom" icon={impact.warning ? AlertTriangle : Info}>
<Column gap={0.75}>
{impact.warning && (
<>
<ThemedText.Caption>
The output amount is estimated at {impact.display} less than the input amount due to high price impact
</ThemedText.Caption>
<Rule />
</>
)}
<RoutingDiagram trade={trade} />
</Column>
</Tooltip>
<Price trade={trade} outputUSDC={outputUSDC} />
</>
)
}

View File

@@ -1,14 +0,0 @@
import { Info } from 'lib/icons'
export default function RoutingTooltip() {
return <Info color="secondary" />
/* TODO(zzmp): Implement post-beta launch.
return (
<Tooltip icon={Info} placement="bottom">
<ThemeProvider>
<ThemedText.Subhead2>TODO: Routing Tooltip</ThemedText.Subhead2>
</ThemeProvider>
</Tooltip>
)
*/
}

View File

@@ -1,10 +1,11 @@
import { ALL_SUPPORTED_CHAIN_IDS } from 'constants/chains'
import { useIsAmountPopulated, useSwapInfo } from 'lib/hooks/swap'
import useWrapCallback, { WrapType } from 'lib/hooks/swap/useWrapCallback'
import useActiveWeb3React from 'lib/hooks/useActiveWeb3React'
import { largeIconCss } from 'lib/icons'
import { Field } from 'lib/state/swap'
import styled, { ThemedText } from 'lib/theme'
import { useMemo } from 'react'
import { memo, useMemo } from 'react'
import { TradeState } from 'state/routing/types'
import Row from '../../Row'
@@ -16,16 +17,17 @@ const ToolbarRow = styled(Row)`
${largeIconCss}
`
export default function Toolbar({ disabled }: { disabled?: boolean }) {
export default memo(function Toolbar({ disabled }: { disabled?: boolean }) {
const { chainId } = useActiveWeb3React()
const {
[Field.INPUT]: { currency: inputCurrency, balance },
[Field.OUTPUT]: { currency: outputCurrency, usdc: outputUSDC },
trade: { trade, state },
currencies: { [Field.INPUT]: inputCurrency, [Field.OUTPUT]: outputCurrency },
currencyBalances: { [Field.INPUT]: balance },
impact,
} = useSwapInfo()
const isRouteLoading = state === TradeState.SYNCING || state === TradeState.LOADING
const isAmountPopulated = useIsAmountPopulated()
const { type: wrapType, loading: wrapLoading } = useWrapCallback()
const caption = useMemo(() => {
if (disabled) {
return <Caption.ConnectWallet />
@@ -36,6 +38,9 @@ export default function Toolbar({ disabled }: { disabled?: boolean }) {
}
if (inputCurrency && outputCurrency && isAmountPopulated) {
if (wrapType !== WrapType.NOT_APPLICABLE) {
return <Caption.WrapCurrency wrapType={wrapType} loading={wrapLoading} />
}
if (isRouteLoading) {
return <Caption.LoadingTrade />
}
@@ -46,12 +51,25 @@ export default function Toolbar({ disabled }: { disabled?: boolean }) {
return <Caption.InsufficientBalance currency={trade.inputAmount.currency} />
}
if (trade.inputAmount && trade.outputAmount) {
return <Caption.Trade trade={trade} />
return <Caption.Trade trade={trade} outputUSDC={outputUSDC} impact={impact} />
}
}
return <Caption.Empty />
}, [balance, chainId, disabled, inputCurrency, isAmountPopulated, isRouteLoading, outputCurrency, trade])
}, [
balance,
chainId,
disabled,
impact,
inputCurrency,
isAmountPopulated,
isRouteLoading,
outputCurrency,
outputUSDC,
trade,
wrapLoading,
wrapType,
])
return (
<>
@@ -63,4 +81,4 @@ export default function Toolbar({ disabled }: { disabled?: boolean }) {
</ThemedText.Caption>
</>
)
}
})

View File

@@ -0,0 +1,22 @@
import { Trans } from '@lingui/macro'
import { WrapError } from 'lib/hooks/swap/useWrapCallback'
import useNativeCurrency from 'lib/hooks/useNativeCurrency'
export function WrapErrorText({ wrapError }: { wrapError: WrapError }) {
const native = useNativeCurrency()
const wrapped = native?.wrapped
switch (wrapError) {
case WrapError.ENTER_NATIVE_AMOUNT:
return <Trans>Enter {native?.symbol} amount</Trans>
case WrapError.ENTER_WRAPPED_AMOUNT:
return <Trans>Enter {wrapped?.symbol} amount</Trans>
case WrapError.INSUFFICIENT_NATIVE_BALANCE:
return <Trans>Insufficient {native?.symbol} balance</Trans>
case WrapError.INSUFFICIENT_WRAPPED_BALANCE:
return <Trans>Insufficient {wrapped?.symbol} balance</Trans>
case WrapError.NO_ERROR:
default:
return null
}
}

View File

@@ -1,16 +1,16 @@
import { Trans } from '@lingui/macro'
import { TokenInfo } from '@uniswap/token-lists'
import { ALL_SUPPORTED_CHAIN_IDS } from 'constants/chains'
import { useAtom } from 'jotai'
import { SwapInfoUpdater } from 'lib/hooks/swap/useSwapInfo'
import useSyncConvenienceFee from 'lib/hooks/swap/useSyncConvenienceFee'
import useSyncSwapDefaults from 'lib/hooks/swap/useSyncSwapDefaults'
import useSyncConvenienceFee, { FeeOptions } from 'lib/hooks/swap/useSyncConvenienceFee'
import useSyncTokenDefaults, { TokenDefaults } from 'lib/hooks/swap/useSyncTokenDefaults'
import { usePendingTransactions } from 'lib/hooks/transactions'
import useActiveWeb3React from 'lib/hooks/useActiveWeb3React'
import useHasFocus from 'lib/hooks/useHasFocus'
import useOnSupportedNetwork from 'lib/hooks/useOnSupportedNetwork'
import useTokenList, { useSyncTokenList } from 'lib/hooks/useTokenList'
import { displayTxHashAtom } from 'lib/state/swap'
import { SwapTransactionInfo, Transaction, TransactionType } from 'lib/state/transactions'
import { SwapTransactionInfo, Transaction, TransactionType, WrapTransactionInfo } from 'lib/state/transactions'
import { useMemo, useState } from 'react'
import Dialog from '../Dialog'
@@ -26,63 +26,64 @@ import SwapButton from './SwapButton'
import SwapPropValidator from './SwapPropValidator'
import Toolbar from './Toolbar'
export type DefaultAddress = string | { [chainId: number]: string | 'NATIVE' } | 'NATIVE'
function getSwapTx(txs: { [hash: string]: Transaction }, hash?: string): Transaction<SwapTransactionInfo> | undefined {
function getTransactionFromMap(
txs: { [hash: string]: Transaction },
hash?: string
): Transaction<SwapTransactionInfo | WrapTransactionInfo> | undefined {
if (hash) {
const tx = txs[hash]
if (tx?.info?.type === TransactionType.SWAP) {
return tx as Transaction<SwapTransactionInfo>
}
if (tx?.info?.type === TransactionType.WRAP) {
return tx as Transaction<WrapTransactionInfo>
}
}
return
}
export interface SwapProps {
export interface SwapProps extends TokenDefaults, FeeOptions {
tokenList?: string | TokenInfo[]
defaultInputAddress?: DefaultAddress
defaultInputAmount?: string
defaultOutputAddress?: DefaultAddress
defaultOutputAmount?: string
convenienceFee?: number
convenienceFeeRecipient?: string | { [chainId: number]: string }
onConnectWallet?: () => void
}
export default function Swap(props: SwapProps) {
useSyncTokenList(props.tokenList)
useSyncSwapDefaults(props)
useSyncTokenDefaults(props)
useSyncConvenienceFee(props)
const { active, account, chainId } = useActiveWeb3React()
const { active, account } = useActiveWeb3React()
const [wrapper, setWrapper] = useState<HTMLDivElement | null>(null)
const [displayTxHash, setDisplayTxHash] = useAtom(displayTxHashAtom)
const pendingTxs = usePendingTransactions()
const displayTx = getSwapTx(pendingTxs, displayTxHash)
const displayTx = getTransactionFromMap(pendingTxs, displayTxHash)
const tokenList = useTokenList()
const onSupportedNetwork = useOnSupportedNetwork()
const isSwapSupported = useMemo(
() => Boolean(chainId && ALL_SUPPORTED_CHAIN_IDS.includes(chainId) && tokenList?.length),
[chainId, tokenList]
() => Boolean(active && onSupportedNetwork && tokenList?.length),
[active, onSupportedNetwork, tokenList?.length]
)
const focused = useHasFocus(wrapper)
const isInteractive = Boolean(active && onSupportedNetwork)
return (
<SwapPropValidator {...props}>
{isSwapSupported && <SwapInfoUpdater />}
<Header title={<Trans>Swap</Trans>}>
{active && <Wallet disabled={!account} onClick={props.onConnectWallet} />}
<Settings disabled={!active} />
<Settings disabled={!isInteractive} />
</Header>
<div ref={setWrapper}>
<BoundaryProvider value={wrapper}>
<Input disabled={!active} focused={focused} />
<ReverseButton disabled={!active} />
<Output disabled={!active} focused={focused}>
<Input disabled={!isInteractive} focused={focused} />
<ReverseButton disabled={!isInteractive} />
<Output disabled={!isInteractive} focused={focused}>
<Toolbar disabled={!active} />
<SwapButton disabled={!account} />
<SwapButton disabled={!isSwapSupported} />
</Output>
</BoundaryProvider>
</div>

View File

@@ -1,18 +1,23 @@
import { Currency } from '@uniswap/sdk-core'
import { useToken } from 'lib/hooks/useCurrency'
import useCurrencyLogoURIs from 'lib/hooks/useCurrencyLogoURIs'
import { Slash } from 'lib/icons'
import { MissingToken } from 'lib/icons'
import styled from 'lib/theme'
import { useCallback, useEffect, useState } from 'react'
const badSrcs = new Set<string>()
interface TokenImgProps {
className?: string
interface BaseProps {
token: Currency
}
function TokenImg({ className, token }: TokenImgProps) {
const srcs = useCurrencyLogoURIs(token)
type TokenImgProps = BaseProps & Omit<React.ImgHTMLAttributes<HTMLImageElement>, keyof BaseProps>
function TokenImg({ token, ...rest }: TokenImgProps) {
// Use the wrapped token info so that it includes the logoURI.
const tokenInfo = useToken(token.isToken ? token.wrapped.address : undefined) ?? token
const srcs = useCurrencyLogoURIs(tokenInfo)
const [src, setSrc] = useState<string | undefined>()
useEffect(() => {
setSrc(srcs.find((src) => !badSrcs.has(src)))
@@ -23,9 +28,9 @@ function TokenImg({ className, token }: TokenImgProps) {
}, [src, srcs])
if (src) {
return <img className={className} src={src} alt={token.name || token.symbol} onError={onError} />
return <img src={src} alt={tokenInfo.name || tokenInfo.symbol} onError={onError} {...rest} />
}
return <Slash className={className} color="secondary" />
return <MissingToken color="secondary" {...rest} />
}
export default styled(TokenImg)<{ size?: number }>`

View File

@@ -22,9 +22,14 @@ const StyledTokenButton = styled(Button)<{ empty?: boolean }>`
const TokenButtonRow = styled(Row)<{ collapsed: boolean }>`
height: 1.2em;
max-width: ${({ collapsed }) => (collapsed ? '1.2' : '8.2')}em;
overflow-x: hidden;
// max-width must have an absolute value in order to transition.
max-width: ${({ collapsed }) => (collapsed ? '1.2em' : '12em')};
overflow: hidden;
transition: max-width 0.25s linear;
img {
min-width: 1.2em;
}
`
interface TokenButtonProps {

View File

@@ -73,7 +73,6 @@ const TokenBalance = styled.div<{ isLoading: boolean }>`
background-color: ${({ theme, isLoading }) => isLoading && theme.secondary};
border-radius: 0.25em;
padding: 0.375em 0;
width: 1.5em;
`
function TokenOption({ index, value, style }: TokenOptionProps) {

View File

@@ -5,7 +5,7 @@ import { useCurrencyBalances } from 'lib/hooks/useCurrencyBalance'
import useNativeCurrency from 'lib/hooks/useNativeCurrency'
import useTokenList, { useIsTokenListLoaded, useQueryCurrencies } from 'lib/hooks/useTokenList'
import styled, { ThemedText } from 'lib/theme'
import { ElementRef, useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { ElementRef, memo, useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { currencyId } from 'utils/currencyId'
import Column from '../Column'
@@ -55,7 +55,7 @@ export function TokenSelectDialog({ value, onSelect }: TokenSelectDialogProps) {
// Give the balance-less tokens a small block period to avoid layout thrashing from re-sorting.
useEffect(() => {
if (!isLoaded) {
const timeout = setTimeout(() => setIsLoaded(true), 1500)
const timeout = setTimeout(() => setIsLoaded(true), 250)
return () => clearTimeout(timeout)
}
return
@@ -97,7 +97,21 @@ export function TokenSelectDialog({ value, onSelect }: TokenSelectDialogProps) {
)}
<Rule padded />
</Column>
{isLoaded ? <TokenOptions tokens={tokens} onSelect={onSelect} ref={setOptions} /> : <TokenOptionsSkeleton />}
{isLoaded ? (
tokens.length ? (
<TokenOptions tokens={tokens} onSelect={onSelect} ref={setOptions} />
) : (
<Column padded>
<Row justify="center">
<ThemedText.Body1 color="secondary">
<Trans>No results found.</Trans>
</ThemedText.Body1>
</Row>
</Column>
)
) : (
<TokenOptionsSkeleton />
)}
</>
)
}
@@ -109,10 +123,11 @@ interface TokenSelectProps {
onSelect: (value: Currency) => void
}
export default function TokenSelect({ value, collapsed, disabled, onSelect }: TokenSelectProps) {
export default memo(function TokenSelect({ value, collapsed, disabled, onSelect }: TokenSelectProps) {
usePrefetchBalances()
const [open, setOpen] = useState(false)
const onOpen = useCallback(() => setOpen(true), [])
const selectAndClose = useCallback(
(value: Currency) => {
onSelect(value)
@@ -122,7 +137,7 @@ export default function TokenSelect({ value, collapsed, disabled, onSelect }: To
)
return (
<>
<TokenButton value={value} collapsed={collapsed} disabled={disabled} onClick={() => setOpen(true)} />
<TokenButton value={value} collapsed={collapsed} disabled={disabled} onClick={onOpen} />
{open && (
<Dialog color="module" onClose={() => setOpen(false)}>
<TokenSelectDialog value={value} onSelect={selectAndClose} />
@@ -130,4 +145,4 @@ export default function TokenSelect({ value, collapsed, disabled, onSelect }: To
)}
</>
)
}
})

View File

@@ -1,7 +1,8 @@
import { Trans } from '@lingui/macro'
import { Wallet as WalletIcon } from 'lib/icons'
import styled, { ThemedText } from 'lib/theme'
import { ThemedText } from 'lib/theme'
import { TextButton } from './Button'
import Row from './Row'
interface WalletProps {
@@ -9,17 +10,15 @@ interface WalletProps {
onClick?: () => void
}
const ClickableRow = styled(Row)<{ onClick?: unknown }>`
cursor: ${({ onClick }) => onClick && 'pointer'};
`
export default function Wallet({ disabled, onClick }: WalletProps) {
return disabled ? (
<ThemedText.Caption color="secondary">
<ClickableRow gap={0.5} onClick={onClick}>
<WalletIcon />
<Trans>Connect your wallet</Trans>
</ClickableRow>
</ThemedText.Caption>
<TextButton disabled={!onClick} onClick={onClick} color="secondary" style={{ filter: 'none' }}>
<ThemedText.Caption>
<Row gap={0.5}>
<WalletIcon />
<Trans>Connect your wallet</Trans>
</Row>
</ThemedText.Caption>
</TextButton>
) : null
}

View File

@@ -1,39 +0,0 @@
import { initializeConnector, Web3ReactHooks } from '@web3-react/core'
import { EIP1193 } from '@web3-react/eip1193'
import { Actions, Connector, Provider as Eip1193Provider } from '@web3-react/types'
import { Url } from '@web3-react/url'
import { SetStateAction } from 'jotai'
import { RESET, useUpdateAtom } from 'jotai/utils'
import { injectedAtom, urlAtom } from 'lib/state/web3'
import { ReactNode, useEffect } from 'react'
interface Web3ProviderProps {
jsonRpcEndpoint?: string
provider?: Eip1193Provider
children: ReactNode
}
function useConnector<T extends { new (actions: Actions, initializer: I): Connector }, I>(
Connector: T,
initializer: I | undefined,
setContext: (update: typeof RESET | SetStateAction<[Connector, Web3ReactHooks]>) => void
) {
return useEffect(() => {
if (initializer) {
const [connector, hooks] = initializeConnector((actions) => new Connector(actions, initializer))
setContext([connector, hooks])
} else {
setContext(RESET)
}
}, [Connector, initializer, setContext])
}
export default function Web3Provider({ jsonRpcEndpoint, provider, children }: Web3ProviderProps) {
const setUrl = useUpdateAtom(urlAtom)
useConnector(Url, jsonRpcEndpoint, setUrl)
const setInjected = useUpdateAtom(injectedAtom)
useConnector(EIP1193, provider, setInjected)
return <>{children}</>
}

View File

@@ -1,9 +1,9 @@
import { Provider as EthersProvider } from '@ethersproject/abstract-provider'
import { Signer as EthersSigner } from '@ethersproject/abstract-signer'
import { Provider as Eip1193Provider } from '@web3-react/types'
import { DEFAULT_LOCALE, SupportedLocale } from 'constants/locales'
import { Provider as AtomProvider } from 'jotai'
import { TransactionsUpdater } from 'lib/hooks/transactions'
import { Web3Provider } from 'lib/hooks/useActiveWeb3React'
import { BlockUpdater } from 'lib/hooks/useBlockNumber'
import useEip1193Provider from 'lib/hooks/useEip1193Provider'
import { UNMOUNTING } from 'lib/hooks/useUnmount'
@@ -16,7 +16,6 @@ import { Provider as ReduxProvider } from 'react-redux'
import { Modal, Provider as DialogProvider } from './Dialog'
import ErrorBoundary, { ErrorHandler } from './Error/ErrorBoundary'
import WidgetPropValidator from './Error/WidgetsPropsValidator'
import Web3Provider from './Web3Provider'
const WidgetWrapper = styled.div<{ width?: number | string }>`
-moz-osx-font-smoothing: grayscale;
@@ -24,6 +23,7 @@ const WidgetWrapper = styled.div<{ width?: number | string }>`
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
background-color: ${({ theme }) => theme.container};
border-radius: ${({ theme }) => theme.borderRadius}em;
box-sizing: border-box;
color: ${({ theme }) => theme.primary};
display: flex;
flex-direction: column;
@@ -31,19 +31,19 @@ const WidgetWrapper = styled.div<{ width?: number | string }>`
font-size: 16px;
font-smooth: always;
font-variant: none;
height: 348px;
height: 356px;
min-width: 300px;
padding: 0.25em;
position: relative;
user-select: none;
width: ${({ width }) => width && (isNaN(Number(width)) ? width : `${width}px`)};
* {
box-sizing: border-box;
font-family: ${({ theme }) => theme.fontFamily};
user-select: none;
font-family: ${({ theme }) => (typeof theme.fontFamily === 'string' ? theme.fontFamily : theme.fontFamily.font)};
@supports (font-variation-settings: normal) {
font-family: ${({ theme }) => theme.fontFamilyVariable};
font-family: ${({ theme }) => (typeof theme.fontFamily === 'string' ? undefined : theme.fontFamily.variable)};
}
}
`
@@ -94,7 +94,7 @@ function Updaters() {
export type WidgetProps = {
theme?: Theme
locale?: SupportedLocale
provider?: Eip1193Provider | EthersProvider | { provider: EthersProvider; signer: EthersSigner }
provider?: Eip1193Provider | EthersProvider
jsonRpcEndpoint?: string
width?: string | number
dialog?: HTMLElement | null

View File

@@ -1,14 +1,19 @@
import { initializeConnector } from '@web3-react/core'
import { MetaMask } from '@web3-react/metamask'
import { Connector } from '@web3-react/types'
import { WalletConnect } from '@web3-react/walletconnect'
import { SupportedChainId } from 'constants/chains'
import { INFURA_NETWORK_URLS } from 'constants/infura'
import { DEFAULT_LOCALE, SUPPORTED_LOCALES } from 'constants/locales'
import Widget from 'lib/components/Widget'
import { darkTheme, defaultTheme, lightTheme } from 'lib/theme'
import { ReactNode, useEffect, useMemo } from 'react'
import { ReactNode, useEffect, useState } from 'react'
import { useSelect, useValue } from 'react-cosmos/fixture'
export const [metaMask] = initializeConnector<MetaMask>((actions) => new MetaMask(actions))
const [metaMask] = initializeConnector<MetaMask>((actions) => new MetaMask(actions))
const [walletConnect] = initializeConnector<WalletConnect>(
(actions) => new WalletConnect(actions, { rpc: INFURA_NETWORK_URLS })
)
export default function Wrapper({ children }: { children: ReactNode }) {
const [width] = useValue('width', { defaultValue: 360 })
@@ -27,21 +32,40 @@ export default function Wrapper({ children }: { children: ReactNode }) {
options: [NO_JSON_RPC, ...Object.values(INFURA_NETWORK_URLS).sort()],
})
const NO_PROVIDER = 'None'
const NO_CONNECTOR = 'None'
const META_MASK = 'MetaMask'
const [providerType] = useSelect('Provider', {
defaultValue: NO_PROVIDER,
options: [NO_PROVIDER, META_MASK],
const WALLET_CONNECT = 'WalletConnect'
const [connectorType] = useSelect('Provider', {
defaultValue: NO_CONNECTOR,
options: [NO_CONNECTOR, META_MASK, WALLET_CONNECT],
})
const provider = useMemo(() => {
switch (providerType) {
case META_MASK:
metaMask.activate()
return metaMask.provider
default:
return undefined
const [connector, setConnector] = useState<Connector>()
useEffect(() => {
let stale = false
activateConnector(connectorType)
return () => {
stale = true
}
}, [providerType])
async function activateConnector(connectorType: 'None' | 'MetaMask' | 'WalletConnect') {
let connector: Connector
switch (connectorType) {
case META_MASK:
await metaMask.activate()
connector = metaMask
break
case WALLET_CONNECT:
await walletConnect.activate()
connector = walletConnect
}
if (!stale) {
setConnector((oldConnector) => {
oldConnector?.deactivate?.()
return connector
})
}
}
}, [connectorType])
return (
<Widget
@@ -49,7 +73,7 @@ export default function Wrapper({ children }: { children: ReactNode }) {
theme={theme}
locale={locale}
jsonRpcEndpoint={jsonRpcEndpoint === NO_JSON_RPC ? undefined : jsonRpcEndpoint}
provider={provider}
provider={connector?.provider}
>
{children}
</Widget>

View File

@@ -1,8 +1,14 @@
import { css } from 'lib/theme'
// need to use $loading as `loading` is a reserved prop
export const loadingOpacityCss = css<{ $loading: boolean }>`
filter: ${({ $loading }) => ($loading ? 'grayscale(1)' : 'none')};
opacity: ${({ $loading }) => ($loading ? '0.4' : '1')};
transition: opacity 0.2s ease-in-out;
export const loadingOpacity = 0.6
export const loadingCss = css`
filter: grayscale(1);
opacity: ${loadingOpacity};
`
// need to use isLoading as `loading` is a reserved prop
export const loadingTransitionCss = css<{ isLoading: boolean }>`
${({ isLoading }) => isLoading && loadingCss};
transition: opacity ${({ isLoading }) => (isLoading ? 0 : 0.2)}s ease-in-out;
`

View File

@@ -1,10 +1,13 @@
import { BigintIsh, CurrencyAmount, Token, TradeType } from '@uniswap/sdk-core'
import { AlphaRouter, AlphaRouterConfig, AlphaRouterParams, ChainId } from '@uniswap/smart-order-router'
import JSBI from 'jsbi'
import useBlockNumber from 'lib/hooks/useBlockNumber'
import { GetQuoteResult } from 'state/routing/types'
import { transformSwapRouteToGetQuoteResult } from 'utils/transformSwapRouteToGetQuoteResult'
export const AUTO_ROUTER_SUPPORTED_CHAINS: ChainId[] = Object.values(ChainId) as number[]
export const AUTO_ROUTER_SUPPORTED_CHAINS: ChainId[] = Object.values(ChainId).filter((chainId): chainId is ChainId =>
Number.isInteger(chainId)
)
async function getQuote(
{
@@ -96,3 +99,14 @@ export async function getClientSideQuote(
routerConfig
)
}
/** Used to keep quotes up to date given a certain block age. Returns undefined if past limit. */
export function useFilterFreshQuote(
quoteResult: GetQuoteResult | undefined,
maxBlockAge = 10
): GetQuoteResult | undefined {
const block = useBlockNumber()
if (!block || !quoteResult) return undefined
if (block - (Number(quoteResult.blockNumber) || 0) > maxBlockAge) return undefined
return quoteResult
}

View File

@@ -2,13 +2,16 @@ import { Protocol } from '@uniswap/router-sdk'
import { Currency, CurrencyAmount, TradeType } from '@uniswap/sdk-core'
import { ChainId } from '@uniswap/smart-order-router'
import useDebounce from 'hooks/useDebounce'
import useLast from 'hooks/useLast'
import { useStablecoinAmountFromFiatValue } from 'hooks/useUSDCPrice'
import { useEffect, useMemo, useState } from 'react'
import { useCallback, useMemo } from 'react'
import { GetQuoteResult, InterfaceTrade, TradeState } from 'state/routing/types'
import { computeRoutes, transformRoutesToTrade } from 'state/routing/utils'
import useWrapCallback, { WrapType } from '../swap/useWrapCallback'
import useActiveWeb3React from '../useActiveWeb3React'
import { getClientSideQuote } from './clientSideSmartOrderRouter'
import usePoll from '../usePoll'
import { getClientSideQuote, useFilterFreshQuote } from './clientSideSmartOrderRouter'
import { useRoutingAPIArguments } from './useRoutingAPIArguments'
/**
@@ -22,14 +25,11 @@ const DistributionPercents: { [key: number]: number } = {
[ChainId.ARBITRUM_ONE]: 25,
[ChainId.ARBITRUM_RINKEBY]: 25,
}
const DEFAULT_DISTRIBUTION_PERCENT = 10
function getConfig(chainId: ChainId | undefined) {
return {
// Limit to only V2 and V3.
protocols: [Protocol.V2, Protocol.V3],
distributionPercent: (chainId && DistributionPercents[chainId]) ?? DEFAULT_DISTRIBUTION_PERCENT,
}
}
@@ -42,22 +42,20 @@ export default function useClientSideSmartOrderRouterTrade<TTradeType extends Tr
state: TradeState
trade: InterfaceTrade<Currency, Currency, TTradeType> | undefined
} {
const amount = useMemo(() => amountSpecified?.asFraction, [amountSpecified])
const [currencyIn, currencyOut] =
tradeType === TradeType.EXACT_INPUT
? [amountSpecified?.currency, otherCurrency]
: [otherCurrency, amountSpecified?.currency]
// Debounce is used to prevent excessive requests to SOR, as it is data intensive.
// This helps provide a "syncing" state the UI can reference for loading animations.
const inputs = useMemo(() => [tradeType, amountSpecified, otherCurrency], [tradeType, amountSpecified, otherCurrency])
const debouncedInputs = useDebounce(inputs, 200)
const isDebouncing = inputs !== debouncedInputs
const chainId = amountSpecified?.currency.chainId
const { library } = useActiveWeb3React()
const [currencyIn, currencyOut]: [Currency | undefined, Currency | undefined] = useMemo(
() =>
tradeType === TradeType.EXACT_INPUT
? [amountSpecified?.currency, otherCurrency]
: [otherCurrency, amountSpecified?.currency],
[amountSpecified, otherCurrency, tradeType]
// Fast user actions (ie updating the input) should be debounced, but currency changes should not.
const [debouncedAmount, debouncedCurrencyIn, debouncedCurrencyOut] = useDebounce(
useMemo(() => [amount, currencyIn, currencyOut], [amount, currencyIn, currencyOut]),
200
)
const isDebouncing =
amount !== debouncedAmount && currencyIn === debouncedCurrencyIn && currencyOut === debouncedCurrencyOut
const queryArgs = useRoutingAPIArguments({
tokenIn: currencyIn,
@@ -66,58 +64,53 @@ export default function useClientSideSmartOrderRouterTrade<TTradeType extends Tr
tradeType,
useClientSideRouter: true,
})
const chainId = amountSpecified?.currency.chainId
const { library } = useActiveWeb3React()
const params = useMemo(() => chainId && library && { chainId, provider: library }, [chainId, library])
const [loading, setLoading] = useState(false)
const [{ data: quoteResult, error }, setResult] = useState<{
data?: GetQuoteResult
error?: unknown
}>({ error: undefined })
const config = useMemo(() => getConfig(chainId), [chainId])
const { type: wrapType } = useWrapCallback()
// When arguments update, make a new call to SOR for updated quote
useEffect(() => {
setLoading(true)
if (isDebouncing) return
let stale = false
fetchQuote()
return () => {
stale = true
setLoading(false)
const getQuoteResult = useCallback(async (): Promise<{ data?: GetQuoteResult; error?: unknown }> => {
if (wrapType !== WrapType.NOT_APPLICABLE) return { error: undefined }
if (!queryArgs || !params) return { error: undefined }
try {
return await getClientSideQuote(queryArgs, params, config)
} catch {
return { error: true }
}
}, [config, params, queryArgs, wrapType])
async function fetchQuote() {
if (queryArgs && params) {
let result
try {
result = await getClientSideQuote(queryArgs, params, config)
} catch {
result = { error: true }
}
if (!stale) {
setResult(result)
setLoading(false)
}
}
}
}, [queryArgs, params, config, isDebouncing])
const { data, error } = usePoll(getQuoteResult, JSON.stringify(queryArgs), isDebouncing) ?? {
error: undefined,
}
const quoteResult = useFilterFreshQuote(data)
const isLoading = !quoteResult
const route = useMemo(
() => computeRoutes(currencyIn, currencyOut, tradeType, quoteResult),
[currencyIn, currencyOut, quoteResult, tradeType]
)
const gasUseEstimateUSD = useStablecoinAmountFromFiatValue(quoteResult?.gasUseEstimateUSD) ?? null
const trade = useMemo(() => {
if (route) {
try {
return route && transformRoutesToTrade(route, tradeType, gasUseEstimateUSD)
} catch (e: unknown) {
console.debug('transformRoutesToTrade failed: ', e)
}
}
return
}, [gasUseEstimateUSD, route, tradeType])
const trade =
useLast(
useMemo(() => {
if (route) {
try {
return route && transformRoutesToTrade(route, tradeType, gasUseEstimateUSD)
} catch (e: unknown) {
console.debug('transformRoutesToTrade failed: ', e)
}
}
return
}, [gasUseEstimateUSD, route, tradeType]),
Boolean
) ?? undefined
// Dont return old trade if currencies dont match.
const isStale =
(currencyIn && !trade?.inputAmount?.currency.equals(currencyIn)) ||
(currencyOut && !trade?.outputAmount?.currency.equals(currencyOut))
return useMemo(() => {
if (!currencyIn || !currencyOut) {
@@ -125,10 +118,14 @@ export default function useClientSideSmartOrderRouterTrade<TTradeType extends Tr
}
// Returns the last trade state while syncing/loading to avoid jank from clearing the last trade while loading.
if (isDebouncing) {
return { state: TradeState.SYNCING, trade }
} else if (loading) {
return { state: TradeState.LOADING, trade }
if (!quoteResult && !error) {
if (isStale) {
return { state: TradeState.LOADING, trade: undefined }
} else if (isDebouncing) {
return { state: TradeState.SYNCING, trade }
} else if (isLoading) {
return { state: TradeState.LOADING, trade }
}
}
let otherAmount = undefined
@@ -151,5 +148,17 @@ export default function useClientSideSmartOrderRouterTrade<TTradeType extends Tr
return { state: TradeState.VALID, trade }
}
return { state: TradeState.INVALID, trade: undefined }
}, [currencyIn, currencyOut, isDebouncing, loading, quoteResult, error, route, queryArgs, trade, tradeType])
}, [
currencyIn,
currencyOut,
error,
quoteResult,
route,
queryArgs,
trade,
isStale,
isDebouncing,
isLoading,
tradeType,
])
}

View File

@@ -83,7 +83,7 @@ export function useIsAmountPopulated() {
export function useSwapAmount(field: Field): [string | undefined, (amount: string) => void] {
const amount = useAtomValue(amountAtom)
const isFieldIndependent = useIsSwapFieldIndependent(field)
const value = useMemo(() => (isFieldIndependent ? amount : undefined), [amount, isFieldIndependent])
const value = isFieldIndependent ? amount : undefined
const updateSwap = useUpdateAtom(swapAtom)
const updateAmount = useCallback(
(amount: string) =>
@@ -101,8 +101,9 @@ export function useSwapCurrencyAmount(field: Field): CurrencyAmount<Currency> |
const isAmountPopulated = useIsAmountPopulated()
const [swapAmount] = useSwapAmount(field)
const [swapCurrency] = useSwapCurrency(field)
const currencyAmount = useMemo(() => tryParseCurrencyAmount(swapAmount, swapCurrency), [swapAmount, swapCurrency])
if (isFieldIndependent && isAmountPopulated) {
return tryParseCurrencyAmount(swapAmount, swapCurrency)
return currencyAmount
}
return
}

View File

@@ -4,7 +4,9 @@ import { Pair, Route as V2Route, Trade as V2Trade } from '@uniswap/v2-sdk'
import { Pool, Route as V3Route, Trade as V3Trade } from '@uniswap/v3-sdk'
import { SWAP_ROUTER_ADDRESSES, V2_ROUTER_ADDRESS, V3_ROUTER_ADDRESS } from 'constants/addresses'
import useActiveWeb3React from 'hooks/useActiveWeb3React'
import { useMemo } from 'react'
import { useERC20PermitFromTrade, UseERC20PermitState } from 'hooks/useERC20Permit'
import useTransactionDeadline from 'lib/hooks/useTransactionDeadline'
import { useCallback, useMemo } from 'react'
import invariant from 'tiny-invariant'
import { getTxOptimizedSwapRouter, SwapRouterVersion } from 'utils/getTxOptimizedSwapRouter'
@@ -134,3 +136,78 @@ export function useSwapApprovalOptimizedTrade(
}
}, [trade, optimizedSwapRouter])
}
export enum ApproveOrPermitState {
REQUIRES_APPROVAL,
PENDING_APPROVAL,
REQUIRES_SIGNATURE,
PENDING_SIGNATURE,
APPROVED,
}
/**
* Returns all relevant statuses and callback functions for approvals.
* Considers both standard approval and ERC20 permit.
*/
export const useApproveOrPermit = (
trade:
| V2Trade<Currency, Currency, TradeType>
| V3Trade<Currency, Currency, TradeType>
| Trade<Currency, Currency, TradeType>
| undefined,
allowedSlippage: Percent,
useIsPendingApproval: (token?: Token, spender?: string) => boolean,
amount?: CurrencyAmount<Currency> // defaults to trade.maximumAmountIn(allowedSlippage)
) => {
const deadline = useTransactionDeadline()
// Check approvals on ERC20 contract based on amount.
const [approval, getApproval] = useSwapApproval(trade, allowedSlippage, useIsPendingApproval, amount)
// Check status of permit and whether token supports it.
const {
state: signatureState,
signatureData,
gatherPermitSignature,
} = useERC20PermitFromTrade(trade, allowedSlippage, deadline)
const notApproved = approval === ApprovalState.NOT_APPROVED && !(signatureState === UseERC20PermitState.SIGNED)
// If permit is supported, trigger a signature, if not create approval transaction.
const handleApproveOrPermit = useCallback(async () => {
if (signatureState === UseERC20PermitState.NOT_SIGNED && gatherPermitSignature) {
try {
return await gatherPermitSignature()
} catch (error) {
// Try to approve if gatherPermitSignature failed for any reason other than the user rejecting it.
if (error?.code !== 4001) {
return getApproval()
}
}
} else {
return getApproval()
}
}, [signatureState, gatherPermitSignature, getApproval])
const approvalState = useMemo(() => {
if (approval === ApprovalState.PENDING) {
return ApproveOrPermitState.PENDING_APPROVAL
}
if (signatureState === UseERC20PermitState.LOADING) {
return ApproveOrPermitState.PENDING_SIGNATURE
}
if (notApproved && Boolean(gatherPermitSignature)) {
return ApproveOrPermitState.REQUIRES_SIGNATURE
}
if (notApproved) {
return ApproveOrPermitState.REQUIRES_APPROVAL
}
return ApproveOrPermitState.APPROVED
}, [approval, gatherPermitSignature, notApproved, signatureState])
return {
approvalState,
signatureData,
handleApproveOrPermit,
}
}

View File

@@ -1,151 +1,104 @@
import { Trans } from '@lingui/macro'
import { Currency, CurrencyAmount, Percent, TradeType } from '@uniswap/sdk-core'
import { FeeOptions } from '@uniswap/v3-sdk'
import { atom } from 'jotai'
import { useAtomValue, useUpdateAtom } from 'jotai/utils'
import { useCurrencyBalances } from 'lib/hooks/useCurrencyBalance'
import { feeOptionsAtom, Field, swapAtom } from 'lib/state/swap'
import { Field, swapAtom } from 'lib/state/swap'
import tryParseCurrencyAmount from 'lib/utils/tryParseCurrencyAmount'
import { ReactNode, useEffect, useMemo } from 'react'
import { useEffect, useMemo } from 'react'
import { InterfaceTrade, TradeState } from 'state/routing/types'
import { isAddress } from '../../../utils'
import useActiveWeb3React from '../useActiveWeb3React'
import useAllowedSlippage from '../useAllowedSlippage'
import useSlippage, { Slippage } from '../useSlippage'
import useUSDCPriceImpact, { PriceImpact } from '../useUSDCPriceImpact'
import { useBestTrade } from './useBestTrade'
import useWrapCallback, { WrapType } from './useWrapCallback'
interface SwapField {
currency?: Currency
amount?: CurrencyAmount<Currency>
balance?: CurrencyAmount<Currency>
usdc?: CurrencyAmount<Currency>
}
interface SwapInfo {
currencies: { [field in Field]?: Currency }
currencyBalances: { [field in Field]?: CurrencyAmount<Currency> }
currencyAmounts: { [field in Field]?: CurrencyAmount<Currency> }
[Field.INPUT]: SwapField
[Field.OUTPUT]: SwapField
trade: {
trade?: InterfaceTrade<Currency, Currency, TradeType>
state: TradeState
}
allowedSlippage: Percent
feeOptions: FeeOptions | undefined
}
const BAD_RECIPIENT_ADDRESSES: { [address: string]: true } = {
'0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f': true, // v2 factory
'0xf164fC0Ec4E93095b804a4795bBe1e041497b92a': true, // v2 router 01
'0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D': true, // v2 router 02
slippage: Slippage
impact: PriceImpact
}
// from the current swap inputs, compute the best trade and return it.
function useComputeSwapInfo(): SwapInfo {
const { account } = useActiveWeb3React()
const { type: wrapType } = useWrapCallback()
const isWrapping = wrapType === WrapType.WRAP || wrapType === WrapType.UNWRAP
const { independentField, amount, [Field.INPUT]: currencyIn, [Field.OUTPUT]: currencyOut } = useAtomValue(swapAtom)
const isExactIn = independentField === Field.INPUT
const {
independentField,
amount,
[Field.INPUT]: inputCurrency,
[Field.OUTPUT]: outputCurrency,
} = useAtomValue(swapAtom)
const feeOptions = useAtomValue(feeOptionsAtom)
const to = account
const relevantTokenBalances = useCurrencyBalances(
account,
useMemo(() => [inputCurrency ?? undefined, outputCurrency ?? undefined], [inputCurrency, outputCurrency])
)
const isExactIn: boolean = independentField === Field.INPUT
const parsedAmount = useMemo(
() => tryParseCurrencyAmount(amount, (isExactIn ? inputCurrency : outputCurrency) ?? undefined),
[inputCurrency, isExactIn, outputCurrency, amount]
() => tryParseCurrencyAmount(amount, (isExactIn ? currencyIn : currencyOut) ?? undefined),
[amount, isExactIn, currencyIn, currencyOut]
)
//@TODO(ianlapham): this would eventually be replaced with routing api logic.
const trade = useBestTrade(
isExactIn ? TradeType.EXACT_INPUT : TradeType.EXACT_OUTPUT,
parsedAmount,
(isExactIn ? outputCurrency : inputCurrency) ?? undefined
(isExactIn ? currencyOut : currencyIn) ?? undefined
)
const currencies = useMemo(
() => ({
[Field.INPUT]: inputCurrency ?? undefined,
[Field.OUTPUT]: outputCurrency ?? undefined,
}),
[inputCurrency, outputCurrency]
const amountIn = useMemo(
() => (isWrapping || isExactIn ? parsedAmount : trade.trade?.inputAmount),
[isExactIn, isWrapping, parsedAmount, trade.trade?.inputAmount]
)
const amountOut = useMemo(
() => (isWrapping || !isExactIn ? parsedAmount : trade.trade?.outputAmount),
[isExactIn, isWrapping, parsedAmount, trade.trade?.outputAmount]
)
const [balanceIn, balanceOut] = useCurrencyBalances(
account,
useMemo(() => [currencyIn, currencyOut], [currencyIn, currencyOut])
)
const currencyBalances = useMemo(
() => ({
[Field.INPUT]: relevantTokenBalances[0],
[Field.OUTPUT]: relevantTokenBalances[1],
}),
[relevantTokenBalances]
)
const currencyAmounts = useMemo(
() => ({
[Field.INPUT]: trade.trade?.inputAmount,
[Field.OUTPUT]: trade.trade?.outputAmount,
}),
[trade.trade?.inputAmount, trade.trade?.outputAmount]
)
const allowedSlippage = useAllowedSlippage(trade.trade)
const inputError = useMemo(() => {
let inputError: ReactNode | undefined
if (!account) {
inputError = <Trans>Connect Wallet</Trans>
}
if (!currencies[Field.INPUT] || !currencies[Field.OUTPUT]) {
inputError = inputError ?? <Trans>Select a token</Trans>
}
if (!parsedAmount) {
inputError = inputError ?? <Trans>Enter an amount</Trans>
}
const formattedTo = isAddress(to)
if (!to || !formattedTo) {
inputError = inputError ?? <Trans>Enter a recipient</Trans>
} else {
if (BAD_RECIPIENT_ADDRESSES[formattedTo]) {
inputError = inputError ?? <Trans>Invalid recipient</Trans>
}
}
// compare input balance to max input based on version
const [balanceIn, amountIn] = [currencyBalances[Field.INPUT], trade.trade?.maximumAmountIn(allowedSlippage)]
if (balanceIn && amountIn && balanceIn.lessThan(amountIn)) {
inputError = <Trans>Insufficient {amountIn.currency.symbol} balance</Trans>
}
return inputError
}, [account, allowedSlippage, currencies, currencyBalances, parsedAmount, to, trade.trade])
// Compute slippage and impact off of the trade so that it refreshes with the trade.
// (Using amountIn/amountOut would show (incorrect) intermediate values.)
const slippage = useSlippage(trade.trade)
const {
inputUSDC: usdcIn,
outputUSDC: usdcOut,
priceImpact: impact,
} = useUSDCPriceImpact(trade.trade?.inputAmount, trade.trade?.outputAmount)
return useMemo(
() => ({
currencies,
currencyBalances,
currencyAmounts,
inputError,
[Field.INPUT]: {
currency: currencyIn,
amount: amountIn,
balance: balanceIn,
usdc: usdcIn,
},
[Field.OUTPUT]: {
currency: currencyOut,
amount: amountOut,
balance: balanceOut,
usdc: usdcOut,
},
trade,
allowedSlippage,
feeOptions,
slippage,
impact,
}),
[currencies, currencyBalances, currencyAmounts, inputError, trade, allowedSlippage, feeOptions]
[amountIn, amountOut, balanceIn, balanceOut, currencyIn, currencyOut, impact, slippage, trade, usdcIn, usdcOut]
)
}
const swapInfoAtom = atom<SwapInfo>({
currencies: {},
currencyBalances: {},
currencyAmounts: {},
[Field.INPUT]: {},
[Field.OUTPUT]: {},
trade: { state: TradeState.INVALID },
allowedSlippage: new Percent(0),
feeOptions: undefined,
slippage: { auto: true, allowed: new Percent(0) },
impact: {},
})
export function SwapInfoUpdater() {

View File

@@ -4,12 +4,12 @@ import { useUpdateAtom } from 'jotai/utils'
import { feeOptionsAtom } from 'lib/state/swap'
import { useEffect } from 'react'
interface FeeOptionsArgs {
export interface FeeOptions {
convenienceFee?: number
convenienceFeeRecipient?: string | string | { [chainId: number]: string }
}
export default function useSyncConvenienceFee({ convenienceFee, convenienceFeeRecipient }: FeeOptionsArgs) {
export default function useSyncConvenienceFee({ convenienceFee, convenienceFeeRecipient }: FeeOptions) {
const { chainId } = useActiveWeb3React()
const updateFeeOptions = useUpdateAtom(feeOptionsAtom)

View File

@@ -1,70 +0,0 @@
import { Currency } from '@uniswap/sdk-core'
import { nativeOnChain } from 'constants/tokens'
import { useUpdateAtom } from 'jotai/utils'
import { DefaultAddress } from 'lib/components/Swap'
import useActiveWeb3React from 'lib/hooks/useActiveWeb3React'
import { useToken } from 'lib/hooks/useCurrency'
import { Field, Swap, swapAtom } from 'lib/state/swap'
import { useCallback, useLayoutEffect, useState } from 'react'
function useDefaultToken(
defaultAddress: DefaultAddress | undefined,
chainId: number | undefined
): Currency | null | undefined {
let address = undefined
if (typeof defaultAddress === 'string') {
address = defaultAddress
} else if (typeof defaultAddress === 'object' && chainId) {
address = defaultAddress[chainId]
}
const token = useToken(address)
if (chainId && address === 'NATIVE') {
return nativeOnChain(chainId)
}
return token
}
interface UseSwapDefaultsArgs {
defaultInputAddress?: DefaultAddress
defaultInputAmount?: string
defaultOutputAddress?: DefaultAddress
defaultOutputAmount?: string
}
export default function useSyncSwapDefaults({
defaultInputAddress,
defaultInputAmount,
defaultOutputAddress,
defaultOutputAmount,
}: UseSwapDefaultsArgs) {
const updateSwap = useUpdateAtom(swapAtom)
const { chainId } = useActiveWeb3React()
const defaultInputToken = useDefaultToken(defaultInputAddress, chainId)
const defaultOutputToken = useDefaultToken(defaultOutputAddress, chainId)
const setToDefaults = useCallback(() => {
const defaultSwapState: Swap = {
amount: '',
[Field.INPUT]: defaultInputToken || undefined,
[Field.OUTPUT]: defaultOutputToken || undefined,
independentField: Field.INPUT,
}
if (defaultInputAmount && defaultInputToken) {
defaultSwapState.amount = defaultInputAmount
} else if (defaultOutputAmount && defaultOutputToken) {
defaultSwapState.independentField = Field.OUTPUT
defaultSwapState.amount = defaultOutputAmount
}
updateSwap((swap) => ({ ...swap, ...defaultSwapState }))
}, [defaultInputToken, defaultOutputToken, defaultInputAmount, defaultOutputAmount, updateSwap])
const [previousChainId, setPreviousChainId] = useState(chainId)
useLayoutEffect(() => {
setPreviousChainId(chainId)
}, [chainId])
useLayoutEffect(() => {
if (chainId && chainId !== previousChainId) {
setToDefaults()
}
}, [chainId, previousChainId, setToDefaults])
}

View File

@@ -0,0 +1,83 @@
import { Currency } from '@uniswap/sdk-core'
import { nativeOnChain } from 'constants/tokens'
import { useUpdateAtom } from 'jotai/utils'
import useActiveWeb3React from 'lib/hooks/useActiveWeb3React'
import { useToken } from 'lib/hooks/useCurrency'
import useNativeCurrency from 'lib/hooks/useNativeCurrency'
import { Field, Swap, swapAtom } from 'lib/state/swap'
import { useCallback, useLayoutEffect, useState } from 'react'
import useOnSupportedNetwork from '../useOnSupportedNetwork'
export type DefaultAddress = string | { [chainId: number]: string | 'NATIVE' } | 'NATIVE'
export interface TokenDefaults {
defaultInputTokenAddress?: DefaultAddress
defaultInputAmount?: number | string
defaultOutputTokenAddress?: DefaultAddress
defaultOutputAmount?: number | string
}
function useDefaultToken(
defaultAddress: DefaultAddress | undefined,
chainId: number | undefined
): Currency | undefined {
let address = undefined
if (typeof defaultAddress === 'string') {
address = defaultAddress
} else if (typeof defaultAddress === 'object' && chainId) {
address = defaultAddress[chainId]
}
const token = useToken(address)
const onSupportedNetwork = useOnSupportedNetwork()
// Only use native currency if chain ID is in supported chains. ExtendedEther will error otherwise.
if (chainId && address === 'NATIVE' && onSupportedNetwork) {
return nativeOnChain(chainId)
}
return token ?? undefined
}
export default function useSyncTokenDefaults({
defaultInputTokenAddress,
defaultInputAmount,
defaultOutputTokenAddress,
defaultOutputAmount,
}: TokenDefaults) {
const updateSwap = useUpdateAtom(swapAtom)
const { chainId } = useActiveWeb3React()
const onSupportedNetwork = useOnSupportedNetwork()
const nativeCurrency = useNativeCurrency()
const defaultOutputToken = useDefaultToken(defaultOutputTokenAddress, chainId)
const defaultInputToken =
useDefaultToken(defaultInputTokenAddress, chainId) ??
// Default the input token to the native currency if it is not the output token.
(defaultOutputToken !== nativeCurrency && onSupportedNetwork ? nativeCurrency : undefined)
const setToDefaults = useCallback(() => {
const defaultSwapState: Swap = {
amount: '',
[Field.INPUT]: defaultInputToken,
[Field.OUTPUT]: defaultOutputToken,
independentField: Field.INPUT,
}
if (defaultInputToken && defaultInputAmount) {
defaultSwapState.amount = defaultInputAmount.toString()
} else if (defaultOutputToken && defaultOutputAmount) {
defaultSwapState.independentField = Field.OUTPUT
defaultSwapState.amount = defaultOutputAmount.toString()
}
updateSwap((swap) => ({ ...swap, ...defaultSwapState }))
}, [defaultInputAmount, defaultInputToken, defaultOutputAmount, defaultOutputToken, updateSwap])
const [previousChainId, setPreviousChainId] = useState(chainId)
useLayoutEffect(() => {
setPreviousChainId(chainId)
}, [chainId])
useLayoutEffect(() => {
if (chainId && chainId !== previousChainId) {
setToDefaults()
}
}, [chainId, previousChainId, setToDefaults])
}

View File

@@ -0,0 +1,133 @@
import { ContractTransaction } from '@ethersproject/contracts'
import { useWETHContract } from 'hooks/useContract'
import { atom, useAtom } from 'jotai'
import { useAtomValue } from 'jotai/utils'
import { Field, swapAtom } from 'lib/state/swap'
import tryParseCurrencyAmount from 'lib/utils/tryParseCurrencyAmount'
import { useCallback, useEffect, useMemo } from 'react'
import { WRAPPED_NATIVE_CURRENCY } from '../../../constants/tokens'
import useActiveWeb3React from '../useActiveWeb3React'
import { useCurrencyBalances } from '../useCurrencyBalance'
export enum WrapType {
NOT_APPLICABLE,
WRAP,
UNWRAP,
}
interface UseWrapCallbackReturns {
callback: () => Promise<ContractTransaction>
error: WrapError
loading: boolean
type: WrapType
}
export enum WrapError {
NO_ERROR = 0, // must be equal to 0 so all other errors are truthy
ENTER_NATIVE_AMOUNT,
ENTER_WRAPPED_AMOUNT,
INSUFFICIENT_NATIVE_BALANCE,
INSUFFICIENT_WRAPPED_BALANCE,
}
interface WrapState {
loading: boolean
error: WrapError
}
const wrapState = atom<WrapState>({
loading: false,
error: WrapError.NO_ERROR,
})
export default function useWrapCallback(): UseWrapCallbackReturns {
const { account, chainId } = useActiveWeb3React()
const [{ loading, error }, setWrapState] = useAtom(wrapState)
const wrappedNativeCurrencyContract = useWETHContract()
const { amount, [Field.INPUT]: inputCurrency, [Field.OUTPUT]: outputCurrency } = useAtomValue(swapAtom)
const wrapType = useMemo(() => {
if (!inputCurrency || !outputCurrency || !chainId) {
return WrapType.NOT_APPLICABLE
}
if (inputCurrency.isNative && WRAPPED_NATIVE_CURRENCY[chainId]?.equals(outputCurrency)) {
return WrapType.WRAP
}
if (WRAPPED_NATIVE_CURRENCY[chainId]?.equals(inputCurrency) && outputCurrency.isNative) {
return WrapType.UNWRAP
}
return WrapType.NOT_APPLICABLE
}, [chainId, inputCurrency, outputCurrency])
const parsedAmountIn = useMemo(
() => tryParseCurrencyAmount(amount, inputCurrency ?? undefined),
[inputCurrency, amount]
)
const relevantTokenBalances = useCurrencyBalances(
account,
useMemo(() => [inputCurrency ?? undefined, outputCurrency ?? undefined], [inputCurrency, outputCurrency])
)
const currencyBalances = useMemo(
() => ({
[Field.INPUT]: relevantTokenBalances[0],
[Field.OUTPUT]: relevantTokenBalances[1],
}),
[relevantTokenBalances]
)
const hasInputAmount = Boolean(parsedAmountIn?.greaterThan('0'))
const sufficientBalance = parsedAmountIn && !currencyBalances[Field.INPUT]?.lessThan(parsedAmountIn)
useEffect(() => {
if (sufficientBalance) {
setWrapState((state) => ({ ...state, error: WrapError.NO_ERROR }))
} else if (wrapType === WrapType.WRAP) {
setWrapState((state) => ({
...state,
error: hasInputAmount ? WrapError.INSUFFICIENT_NATIVE_BALANCE : WrapError.ENTER_NATIVE_AMOUNT,
}))
} else if (wrapType === WrapType.UNWRAP) {
setWrapState((state) => ({
...state,
error: hasInputAmount ? WrapError.INSUFFICIENT_WRAPPED_BALANCE : WrapError.ENTER_WRAPPED_AMOUNT,
}))
}
}, [hasInputAmount, setWrapState, sufficientBalance, wrapType])
const callback = useCallback(async () => {
if (!parsedAmountIn) {
return Promise.reject('Must provide an input amount to wrap.')
}
if (wrapType === WrapType.NOT_APPLICABLE) {
return Promise.reject('Wrapping not applicable to this asset.')
}
if (!sufficientBalance) {
return Promise.reject('Insufficient balance to wrap desired amount.')
}
if (!wrappedNativeCurrencyContract) {
return Promise.reject('Wrap contract not found.')
}
setWrapState((state) => ({ ...state, loading: true }))
const result = await (wrapType === WrapType.WRAP
? wrappedNativeCurrencyContract.deposit({ value: `0x${parsedAmountIn.quotient.toString(16)}` })
: wrappedNativeCurrencyContract.withdraw(`0x${parsedAmountIn.quotient.toString(16)}`)
).catch((e: unknown) => {
setWrapState((state) => ({ ...state, loading: false }))
throw e
})
// resolve loading state after one confirmation
result.wait(1).finally(() => setWrapState((state) => ({ ...state, loading: false })))
return Promise.resolve(result)
}, [wrappedNativeCurrencyContract, sufficientBalance, parsedAmountIn, wrapType, setWrapState])
return useMemo(
() => ({
callback,
error,
loading,
type: wrapType,
}),
[callback, error, loading, wrapType]
)
}

View File

@@ -1,19 +0,0 @@
import { Web3ReactHooks } from '@web3-react/core'
import { useAtomValue } from 'jotai/utils'
import { injectedAtom, urlAtom, Web3ReactState } from 'lib/state/web3'
export function useActiveWeb3ReactState(): Web3ReactState {
const injected = useAtomValue(injectedAtom)
const url = useAtomValue(urlAtom)
return injected[1].useIsActive() ? injected : url
}
export function useActiveWeb3ReactHooks(): Web3ReactHooks {
const [, hooks] = useActiveWeb3ReactState()
return hooks
}
export default function useActiveWeb3React() {
const { useProvider, useWeb3React } = useActiveWeb3ReactHooks()
return useWeb3React(useProvider())
}

View File

@@ -0,0 +1,63 @@
import { getPriorityConnector, initializeConnector, Web3ReactHooks } from '@web3-react/core'
import { EIP1193 } from '@web3-react/eip1193'
import { EMPTY } from '@web3-react/empty'
import { Actions, Connector, Provider as Eip1193Provider } from '@web3-react/types'
import { Url } from '@web3-react/url'
import { useAtom, WritableAtom } from 'jotai'
import { useAtomValue } from 'jotai/utils'
import { atomWithDefault, RESET, useUpdateAtom } from 'jotai/utils'
import { PropsWithChildren, useEffect } from 'react'
const [connector, hooks] = initializeConnector(() => EMPTY)
const EMPTY_CONNECTOR: [Connector, Web3ReactHooks] = [connector, hooks]
const urlConnectorAtom = atomWithDefault<[Connector, Web3ReactHooks]>(() => EMPTY_CONNECTOR)
const injectedConnectorAtom = atomWithDefault<[Connector, Web3ReactHooks]>(() => EMPTY_CONNECTOR)
const web3Atom = atomWithDefault<ReturnType<typeof hooks.useWeb3React>>(() => ({
connector: EMPTY_CONNECTOR[0],
library: undefined,
chainId: undefined,
account: undefined,
active: false,
error: undefined,
}))
export default function useActiveWeb3React() {
return useAtomValue(web3Atom)
}
function useConnector<T extends { new (actions: Actions, initializer: I): Connector }, I>(
connectorAtom: WritableAtom<[Connector, Web3ReactHooks], typeof RESET | [Connector, Web3ReactHooks]>,
Connector: T,
initializer: I | undefined
) {
const [connector, setConnector] = useAtom(connectorAtom)
useEffect(() => {
if (initializer) {
const [connector, hooks] = initializeConnector((actions) => new Connector(actions, initializer))
connector.activate()
setConnector([connector, hooks])
} else {
setConnector(RESET)
}
}, [Connector, initializer, setConnector])
return connector
}
interface Web3ProviderProps {
provider?: Eip1193Provider
jsonRpcEndpoint?: string
}
export function Web3Provider({ provider, jsonRpcEndpoint, children }: PropsWithChildren<Web3ProviderProps>) {
const injectedConnector = useConnector(injectedConnectorAtom, EIP1193, provider)
const urlConnector = useConnector(urlConnectorAtom, Url, jsonRpcEndpoint)
const priorityConnector = getPriorityConnector(injectedConnector, urlConnector)
const priorityProvider = priorityConnector.usePriorityProvider()
const priorityWeb3React = priorityConnector.usePriorityWeb3React(priorityProvider)
const setWeb3 = useUpdateAtom(web3Atom)
useEffect(() => {
setWeb3(priorityWeb3React)
}, [priorityWeb3React, setWeb3])
return <>{children}</>
}

View File

@@ -1,27 +0,0 @@
import { Currency, Percent, TradeType } from '@uniswap/sdk-core'
import useAutoSlippageTolerance from 'hooks/useAutoSlippageTolerance'
import { useAtomValue } from 'jotai/utils'
import { autoSlippageAtom, maxSlippageAtom } from 'lib/state/settings'
import { InterfaceTrade } from 'state/routing/types'
export function toPercent(maxSlippage: number | undefined): Percent | undefined {
if (!maxSlippage) return undefined
const numerator = Math.floor(maxSlippage * 100)
return new Percent(numerator, 10_000)
}
/** Returns the user-inputted max slippage. */
export default function useAllowedSlippage(trade: InterfaceTrade<Currency, Currency, TradeType> | undefined): Percent {
const autoSlippage = useAutoSlippageTolerance(trade)
const maxSlippage = toPercent(useAtomValue(maxSlippageAtom))
return useAtomValue(autoSlippageAtom) ? autoSlippage : maxSlippage ?? autoSlippage
}
export const MAX_VALID_SLIPPAGE = new Percent(1, 2)
export const MIN_HIGH_SLIPPAGE = new Percent(1, 100)
export function getSlippageWarning(slippage?: Percent): 'warning' | 'error' | undefined {
if (slippage?.greaterThan(MAX_VALID_SLIPPAGE)) return 'error'
if (slippage?.greaterThan(MIN_HIGH_SLIPPAGE)) return 'warning'
return
}

View File

@@ -25,7 +25,8 @@ function useBlock() {
useEffect(() => {
if (library && chainId && windowVisible) {
setState({ chainId })
// If chainId hasn't changed, don't clear the block. This prevents re-fetching still valid data.
setState((state) => (state.chainId === chainId ? state : { chainId }))
library
.getBlockNumber()

View File

@@ -7,9 +7,10 @@ import { NEVER_RELOAD, useSingleCallResult } from 'lib/hooks/multicall'
import useNativeCurrency from 'lib/hooks/useNativeCurrency'
import { useMemo } from 'react'
import { TOKEN_SHORTHANDS } from '../../constants/tokens'
import { isAddress } from '../../utils'
import { useTokenMap } from './useTokenList'
import { TokenMap } from './useTokenList'
import { supportedChainId } from '../../utils/supportedChainId'
import { TokenMap, useTokenMap } from './useTokenList'
// parse a name or symbol from a token response
const BYTES32_REGEX = /^0x[a-fA-F0-9]{64}$/
@@ -28,35 +29,27 @@ function parseStringOrBytes32(str: string | undefined, bytes32: string | undefin
* Returns null if token is loading or null was passed.
* Returns undefined if tokenAddress is invalid or token does not exist.
*/
export function useTokenFromMap(tokens: TokenMap, tokenAddress?: string | null): Token | null | undefined {
export function useTokenFromNetwork(tokenAddress: string | null | undefined): Token | null | undefined {
const { chainId } = useActiveWeb3React()
const address = isAddress(tokenAddress)
const formattedAddress = 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 tokenContract = useTokenContract(formattedAddress ? formattedAddress : undefined, false)
const tokenContractBytes32 = useBytes32TokenContract(formattedAddress ? formattedAddress : undefined, false)
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)
const tokenName = useSingleCallResult(tokenContract, 'name', undefined, NEVER_RELOAD)
const tokenNameBytes32 = useSingleCallResult(tokenContractBytes32, 'name', undefined, NEVER_RELOAD)
const symbol = useSingleCallResult(tokenContract, 'symbol', undefined, NEVER_RELOAD)
const symbolBytes32 = useSingleCallResult(tokenContractBytes32, 'symbol', undefined, NEVER_RELOAD)
const decimals = useSingleCallResult(tokenContract, 'decimals', undefined, NEVER_RELOAD)
return useMemo(() => {
if (token) return token
if (tokenAddress === null) return null
if (!chainId || !address) return undefined
if (typeof tokenAddress !== 'string' || !chainId || !formattedAddress) return undefined
if (decimals.loading || symbol.loading || tokenName.loading) return null
if (decimals.result) {
return new Token(
chainId,
address,
formattedAddress,
decimals.result[0],
parseStringOrBytes32(symbol.result?.[0], symbolBytes32.result?.[0], 'UNKNOWN'),
parseStringOrBytes32(tokenName.result?.[0], tokenNameBytes32.result?.[0], 'Unknown Token')
@@ -64,14 +57,13 @@ export function useTokenFromMap(tokens: TokenMap, tokenAddress?: string | null):
}
return undefined
}, [
address,
formattedAddress,
chainId,
decimals.loading,
decimals.result,
symbol.loading,
symbol.result,
symbolBytes32.result,
token,
tokenAddress,
tokenName.loading,
tokenName.result,
@@ -79,6 +71,20 @@ export function useTokenFromMap(tokens: TokenMap, tokenAddress?: string | null):
])
}
/**
* Returns a Token from the tokenAddress.
* Returns null if token is loading or null was passed.
* Returns undefined if tokenAddress is invalid or token does not exist.
*/
export function useTokenFromMapOrNetwork(tokens: TokenMap, tokenAddress?: string | null): Token | null | undefined {
const address = isAddress(tokenAddress)
const token: Token | undefined = address ? tokens[address] : undefined
const tokenFromNetwork = useTokenFromNetwork(token ? undefined : address ? address : undefined)
return tokenFromNetwork ?? token
}
/**
* Returns a Token from the tokenAddress.
* Returns null if token is loading or null was passed.
@@ -86,7 +92,7 @@ export function useTokenFromMap(tokens: TokenMap, tokenAddress?: string | null):
*/
export function useToken(tokenAddress?: string | null): Token | null | undefined {
const tokens = useTokenMap()
return useTokenFromMap(tokens, tokenAddress)
return useTokenFromMapOrNetwork(tokens, tokenAddress)
}
/**
@@ -96,8 +102,14 @@ export function useToken(tokenAddress?: string | null): Token | null | undefined
*/
export function useCurrencyFromMap(tokens: TokenMap, currencyId?: string | null): Currency | null | undefined {
const nativeCurrency = useNativeCurrency()
const { chainId } = useActiveWeb3React()
const isNative = Boolean(nativeCurrency && currencyId?.toUpperCase() === 'ETH')
const token = useTokenFromMap(tokens, isNative ? undefined : currencyId)
const shorthandMatchAddress = useMemo(() => {
const chain = supportedChainId(chainId)
return chain && currencyId ? TOKEN_SHORTHANDS[currencyId.toUpperCase()]?.[chain] : undefined
}, [chainId, currencyId])
const token = useTokenFromMapOrNetwork(tokens, isNative ? undefined : shorthandMatchAddress ?? currencyId)
if (currencyId === null || currencyId === undefined) return currencyId

View File

@@ -1,6 +1,6 @@
import { Currency } from '@uniswap/sdk-core'
import { useTheme } from 'lib/theme'
import Vibrant from 'node-vibrant/lib/bundle'
import Vibrant from 'node-vibrant/lib/bundle.js'
import { useEffect, useLayoutEffect, useState } from 'react'
import useCurrencyLogoURIs from './useCurrencyLogoURIs'

View File

@@ -1,8 +1,7 @@
import { Provider as EthersProvider } from '@ethersproject/abstract-provider'
import { Signer as EthersSigner } from '@ethersproject/abstract-signer'
import { VoidSigner } from '@ethersproject/abstract-signer'
import { Eip1193Bridge as ExperimentalEip1193Bridge } from '@ethersproject/experimental'
import { JsonRpcProvider } from '@ethersproject/providers'
import { JsonRpcProvider, JsonRpcSigner } from '@ethersproject/providers'
import { Provider as Eip1193Provider } from '@web3-react/types'
import { ZERO_ADDRESS } from 'constants/misc'
import { useMemo } from 'react'
@@ -17,32 +16,48 @@ class Eip1193Bridge extends ExperimentalEip1193Bridge {
const result = await this.provider.getNetwork()
return '0x' + result.chainId.toString(16)
}
case 'eth_requestAccounts':
try {
return await super.send(method, params)
} catch (e) {
return this.send('eth_accounts')
}
case 'eth_sendTransaction': {
if (!this.signer) break
// TODO(zzmp): JsonRpcProvider filters from/gas fields from the params.
const req = JsonRpcProvider.hexlifyTransaction(params?.[0], { from: true, gas: true })
const tx = await this.signer.sendTransaction(req)
return tx.hash
}
default:
return super.send(method, params)
}
}
}
interface EthersSigningProvider extends EthersProvider {
getSigner(): JsonRpcSigner
}
export default function useEip1193Provider(
provider?: Eip1193Provider | EthersProvider | JsonRpcProvider | { provider: EthersProvider; signer: EthersSigner }
provider?: Eip1193Provider | EthersSigningProvider | EthersProvider
): Eip1193Provider | undefined {
return useMemo(() => {
if (provider) {
if (provider instanceof EthersProvider) {
// A JsonRpcProvider includes its own Signer, otherwise use a VoidSigner.
if (EthersProvider.isProvider(provider)) {
const signer = 'getSigner' in provider ? provider.getSigner() : null ?? voidSigner
return new Eip1193Bridge(signer, provider)
}
if ('provider' in provider && 'signer' in provider) {
return new Eip1193Bridge(provider.signer, provider.provider)
}
// See https://github.com/ethereum/EIPs/blob/master/EIPS/eip-1193.md.
if ('request' in provider && 'on' in provider && 'removeListener' in provider) {
return provider
} else if (EthersProvider.isProvider((provider as ExperimentalEip1193Bridge).provider)) {
/*
* Direct users to use our own wrapper to avoid any pitfalls:
* - Eip1193Bridge is experimental
* - signer is not straightforward
* - bugs out if chainId>8
*/
throw new Error('Eip1193Bridge is experimental: pass your ethers Provider directly')
}
}
return
return provider
}, [provider])
}

View File

@@ -1,7 +1,15 @@
import { useCallback, useEffect, useState } from 'react'
export default function useHasFocus(node: Node | null | undefined): boolean {
const [hasFocus, setHasFocus] = useState(node?.contains(document.activeElement) ?? false)
useEffect(() => {
if (node instanceof HTMLElement) {
// tabIndex is required to receive blur events from non-button elements.
node.tabIndex = node.tabIndex || -1
// Without explicitly omitting outline, Safari will now outline this node when focused.
node.style.outline = node.style.outline || 'none'
}
}, [node])
const [hasFocus, setHasFocus] = useState(node?.contains(document?.activeElement) ?? false)
const onFocus = useCallback(() => setHasFocus(true), [])
const onBlur = useCallback((e) => setHasFocus(node?.contains(e.relatedTarget) ?? false), [node])
useEffect(() => {

View File

@@ -0,0 +1,11 @@
import { ALL_SUPPORTED_CHAIN_IDS } from 'constants/chains'
import { useMemo } from 'react'
import useActiveWeb3React from './useActiveWeb3React'
function useOnSupportedNetwork() {
const { chainId } = useActiveWeb3React()
return useMemo(() => chainId && ALL_SUPPORTED_CHAIN_IDS.includes(chainId), [chainId])
}
export default useOnSupportedNetwork

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