Compare commits

...

107 Commits

Author SHA1 Message Date
Crowdin Bot
75ecc5810e chore(i18n): synchronize translations from crowdin [skip ci] 2022-02-10 09:06:53 +00:00
Crowdin Bot
c30eb89725 chore(i18n): synchronize translations from crowdin [skip ci] 2022-02-10 02:15:57 +00:00
Zach Pomerantz
108feace02 fix: enable max swap amount (#3278)
* fix: enable max swap amount

* fix: bad syntax

* fix: unnecessary optional
2022-02-09 17:54:27 -08:00
Zach Pomerantz
e2c013a4d8 chore: clean up old generated assets (#3276) 2022-02-09 17:31:41 -08:00
Zach Pomerantz
66308257d6 feat: connect wallet ux (#3275)
* fix: wallet styling

* feat: onConnectWallet prop
2022-02-09 17:27:26 -08:00
Zach Pomerantz
fd160531cc chore: add dts to .gitignore (#3273) 2022-02-09 15:52:31 -08:00
Zach Pomerantz
da36e638c2 fix: update max slippage state (#3268)
* fix: max slippage state

* chore: rename to useAllowedSlippage

* nit: maxSlippageInput name
2022-02-09 15:52:14 -08:00
Ian Lapham
fad55b8dbc update sig figs (#3270) 2022-02-09 15:44:15 -08:00
Zach Pomerantz
c9c59698de fix: tooltip overflow and cursor (#3271)
* fix: tooltip cursor

* fix: only clip dialog

* nit: clean up class name
2022-02-09 15:35:50 -08:00
Zach Pomerantz
828967031f fix: use greaterThan (#3269) 2022-02-09 14:23:53 -08:00
Tina
440ac0cba0 feat: track google analytics clientIds (#3264)
* store client id in localstorage

* remove newline

* use React.ga

* fix import
2022-02-09 12:01:01 -08:00
Jordan Frankfurt
b5a72cd63b fix(widgets): remove extra } in Trade caption (#3262) 2022-02-09 09:40:12 -06:00
Crowdin Bot
37f273aab4 chore(i18n): synchronize translations from crowdin [skip ci] 2022-02-09 03:06:30 +00:00
Zach Pomerantz
3acd993ec0 chore: bundling nits (#3258)
* refactor: mv governance contracts to governance

* refactor: mv merkle contract to claim

* refactor: mv staking contract to staking

* chore: 0.0.6-beta

* chore: add @reduxjs/toolkit to peerDeps

* chore: v0.0.7-beta

* chore: add @reduxjs/toolkit to deps

* chore: v0.0.8-beta

* chore: swap web3-react aliasing

* chore: v0.0.9-beta

* chore: v0.0.10-beta
2022-02-08 16:43:14 -08:00
Crowdin Bot
58778b5775 chore(i18n): synchronize translations from crowdin [skip ci] 2022-02-08 22:07:44 +00:00
Ian Lapham
5bc21bebc3 update summary details (#3254) 2022-02-08 13:33:31 -08:00
Crowdin Bot
c3d6727438 chore(i18n): synchronize translations from crowdin [skip ci] 2022-02-08 20:07:57 +00:00
Zach Pomerantz
290f4bc1cb feat: bundle widgets (#3244)
* fix: tsconfig emissions

* fix: avoid importing app state in lib

* fix: export theming

* fix: asset resolutions

* fix: bundle widget

* test: update failing snapshots

* fix: maintain i18n for cosmos

* fix: use npm-compatible aliasing

* fix: include fonts in bundle
2022-02-08 11:04:48 -08:00
Jordan Frankfurt
f95275d5ac feat(widgets): Localize CurrencyAmounts and Prices (#3247)
* add basic number formatting

* test formatLocaleNumber

* localize CurrencyAmounts and Prices

* use lingui locale hook

* pr review

* cleaner type assertions

* check if locale is supported when formatting

* pr feedback
2022-02-08 12:45:40 -06:00
Jordan Frankfurt
0ec2dd4173 fix(widgets): fix broken unsupported network message (#3256)
* fix(widgets): fix broken unsupported network message

* require that the user be on a Uniswap chainId AND a token-list supported chainID before fetching swap info

* use .some instead of .reduce
2022-02-08 12:45:30 -06:00
Ian Lapham
3b3db6f6d0 fix: update widget loading state detection (#3253)
* update loading state detection

* create custom hook for amount detection
2022-02-08 10:38:48 -08:00
Jordan Frankfurt
707abd0071 feat(widgets): ensure passed locale is supported (#3245)
* ensure passed locale is supported

* warn on locale mismatch

* export SUPPORTED_LOCALES
2022-02-07 19:57:26 -06:00
Jordan Frankfurt
2efc1fb372 fix(widgets): convert widget colors from hex to hsl (#3239)
* convert widget colors from hex to hsl

* nits
2022-02-07 17:15:24 -06:00
Jordan Frankfurt
55b37825f3 fix(widgets): white accentText color on some buttons (#3238)
* white accentText color on some buttons

* put color calculations in useMemo, change accentText name, make hsl hex

* onAccent -> onAccentText
2022-02-07 16:56:10 -06:00
Ian Lapham
bb27b7a2ef feat: widget loading animations polish (#3232)
* create use best trade hook for widgets

* update comment in hook file

* add loading states to input / output fields

* update to not use imports from app

* remove custom loading component

* update var name and syncing detection logic

* fix USD div type

* simplify loading css, small changes
2022-02-07 14:38:07 -08:00
Zach Pomerantz
c595ba951b fix: isolate infura (#3241)
* fix: rm infura urls from lib

* fix: use passed providers for client SOR

* fix: clean up supported chain ids

* nit: rename params with specificity

* fix: use public rpc urls for l2

* fix: special-case rpc urls
2022-02-07 10:12:45 -08:00
Zach Pomerantz
96a122d7b8 chore: rename web3-react-alpha (#3243)
Renames widgets-web3-react/* to @widgets/web3-react/*.
npm treats nested packages as scoped, and requires scoped packages to begin with @.
2022-02-07 08:50:01 -08:00
Zach Pomerantz
610f7d3581 fix: named imports (webpack 5 compat) (#3242)
* fix: avoid json named imports

This is required by webpack 5, and is done to keep the widgets library compatible.
See https://webpack.js.org/migrate/5/#using-named-exports-from-json-modules

Note that this must be done upstream as well, in @uniswap/v3-sdk and @uniswap/router-sdk.

* chore: bump v3-sdk to avoid json named imports
2022-02-07 08:49:48 -08:00
Zach Pomerantz
781e774ce7 fix: set dialog wrapper with callback (#3240) 2022-02-04 16:08:50 -08:00
Ian Lapham
2aa1e40481 feat: create use best trade hook for widgets (#3226)
* create use best trade hook for widgets

* update comment in hook file

* refactor loading state conditional

* update logic in use best trade

* clean code in best trade hook
2022-02-04 18:38:27 -05:00
Zach Pomerantz
1c278d5012 fix: close summary after confirmation (#3233) 2022-02-03 15:04:11 -08:00
Jordan Frankfurt
a323a5c48b feat(widgets): convenience fee (#3231)
* feat(widgets): support convenience fee in trades (#3219)

* feat(widgets): support convenience fee in trades

* update call signature

* pr feedback

* set default convenience fee to undefined

* pr feedback
2022-02-03 14:48:30 -06:00
Zach Pomerantz
43931dd689 feat: chain-specific ttls (#3228) 2022-02-03 11:30:50 -08:00
Zach Pomerantz
efa3d5529c fix: only show max where appropriate (#3229) 2022-02-03 11:30:24 -08:00
Zach Pomerantz
5c0246cfc6 feat: outline tooltips (#3230) 2022-02-03 11:30:05 -08:00
Jordan Frankfurt
ee32418ff8 Revert "feat(widgets): support convenience fee in trades (#3219)" (#3224)
This reverts commit 8064dd8ede.
2022-02-03 10:46:18 -06:00
Zach Pomerantz
6e22389791 fix: slippage and price impact ux (#3222) 2022-02-03 08:23:27 -08:00
Jordan Frankfurt
8064dd8ede feat(widgets): support convenience fee in trades (#3219)
* feat(widgets): support convenience fee in trades

* update call signature

* pr feedback
2022-02-03 09:38:42 -06:00
Crowdin Bot
921310ef52 chore(i18n): synchronize translations from crowdin [skip ci] 2022-02-03 08:06:38 +00:00
Ian Lapham
7b90fe137e update list component (#3221) 2022-02-02 17:52:38 -05:00
Ian Lapham
05b2711a8a feat: update widget with client side SOR (#3210)
* start SOR by creating custom widget hook

* update best trade hook to use SOR in widget

* update organization for client side SOR logic

* fix auto router chain id import

* remove dependency on react GA for widget

* update dependencies for SOr

* remove new useBestTrade.ts

* update loading logic for fetching hook

* update dependencies with import from ethersproject

* update import version

* add try catch on SOR usage

* code cleanup, nit fixes
2022-02-02 17:47:49 -05:00
Crowdin Bot
d060782242 chore(i18n): synchronize translations from crowdin [skip ci] 2022-02-02 22:07:03 +00:00
Zach Pomerantz
e19e8492c9 feat: ux warnings (#3220)
* chore: mv Toolbar to a directory

* refactor: clean up Toolbar

* refactor: simplify Toolbar Caption

* feat: warn on price impact in Summary

* refactor: add computeRealizedPriceImpact util
2022-02-02 13:55:36 -08:00
Ian Lapham
800b5e0bda fix: fix pricing displays (#3214)
* fix pricing displays

* update rate logic, code clean
2022-02-02 13:43:12 -05:00
Ian Lapham
fc637071f9 update deadline signature data (#3215) 2022-02-02 12:33:00 -05:00
Moody Salem
1b78ceec10 chore: lockfile update only from the walletlink connector update 2022-02-02 00:13:29 -05:00
Moody Salem
e5be3ebf8f chore: put back the integrity hashes that were removed by the walletlink change 2022-02-02 00:12:59 -05:00
Brendan Weinstein
1c73719766 fix: update walletlink-connector to 6.2.11 (#3213) 2022-02-02 00:10:01 -05:00
Crowdin Bot
14c91f9bba chore(i18n): synchronize translations from crowdin [skip ci] 2022-02-02 00:13:11 +00:00
Zach Pomerantz
4b762ef5c9 feat: slippage warning ux (#3211)
* feat: setting input spacings

* feat: popover icon props

* fix: slippage input border

* feat: slippage input warning ux

* feat: slippage summary warning ux

* fix: summary layout

* fix: large icon compatibility

* fix: input option style

* fix: large icon compatibility

* fix: popover dimensions

* feat: tooltip hook

* fix: better max slippage popovers

* feat: error color input on invalid slippage

* fix: use default tx ttl

* fix: type userDeadline
2022-02-01 15:03:55 -08:00
Zach Pomerantz
c82b4fae64 fix: branded footer nits (#3209)
* chore: export brand color

* fix: target only children for extracted color transitions

* fix: branded footer nits
2022-01-31 14:08:39 -06:00
Zach Pomerantz
ab8c1e3e90 fix: input/output value/balance styles (#3207)
* fix: right-align balance

* fix: set min-height on text
2022-01-31 10:46:10 -08:00
Ian Lapham
7055d60406 remove survey (#3206) 2022-01-31 13:17:51 -05:00
Ian Lapham
c641cec651 update button color (#3205) 2022-01-31 13:02:58 -05:00
Brendan Weinstein
b6a47c734f fix: support networks other than ethereum mainnet for walletlink/coinbase wallet (#3202) 2022-01-31 12:03:00 -05:00
Crowdin Bot
7aecf5d398 chore(i18n): synchronize translations from crowdin [skip ci] 2022-01-28 20:07:31 +00:00
Crowdin Bot
5bf2b81743 chore(i18n): synchronize translations from crowdin [skip ci] 2022-01-28 18:07:13 +00:00
Ian Lapham
ed247065a7 feat: format usd prices, add loading states (#3196)
* format usd prices, add loading states

* remove tildes, collapse details by default

* update swap deadline to use seconds

* update syntax for loading states
2022-01-28 12:59:23 -05:00
Crowdin Bot
0d0ad633fb chore(i18n): synchronize translations from crowdin [skip ci] 2022-01-28 17:10:31 +00:00
Jordan Frankfurt
4a8f1d9b96 feat(widgets): move uniswap protocol branding from header to bottom of output (#3194)
* move branding from header to bottom of output

* remove old prop

* BrandingFooter component

* pr feedback
2022-01-28 10:45:35 -06:00
Zach Pomerantz
043fb95d22 chore: no default color extraction (#3192) 2022-01-27 13:24:50 -08:00
Zach Pomerantz
06536bc925 chore: comment out routing tooltip (#3191) 2022-01-27 13:24:39 -08:00
Ian Lapham
a598a15799 feat: Make pending txn status functional (#3193)
* update swap hooks to add swap txn confirmations

* fix: remove uneeded comments

* update with latest

* update utils to separate swap callback hooks

* create generic swap callabck to be used by both app and widget

* update app swap callback to use logic from lib

* update big number import

* add swap txn to state on submit

* remove redundant  fields in txn interfaces

* consolidate trade type logic
2022-01-27 13:38:35 -05:00
Crowdin Bot
b0265c081e chore(i18n): synchronize translations from crowdin [skip ci] 2022-01-27 01:28:46 +00:00
Zach Pomerantz
47aff6ff74 feat: max slippage ui (#3190)
* style: input padding

* feat: expand Row grow

* style: polish max slippage
2022-01-26 16:44:21 -08:00
Zach Pomerantz
56717005e6 feat: pending tx state (#3189)
* refactor: state cleanup

* feat: add pending tx hash to swap state

* fix: update name to display tx hash
2022-01-26 16:39:10 -08:00
Ian Lapham
b50d10cbb2 feat: update swap hooks and add swap txn submission (#3187)
* update swap hooks to add swap txn confirmations

* fix: remove uneeded comments

* update with latest

* update utils to separate swap callback hooks

* create generic swap callabck to be used by both app and widget

* update app swap callback to use logic from lib

* update big number import
2022-01-26 19:21:10 -05:00
Jordan Frankfurt
ce96873a72 feat(widgets): use default input/output (#3161)
* feat: use default input/output on chain switch

* feat(widgets): ErrorGenerator -> PropValidator

* default prop validation

* useDefaults hook

* pr feedback

* fix cosmos

* drop token map changes

* add default inputs to cosmos fixture

* set up different validation layers for widget and swap

* split widget/swap prop types

* cleanup

* pr feedback

* clear defaults when they're no longer valid on the current chain

* remove state checks on validators

* stop using address in cosmos fixture

* pr feedback

* useMemo on useSwapDefaults args

* tell the user what they gave to error'd props

Co-authored-by: Zach Pomerantz <zzmp@uniswap.org>
2022-01-26 12:14:18 -06:00
Tina
779625a04e fix: chain parameter should be able to switch chains on initial load (#3180)
* fix switch network on load

* dont run useeffect when chainId isnt defined yet

* remove newline
2022-01-26 10:03:48 -08:00
Crowdin Bot
d1e0812684 chore(i18n): synchronize translations from crowdin [skip ci] 2022-01-26 03:53:51 +00:00
Zach Pomerantz
98e62b4f93 Revert "chore(i18n): synchronize translations from crowdin [skip ci]"
This reverts commit 9fb0d424c2.
2022-01-25 19:46:36 -08:00
Crowdin Bot
9fb0d424c2 chore(i18n): synchronize translations from crowdin [skip ci] 2022-01-26 01:31:09 +00:00
Zach Pomerantz
8d145b908e feat: pending approval ui (#3186)
* feat: track approval txs

* refactor: update transactions

* feat: pending approval ui

* chore: fix pending approval doc

* fix: clarify optimized trade

* fix: use relative path for data uri assets
2022-01-25 16:24:36 -08:00
Zach Pomerantz
c7633d910b refactor: track txs (#3185)
* feat: track approval txs

* refactor: update transactions

* chore: add ms to deps

* test: rm stale test

* fix: comment usage of trade for optimized trade
2022-01-25 18:55:27 -05:00
Crowdin Bot
1f89a46a3f chore(i18n): synchronize translations from crowdin [skip ci] 2022-01-25 20:07:16 +00:00
Crowdin Bot
8d54b01878 chore(i18n): synchronize translations from crowdin [skip ci] 2022-01-25 19:07:17 +00:00
Zach Pomerantz
ffe334ccbf feat: update summary view with real values (#3179)
* refactor: isolate approval callback hooks

* fix: use approval callback from trade

* chore: pass optimized trade to summary

* start review screen UI updates

* chore: pass optimized trade to summary

* fix: pass Trade to summary

* remove uneeded value type

* remove uneeded styling

* code cleanup

* code styling, update props

* fix fixture bug, code style updates

* bug fix in details array

* update logic in details

Co-authored-by: ianlapham <ianlapham@gmail.com>
2022-01-25 13:48:52 -05:00
Zach Pomerantz
ffe2bd315e fix: track swap approvals (#3183)
* fix: track swap approvals

* fix: type ambiguous return value
2022-01-24 17:52:45 -08:00
Zach Pomerantz
cee4b8c77a fix: disable swap button w/o account (#3177)
* fix: disable swap button w/o account

* nit: indent less
2022-01-24 15:28:48 -08:00
Tina
3153db9f73 feat: add chainId (network) as url parameter (#3057)
* read from query param and change networks if necessary

* dont open network selector menu on url param change

* prompt network change when url changes

* keep url, network in sync

* use chain name instead of id in url param

* only prompt network switch if url chain doesnt match
2022-01-24 15:23:34 -08:00
Ian Lapham
bbdb5f3f56 feat: update slippage tolerance to use auto or custom (#3166)
* update slippage tolerance to use auto or custom

* remove attempted styling for other PR

* back out UI changes, small naming updates

* remove UI work

* small code style changes, fix typo

* update comment to doc comment
2022-01-24 17:56:12 -05:00
Crowdin Bot
7f9c56b68c chore(i18n): synchronize translations from crowdin [skip ci] 2022-01-24 20:08:12 +00:00
Crowdin Bot
2b69974fdc chore(i18n): synchronize translations from crowdin [skip ci] 2022-01-24 19:06:44 +00:00
Zach Pomerantz
5236065769 refactor: isolate approval callback hooks (#3172)
* refactor: isolate approval callback hooks

* fix: use approval callback from trade
2022-01-24 10:56:24 -08:00
Justin Domingue
52128a2dcd chore: reset local tick data state on input change (#3176) 2022-01-24 11:10:33 -05:00
Justin Domingue
c9642c6cd0 feat: use TickLens on chains where subgraph is not functional (#3149) 2022-01-24 09:12:07 -05:00
Crowdin Bot
b878d764e5 chore(i18n): synchronize translations from crowdin [skip ci] 2022-01-23 18:09:33 +00:00
Will Hennessy
6a4f067ac0 Display message if not mainnet (#3151) 2022-01-23 12:31:40 -05:00
Crowdin Bot
e9407bb6bd chore(i18n): synchronize translations from crowdin [skip ci] 2022-01-22 15:07:19 +00:00
Crowdin Bot
8d822fd0e0 chore(i18n): synchronize translations from crowdin [skip ci] 2022-01-22 10:08:02 +00:00
Zach Pomerantz
6404ee6e0b fix: default tokens on chainId change only (#3169) 2022-01-21 13:14:50 -08:00
Crowdin Bot
8ac3ed1128 chore(i18n): synchronize translations from crowdin [skip ci] 2022-01-21 10:07:23 +00:00
Zach Pomerantz
b501974a76 feat: polish select (#3160)
* feat: filter selected currency from select

* test: use infura urls

* fix: load native with chain

* fix: use currencyId for key

* feat: switch currencies when selecting other

* fix: resolve merge conflict name
2022-01-20 16:15:23 -08:00
Zach Pomerantz
567fb0181c fix: chain mismatched currencies (#3163) 2022-01-20 16:12:42 -08:00
Crowdin Bot
8a37c427e6 chore(i18n): synchronize translations from crowdin [skip ci] 2022-01-21 00:11:38 +00:00
Ian Lapham
034b3e3e58 feat: Update swap state structure and attach to UI (#3155)
* refactor: mv settings state to own file

* chore: add default exports

* refactor: update swap state to match biz logic

* feat: copy biz logic to widgets

* Hook up UI to updated swap state

* fix: decimal inputs

* fix max slippage

* fix error in settings

* fix: typing errors

* revert: useBestTrade changes

* fix: use client side trade for widgets

* fix: exhaustive deps

* chore: add router-sdk

* fix: gate old web3 on widget env

* fix building errors

* update trade imports

* update hook naming for swap amount and currencies

* small changes

Co-authored-by: Zach Pomerantz <zzmp@uniswap.org>
2022-01-20 18:51:45 -05:00
Ian Lapham
053000e5fc fix: update address listt (#3159) 2022-01-20 14:50:27 -05:00
Crowdin Bot
b77e7deb49 chore(i18n): synchronize translations from crowdin [skip ci] 2022-01-20 03:06:53 +00:00
Crowdin Bot
c3321ae793 chore(i18n): synchronize translations from crowdin [skip ci] 2022-01-19 22:06:45 +00:00
Jordan Frankfurt
5dec0cf72b show wrong chain message instead of throwing on incorrect chain connection (#3153) 2022-01-19 15:52:41 -06:00
Zach Pomerantz
1efda07e7a refactor: mv try parse currency amount to lib utils (#3152) 2022-01-19 14:34:01 -05:00
Crowdin Bot
fd819260f9 chore(i18n): synchronize translations from crowdin [skip ci] 2022-01-19 16:11:52 +00:00
Jordan Frankfurt
8e3b2cb4b8 feat(widgets): add error reporting component, INTEGRATION ERROR type, and Missing provider error (#3110)
* add error reporting component, INTEGRATION ERROR type, and Missing provider error

* rename reporter to generator

* pr feedback

* refactor provider check

* add chainId, convenienceFee, and width errors

* pr feedback and convenienceFeeRecipient address enforcement

* fix imports for utils
2022-01-19 09:38:21 -06:00
Zach Pomerantz
d54783a324 fix: memoize more swap (#2950)
* fix: memoize derived swap info

* fix: memoize current block timestamp

* fix: memoize price impact

* fix: memoize debounced value updates

* fix: nits
2022-01-18 18:40:23 -05:00
Zach Pomerantz
850a20f6ad feat: include native currency in widget select (#3124)
* fix: token image for chains / natives

* feat: include native currency in select

- Updates widgets swap state to use Currency (and deals with downstream updates)
- Refactors logoURI code to a new lib/hooks/useCurrencyLogoURIs
- Adds native currency to useQueryTokenList

NB: This does not build because tests must be updated to use Currency (they currently use mock tokens)

* test: update fixtures to use real currency

* fix: data uri color extraction

* fix: token img state

* fix: use new array
2022-01-18 12:11:22 -08:00
Zach Pomerantz
99f681818f refactor: mv token hooks to lib (#3122)
* refactor: mv useNativeCurrency to lib/hooks

* refactor: mv useCurrency logic to lib/hooks
2022-01-14 11:30:04 -08:00
Crowdin Bot
1127e74357 chore(i18n): synchronize translations from crowdin [skip ci] 2022-01-14 19:07:20 +00:00
224 changed files with 7495 additions and 5112 deletions

5
.gitignore vendored
View File

@@ -17,11 +17,10 @@
# testing
/coverage
# production
# builds
/build
# widgets
/dist
/dts
# misc
.DS_Store

View File

@@ -1,14 +1,26 @@
// eslint-disable-next-line @typescript-eslint/no-var-requires
/* eslint-disable @typescript-eslint/no-var-requires */
const HtmlWebpackPlugin = require('html-webpack-plugin')
const { DefinePlugin } = require('webpack')
// Renders the cosmos fixtures in isolation, instead of using public/index.html.
module.exports = (webpackConfig) => ({
...webpackConfig,
plugins: webpackConfig.plugins.map((plugin) =>
plugin instanceof HtmlWebpackPlugin
? new HtmlWebpackPlugin({
templateContent: '<body></body>',
})
: plugin
),
plugins: webpackConfig.plugins.map((plugin) => {
if (plugin instanceof HtmlWebpackPlugin) {
return new HtmlWebpackPlugin({
templateContent: '<body></body>',
})
}
if (plugin instanceof DefinePlugin) {
return new DefinePlugin({
...plugin.definitions,
'process.env': {
...plugin.definitions['process.env'],
REACT_APP_IS_WIDGET: true,
REACT_APP_LOCALES: "'../locales'",
},
})
}
return plugin
}),
})

View File

@@ -1,16 +1,20 @@
{
"name": "@uniswap/interface",
"name": "@uniswap/widgets",
"version": "0.0.10-beta",
"description": "Uniswap Interface",
"homepage": ".",
"main": "dist/interface.js",
"module": "dist/interface.esm.js",
"types": "dist/index.d.ts",
"main": "dist/widgets.js",
"module": "dist/widgets.esm.js",
"types": "dist/widgets.d.ts",
"files": [
"lib",
"dist"
],
"private": true,
"devDependencies": {
"@babel/plugin-transform-runtime": "^7.17.0",
"@babel/preset-env": "^7.16.11",
"@babel/preset-react": "^7.16.7",
"@babel/preset-typescript": "^7.16.7",
"@ethersproject/experimental": "^5.4.0",
"@gnosis.pm/safe-apps-web3-react": "^0.6.0",
"@graphql-codegen/cli": "1.21.5",
@@ -22,10 +26,14 @@
"@reach/dialog": "^0.10.3",
"@reach/portal": "^0.10.3",
"@react-hook/window-scroll": "^1.3.0",
"@reduxjs/toolkit": "^1.6.1",
"@rollup/plugin-alias": "^3.1.9",
"@rollup/plugin-babel": "^5.3.0",
"@rollup/plugin-commonjs": "^21.0.1",
"@rollup/plugin-eslint": "^8.0.1",
"@rollup/plugin-json": "^4.1.0",
"@rollup/plugin-node-resolve": "^13.1.3",
"@rollup/plugin-replace": "^3.0.1",
"@rollup/plugin-typescript": "^8.3.0",
"@rollup/plugin-url": "^6.1.0",
"@svgr/rollup": "^6.2.0",
"@testing-library/jest-dom": "^5.14.1",
@@ -56,22 +64,24 @@
"@types/wcag-contrast": "^3.0.0",
"@typescript-eslint/eslint-plugin": "^4.1.0",
"@typescript-eslint/parser": "^4.1.0",
"@uniswap/default-token-list": "^3.0.0",
"@uniswap/governance": "^1.0.2",
"@uniswap/liquidity-staker": "^1.0.2",
"@uniswap/merkle-distributor": "1.0.1",
"@uniswap/router-sdk": "^1.0.3",
"@uniswap/smart-order-router": "^2.5.10",
"@uniswap/v2-core": "1.0.0",
"@uniswap/v2-periphery": "^1.1.0-beta.0",
"@uniswap/v3-core": "1.0.0",
"@uniswap/v3-periphery": "^1.1.1",
"@web3-react/fortmatic-connector": "^6.0.9",
"@web3-react/injected-connector": "^6.0.7",
"@web3-react/portis-connector": "^6.0.9",
"@web3-react/walletconnect-connector": "^7.0.2-alpha.0",
"@web3-react/walletlink-connector": "^6.2.8",
"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",
"array.prototype.flat": "^1.2.4",
"array.prototype.flatmap": "^1.2.4",
"babel-plugin-macros": "^3.1.0",
"copy-to-clipboard": "^3.2.0",
"cross-env": "^7.0.3",
"cypress": "^7.7.0",
@@ -89,12 +99,13 @@
"graphql-request": "^3.4.0",
"inter-ui": "^3.13.1",
"jest-styled-components": "^7.0.5",
"ms.macro": "^2.0.0",
"polyfill-object.fromentries": "^1.0.1",
"prettier": "^2.2.1",
"qs": "^6.9.4",
"react": "^17.0.1",
"react-confetti": "^6.0.0",
"react-cosmos": "^5.6.3",
"react-cosmos": "^5.6.6",
"react-dom": "^17.0.1",
"react-ga": "^2.5.7",
"react-is": "^17.0.2",
"react-markdown": "^4.3.1",
@@ -104,14 +115,17 @@
"react-use-gesture": "^6.0.14",
"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-node-externals": "^3.1.2",
"rollup-plugin-scss": "^3.0.0",
"rollup-plugin-typescript2": "^0.31.1",
"sass": "^1.45.1",
"serve": "^11.3.2",
"start-server-and-test": "^1.11.0",
"typechain": "^5.0.0",
"typescript": "^4.2.3",
"typescript": "^4.4.3",
"ua-parser-js": "^0.7.28",
"use-count-up": "^2.2.5",
"use-resize-observer": "^8.0.0",
@@ -133,12 +147,13 @@
"i18n:extract": "lingui extract --locale en-US",
"i18n:compile": "yarn i18n:extract && lingui compile",
"i18n:pseudo": "lingui extract --locale pseudo && lingui compile",
"postinstall": "yarn contracts:compile && yarn graphql:generate && yarn i18n:compile",
"prepare": "yarn contracts:compile && yarn graphql:generate && yarn i18n:compile",
"prepublishOnly": "yarn widgets:build",
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test --env=./custom-test-env.js",
"test:e2e": "start-server-and-test 'serve build -l 3000' http://localhost:3000 'cypress run --record'",
"widgets:start": "cross-env FAST_REFRESH=false REACT_APP_IS_WIDGET=true cosmos",
"widgets:start": "cosmos",
"widgets:build": "rollup --config --failAfterWarnings --configPlugin typescript2"
},
"browserslist": {
@@ -155,6 +170,7 @@
},
"license": "GPL-3.0-or-later",
"dependencies": {
"@babel/runtime": "^7.17.0",
"@ethersproject/abi": "^5.4.1",
"@ethersproject/abstract-provider": "^5.4.1",
"@ethersproject/address": "^5.4.0",
@@ -163,7 +179,7 @@
"@ethersproject/constants": "^5.4.0",
"@ethersproject/contracts": "^5.4.1",
"@ethersproject/hash": "^5.4.0",
"@ethersproject/providers": "^5.4.5",
"@ethersproject/providers": "5.4.0",
"@ethersproject/solidity": "^5.4.0",
"@ethersproject/strings": "^5.4.0",
"@ethersproject/units": "^5.4.0",
@@ -174,25 +190,27 @@
"@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/token-lists": "^1.0.0-beta.27",
"@uniswap/v2-sdk": "^3.0.1",
"@uniswap/v3-sdk": "^3.7.1",
"@web3-react/core": "^6.0.9",
"@uniswap/v3-sdk": "^3.8.2",
"web3-react-core": "npm:@web3-react/core@^6.0.9",
"ajv": "^6.12.3",
"cids": "^1.0.0",
"immer": "^9.0.6",
"jotai": "^1.3.7",
"jsbi": "^3.1.4",
"make-plural": "^7.0.0",
"ms.macro": "^2.0.0",
"multicodec": "^3.0.1",
"multihashes": "^4.0.2",
"node-vibrant": "^3.2.1-alpha.1",
"polished": "^3.3.2",
"popper-max-size-modifier": "^0.2.0",
"react": "^17.0.1",
"react-dom": "^17.0.1",
"react-feather": "^2.0.8",
"react-popper": "^2.2.3",
"react-redux": "^7.2.2",
@@ -203,12 +221,17 @@
"styled-components": "^5.3.0",
"tiny-invariant": "^1.2.0",
"wcag-contrast": "^3.0.0",
"wicg-inert": "^3.1.1",
"widgets-web3-react/core": "npm:@web3-react/core@8.0.16-alpha.0",
"widgets-web3-react/eip1193": "npm:@web3-react/eip1193@8.0.16-alpha.0",
"widgets-web3-react/empty": "npm:@web3-react/empty@8.0.17-alpha.0",
"widgets-web3-react/metamask": "npm:@web3-react/metamask@8.0.16-alpha.0",
"widgets-web3-react/types": "npm:@web3-react/types@8.0.16-alpha.0",
"widgets-web3-react/url": "npm:@web3-react/url@8.0.17-alpha.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",
"wicg-inert": "^3.1.1"
},
"peerDependencies": {
"@babel/runtime": "^7.17.0",
"react": "^17.0.1",
"react-dom": "^17.0.1"
}
}

View File

@@ -3,61 +3,125 @@
* This library lives in src/lib, but shares code with the interface application.
*/
import eslint from '@rollup/plugin-eslint'
import alias from '@rollup/plugin-alias'
import babel from '@rollup/plugin-babel'
import commonjs from '@rollup/plugin-commonjs'
import json from '@rollup/plugin-json'
import resolve from '@rollup/plugin-node-resolve'
import replace from '@rollup/plugin-replace'
import typescript from '@rollup/plugin-typescript'
import url from '@rollup/plugin-url'
import svgr from '@svgr/rollup'
import path from 'path'
import { RollupWarning } from 'rollup'
import copy from 'rollup-plugin-copy'
import del from 'rollup-plugin-delete'
import dts from 'rollup-plugin-dts'
import externals from 'rollup-plugin-node-externals'
import sass from 'rollup-plugin-scss'
import typescript from 'rollup-plugin-typescript2'
import { CompilerOptions } from 'typescript'
import { dependencies } from './package.json'
const deps = Object.keys(dependencies)
const replacements = {
const REPLACEMENTS = {
'process.env.REACT_APP_IS_WIDGET': true,
'process.env.REACT_APP_LOCALES': "'./locales'",
}
const library = {
const EXTENSIONS = ['.js', '.jsx', '.ts', '.tsx']
const ASSET_EXTENSIONS = ['.png', '.svg']
function isAsset(source: string) {
const extname = path.extname(source)
return extname && [...ASSET_EXTENSIONS, '.css', '.scss'].includes(extname)
}
const TS_CONFIG = './tsconfig.lib.json'
// eslint-disable-next-line @typescript-eslint/no-var-requires
const { baseUrl, paths }: CompilerOptions = require(TS_CONFIG).compilerOptions
const aliases = Object.entries({ ...paths }).flatMap(([find, replacements]) => {
return replacements.map((replacement) => ({
find: path.dirname(find),
replacement: path.join(__dirname, baseUrl || '.', path.dirname(replacement)),
}))
})
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
]
const check = {
input: 'src/lib/index.tsx',
output: { file: 'dist/widgets.tsc' },
external: isAsset,
plugins: [...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' },
external: isAsset,
plugins: [
dts({ compilerOptions: { baseUrl: 'dist/dts' } }),
del({ hook: 'buildEnd', targets: ['dist/widgets.tsc', 'dist/dts'] }),
],
}
const transpile = {
input: 'src/lib/index.tsx',
output: [
{
file: 'dist/widgets.js',
format: 'cjs',
inlineDynamicImports: true,
sourcemap: true,
sourcemap: false,
},
{
file: 'dist/widgets.esm.js',
format: 'esm',
inlineDynamicImports: true,
sourcemap: true,
sourcemap: false,
},
],
// necessary because some nested imports (eg jotai/*) would otherwise not resolve.
external: (source: string) => Boolean(deps.find((dep) => source === dep || source.startsWith(dep + '/'))),
plugins: [
eslint({ include: ['**/*.js', '**/*.jsx', '**/*.ts', '**/*.tsx'] }),
json(), // imports json
replace({ ...replacements, preventAssignment: true }),
url(), // imports files (including svgs) as data URIs
...plugins,
// Source code transformation
url({ include: ASSET_EXTENSIONS.map((extname) => '**/*' + extname) }), // imports assets as data URIs
svgr({ exportType: 'named', svgo: false }), // imports svgs as React components
sass(), // imports sass styles
typescript({ tsconfig: './tsconfig.lib.json', useTsconfigDeclarationDir: true }),
sass({ insert: true }), // imports inlined sass styles
commonjs(), // transforms cjs dependencies into tree-shakeable ES modules
babel({
babelHelpers: 'runtime',
presets: ['@babel/preset-env', ['@babel/preset-react', { runtime: 'automatic' }], '@babel/preset-typescript'],
extensions: EXTENSIONS,
plugins: [
'macros', // enables @lingui and styled-components macros
'@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 typings = {
input: 'dist/dts/lib/index.d.ts',
output: {
file: 'dist/widgets.d.ts',
format: 'es',
},
external: (source: string) => source.endsWith('.scss'),
plugins: [dts({ compilerOptions: { baseUrl: 'dist/dts' } })],
}
const config = [library, typings]
const config = [check, type, transpile]
export default config
function squelchTranspilationWarnings(warning: RollupWarning, warn: (warning: RollupWarning) => void) {
if (warning.pluginCode === 'TS5055') return
warn(warning)
}
function squelchTypeWarnings(warning: RollupWarning, warn: (warning: RollupWarning) => void) {
if (warning.code === 'UNUSED_EXTERNAL_IMPORT') return
warn(warning)
}

View File

@@ -1,11 +1,11 @@
import { Trans } from '@lingui/macro'
import { AbstractConnector } from '@web3-react/abstract-connector'
import { Connector } from '@web3-react/types'
import useActiveWeb3React from 'hooks/useActiveWeb3React'
import { useCallback, useContext } from 'react'
import { ExternalLink as LinkIcon } from 'react-feather'
import { useAppDispatch } from 'state/hooks'
import styled, { ThemeContext } from 'styled-components/macro'
import { Connector } from 'widgets-web3-react/types'
import { AbstractConnector } from 'web3-react-abstract-connector'
import { ReactComponent as Close } from '../../assets/images/x.svg'
import { injected, portis, walletlink } from '../../connectors'

View File

@@ -31,6 +31,8 @@ const BLOCKED_ADDRESSES: string[] = [
'0x6acdfba02d390b97ac2b2d42a63e85293bcc160e',
'0x48549a34ae37b12f6a30566245176994e17c6b4a',
'0x5512d943ed1f7c8a43f3435c85f7ab68b30121b0',
'0xc455f7fd3e0e12afd51fba5c106909934d8a0e4a',
'0x629e7Da20197a5429d30da36E77d06CdF796b71A',
]
export default function Blocklist({ children }: { children: ReactNode }) {

View File

@@ -1,60 +1,18 @@
import { Currency } from '@uniswap/sdk-core'
import EthereumLogo from 'assets/images/ethereum-logo.png'
import MaticLogo from 'assets/svg/matic-token-icon.svg'
import { SupportedChainId } from 'constants/chains'
import useHttpLocations from 'hooks/useHttpLocations'
import React, { useMemo } from 'react'
import { WrappedTokenInfo } from 'state/lists/wrappedTokenInfo'
import useCurrencyLogoURIs from 'lib/hooks/useCurrencyLogoURIs'
import React from 'react'
import styled from 'styled-components/macro'
import Logo from '../Logo'
type Network = 'ethereum' | 'arbitrum' | 'optimism'
function chainIdToNetworkName(networkId: SupportedChainId): Network {
switch (networkId) {
case SupportedChainId.MAINNET:
return 'ethereum'
case SupportedChainId.ARBITRUM_ONE:
return 'arbitrum'
case SupportedChainId.OPTIMISM:
return 'optimism'
default:
return 'ethereum'
}
}
export const getTokenLogoURL = (
address: string,
chainId: SupportedChainId = SupportedChainId.MAINNET
): string | void => {
const networkName = chainIdToNetworkName(chainId)
const networksWithUrls = [SupportedChainId.ARBITRUM_ONE, SupportedChainId.MAINNET, SupportedChainId.OPTIMISM]
if (networksWithUrls.includes(chainId)) {
return `https://raw.githubusercontent.com/Uniswap/assets/master/blockchains/${networkName}/assets/${address}/logo.png`
}
}
const StyledNativeLogo = styled.img<{ size: string }>`
width: ${({ size }) => size};
height: ${({ size }) => size};
background: radial-gradient(white 50%, #ffffff00 calc(75% + 1px), #ffffff00 100%);
border-radius: 50%;
-mox-box-shadow: 0 0 1px white;
-webkit-box-shadow: 0 0 1px white;
box-shadow: 0 0 1px white;
border: 0px solid rgba(255, 255, 255, 0);
`
const StyledLogo = styled(Logo)<{ size: string }>`
const StyledLogo = styled(Logo)<{ size: string; native: boolean }>`
width: ${({ size }) => size};
height: ${({ size }) => size};
background: radial-gradient(white 50%, #ffffff00 calc(75% + 1px), #ffffff00 100%);
border-radius: 50%;
-mox-box-shadow: 0 0 1px black;
-webkit-box-shadow: 0 0 1px black;
box-shadow: 0 0 1px black;
-mox-box-shadow: 0 0 1px ${({ native }) => (native ? 'white' : 'black')};
-webkit-box-shadow: 0 0 1px ${({ native }) => (native ? 'white' : 'black')};
box-shadow: 0 0 1px ${({ native }) => (native ? 'white' : 'black')};
border: 0px solid rgba(255, 255, 255, 0);
`
@@ -68,38 +26,16 @@ export default function CurrencyLogo({
size?: string
style?: React.CSSProperties
}) {
const uriLocations = useHttpLocations(currency instanceof WrappedTokenInfo ? currency.logoURI : undefined)
const logoURIs = useCurrencyLogoURIs(currency)
const srcs: string[] = useMemo(() => {
if (!currency || currency.isNative) return []
if (currency.isToken) {
const defaultUrls = []
const url = getTokenLogoURL(currency.address, currency.chainId)
if (url) {
defaultUrls.push(url)
}
if (currency instanceof WrappedTokenInfo) {
return [...uriLocations, ...defaultUrls]
}
return defaultUrls
}
return []
}, [currency, uriLocations])
if (currency?.isNative) {
let nativeLogoUrl: string
switch (currency.chainId) {
case SupportedChainId.POLYGON_MUMBAI:
case SupportedChainId.POLYGON:
nativeLogoUrl = MaticLogo
break
default:
nativeLogoUrl = EthereumLogo
break
}
return <StyledNativeLogo src={nativeLogoUrl} alt="ethereum logo" size={size} style={style} {...rest} />
}
return <StyledLogo size={size} srcs={srcs} alt={`${currency?.symbol ?? 'token'} logo`} style={style} {...rest} />
return (
<StyledLogo
size={size}
native={currency?.isNative ?? false}
srcs={logoURIs}
alt={`${currency?.symbol ?? 'token'} logo`}
style={style}
{...rest}
/>
)
}

View File

@@ -1,14 +1,19 @@
import { Trans } from '@lingui/macro'
import { CHAIN_INFO } from 'constants/chainInfo'
import { SupportedChainId } from 'constants/chains'
import { CHAIN_IDS_TO_NAMES, SupportedChainId } from 'constants/chains'
import useActiveWeb3React from 'hooks/useActiveWeb3React'
import { useOnClickOutside } from 'hooks/useOnClickOutside'
import { useCallback, useRef } from 'react'
import useParsedQueryString from 'hooks/useParsedQueryString'
import usePrevious from 'hooks/usePrevious'
import { ParsedQs } from 'qs'
import { useCallback, useEffect, useRef } from 'react'
import { ArrowDownCircle, ChevronDown } from 'react-feather'
import { useHistory } from 'react-router-dom'
import { useModalOpen, useToggleModal } from 'state/application/hooks'
import { addPopup, ApplicationModal } from 'state/application/reducer'
import styled from 'styled-components/macro'
import { ExternalLink, MEDIA_WIDTHS } from 'theme'
import { replaceURLParam } from 'utils/routes'
import { useAppDispatch } from '../../state/hooks'
import { switchToNetwork } from '../../utils/switchToNetwork'
@@ -211,31 +216,90 @@ function Row({
return rowContent
}
const getParsedChainId = (parsedQs?: ParsedQs) => {
const chain = parsedQs?.chain
if (!chain || typeof chain !== 'string') return { urlChain: undefined, urlChainId: undefined }
return { urlChain: chain.toLowerCase(), urlChainId: getChainIdFromName(chain) }
}
const getChainIdFromName = (name: string) => {
const entry = Object.entries(CHAIN_IDS_TO_NAMES).find(([_, n]) => n === name)
const chainId = entry?.[0]
return chainId ? parseInt(chainId) : undefined
}
const getChainNameFromId = (id: string | number) => {
// casting here may not be right but fine to return undefined if it's not a supported chain ID
return CHAIN_IDS_TO_NAMES[id as SupportedChainId] || ''
}
export default function NetworkSelector() {
const { chainId, library } = useActiveWeb3React()
const parsedQs = useParsedQueryString()
const { urlChain, urlChainId } = getParsedChainId(parsedQs)
const prevChainId = usePrevious(chainId)
const node = useRef<HTMLDivElement>()
const open = useModalOpen(ApplicationModal.NETWORK_SELECTOR)
const toggle = useToggleModal(ApplicationModal.NETWORK_SELECTOR)
useOnClickOutside(node, open ? toggle : undefined)
const history = useHistory()
const info = chainId ? CHAIN_INFO[chainId] : undefined
const dispatch = useAppDispatch()
const handleRowClick = useCallback(
(targetChain: number) => {
const handleChainSwitch = useCallback(
(targetChain: number, skipToggle?: boolean) => {
if (!library) return
switchToNetwork({ library, chainId: targetChain })
.then(() => toggle())
.then(() => {
if (!skipToggle) {
toggle()
}
history.replace({
search: replaceURLParam(history.location.search, 'chain', getChainNameFromId(targetChain)),
})
})
.catch((error) => {
console.error('Failed to switch networks', error)
toggle()
// we want app network <-> chainId param to be in sync, so if user changes the network by changing the URL
// but the request fails, revert the URL back to current chainId
if (chainId) {
history.replace({ search: replaceURLParam(history.location.search, 'chain', getChainNameFromId(chainId)) })
}
if (!skipToggle) {
toggle()
}
dispatch(addPopup({ content: { failedSwitchNetwork: targetChain }, key: `failed-network-switch` }))
})
},
[dispatch, library, toggle]
[dispatch, library, toggle, history, chainId]
)
useEffect(() => {
if (!chainId || !prevChainId) return
// when network change originates from wallet or dropdown selector, just update URL
if (chainId !== prevChainId) {
history.replace({ search: replaceURLParam(history.location.search, 'chain', getChainNameFromId(chainId)) })
// otherwise assume network change originates from URL
} else if (urlChainId && urlChainId !== chainId) {
handleChainSwitch(urlChainId, true)
}
}, [chainId, urlChainId, prevChainId, handleChainSwitch, history])
// set chain parameter on initial load if not there
useEffect(() => {
if (chainId && !urlChainId) {
history.replace({ search: replaceURLParam(history.location.search, 'chain', getChainNameFromId(chainId)) })
}
}, [chainId, history, urlChainId, urlChain])
if (!chainId || !info || !library) {
return null
}
@@ -252,10 +316,10 @@ export default function NetworkSelector() {
<FlyoutHeader>
<Trans>Select a network</Trans>
</FlyoutHeader>
<Row onSelectChain={handleRowClick} targetChain={SupportedChainId.MAINNET} />
<Row onSelectChain={handleRowClick} targetChain={SupportedChainId.POLYGON} />
<Row onSelectChain={handleRowClick} targetChain={SupportedChainId.OPTIMISM} />
<Row onSelectChain={handleRowClick} targetChain={SupportedChainId.ARBITRUM_ONE} />
<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>
)}
</SelectorWrapper>

View File

@@ -264,9 +264,7 @@ export default function Header() {
const {
infoLink,
addNetworkInfo: {
nativeCurrency: { symbol: nativeCurrencySymbol },
},
nativeCurrency: { symbol: nativeCurrencySymbol },
} = CHAIN_INFO[chainId ? chainId : SupportedChainId.MAINNET]
return (

View File

@@ -1,5 +1,5 @@
import { AbstractConnector } from '@web3-react/abstract-connector'
import { Connector } from 'widgets-web3-react/types'
import { Connector } from '@web3-react/types'
import { AbstractConnector } from 'web3-react-abstract-connector'
import CoinbaseWalletIcon from '../../assets/images/coinbaseWalletIcon.svg'
import FortmaticIcon from '../../assets/images/fortmaticIcon.png'

View File

@@ -1,16 +1,10 @@
import { Currency } from '@uniswap/sdk-core'
import { FeeAmount } from '@uniswap/v3-sdk'
import { usePoolActiveLiquidity } from 'hooks/usePoolTickData'
import JSBI from 'jsbi'
import { TickProcessed, usePoolActiveLiquidity } from 'hooks/usePoolTickData'
import { useCallback, useMemo } from 'react'
import { ChartEntry } from './types'
export interface TickProcessed {
liquidityActive: JSBI
price0: string
}
export function useDensityChartData({
currencyA,
currencyB,

View File

@@ -4,11 +4,10 @@ import styled from 'styled-components/macro'
import { MEDIA_WIDTHS } from 'theme'
import { useActivePopups } from '../../state/application/hooks'
import { useShowSurveyPopup, useURLWarningVisible } from '../../state/user/hooks'
import { useURLWarningVisible } from '../../state/user/hooks'
import { AutoColumn } from '../Column'
import ClaimPopup from './ClaimPopup'
import PopupItem from './PopupItem'
import SurveyPopup from './SurveyPopup'
const MobilePopupWrapper = styled.div<{ height: string | number }>`
position: relative;
@@ -60,9 +59,6 @@ export default function Popups() {
// get all popups
const activePopups = useActivePopups()
// show survey popup if user has not closed
const [showSurveyPopup] = useShowSurveyPopup()
const urlWarningActive = useURLWarningVisible()
// need extra padding if network is not L1 Ethereum
@@ -73,14 +69,12 @@ export default function Popups() {
<>
<FixedPopupColumn gap="20px" extraPadding={urlWarningActive} xlPadding={isNotOnMainnet}>
<ClaimPopup />
<SurveyPopup />
{activePopups.map((item) => (
<PopupItem key={item.key} content={item.content} popKey={item.key} removeAfterMs={item.removeAfterMs} />
))}
</FixedPopupColumn>
<MobilePopupWrapper height={activePopups?.length > 0 || showSurveyPopup ? 'fit-content' : 0}>
<MobilePopupWrapper height={activePopups?.length > 0 ? 'fit-content' : 0}>
<MobilePopupInner>
<SurveyPopup />
{activePopups // reverse so new items up front
.slice(0)
.reverse()

View File

@@ -3,45 +3,45 @@
exports[`renders multi route 1`] = `
<DocumentFragment>
<div
class="RoutingDiagram__Wrapper-sc-o1ook0-0 ePDWDk css-vurnku"
class="RoutingDiagram__Wrapper-sc-i2tbb-0 ivndgC css-vurnku"
>
<div
class="sc-bdnxRM Row-sc-nrd8cx-0 RoutingDiagram__RouteContainerRow-sc-o1ook0-1 lmTMKd itvFNV ibRCpr"
class="sc-bdnxRM Row-sc-u7azg8-0 RoutingDiagram__RouteContainerRow-sc-i2tbb-1 lmTMKd hLLNig hDkZVB"
>
CurrencyLogo currency=USDC
<div
class="sc-bdnxRM Row-sc-nrd8cx-0 RoutingDiagram__RouteRow-sc-o1ook0-2 lmTMKd itvFNV fzMiot"
class="sc-bdnxRM Row-sc-u7azg8-0 RoutingDiagram__RouteRow-sc-i2tbb-2 lmTMKd hLLNig hUDqOH"
>
<div
class="RoutingDiagram__DottedLine-sc-o1ook0-4 kkXINS"
class="RoutingDiagram__DottedLine-sc-i2tbb-4 cKqYfU"
>
<svg
class="RoutingDiagram__DotColor-sc-o1ook0-5 kgYqrO"
class="RoutingDiagram__DotColor-sc-i2tbb-5 fhSaBA"
>
dot_line.svg
</svg>
</div>
<div
class="Badge-sc-1mhw5si-0 RoutingDiagram__OpaqueBadge-sc-o1ook0-6 gayll OurGh"
class="Badge-sc-3epor3-0 RoutingDiagram__OpaqueBadge-sc-i2tbb-6 knpfHF gGARxH"
>
<div
class="Badge-sc-1mhw5si-0 RoutingDiagram__ProtocolBadge-sc-o1ook0-7 gayll bNVqMw"
class="Badge-sc-3epor3-0 RoutingDiagram__ProtocolBadge-sc-i2tbb-7 knpfHF lbdUti"
>
<div
class="theme__TextWrapper-sc-18nh1jk-0 cWOfab RoutingDiagram__BadgeText-sc-o1ook0-8 dYpdfO css-15li2d9"
class="theme__TextWrapper-sc-5lu8um-0 chxxqs RoutingDiagram__BadgeText-sc-i2tbb-8 ijjHig css-15li2d9"
>
V2
</div>
</div>
<div
class="theme__TextWrapper-sc-18nh1jk-0 cWOfab RoutingDiagram__BadgeText-sc-o1ook0-8 dYpdfO css-1aekuku"
class="theme__TextWrapper-sc-5lu8um-0 chxxqs RoutingDiagram__BadgeText-sc-i2tbb-8 ijjHig css-1aekuku"
style="min-width: auto;"
>
75%
</div>
</div>
<div
class="sc-bdnxRM Row-sc-nrd8cx-0 Row__AutoRow-sc-nrd8cx-3 iqvZFe itvFNV kkMfuq"
class="sc-bdnxRM Row-sc-u7azg8-0 Row__AutoRow-sc-u7azg8-3 iqvZFe hLLNig cUhARX"
style="justify-content: space-evenly; z-index: 2;"
width="100%"
>
@@ -51,42 +51,42 @@ exports[`renders multi route 1`] = `
CurrencyLogo currency=DAI
</div>
<div
class="sc-bdnxRM Row-sc-nrd8cx-0 RoutingDiagram__RouteContainerRow-sc-o1ook0-1 lmTMKd itvFNV ibRCpr"
class="sc-bdnxRM Row-sc-u7azg8-0 RoutingDiagram__RouteContainerRow-sc-i2tbb-1 lmTMKd hLLNig hDkZVB"
>
CurrencyLogo currency=USDC
<div
class="sc-bdnxRM Row-sc-nrd8cx-0 RoutingDiagram__RouteRow-sc-o1ook0-2 lmTMKd itvFNV fzMiot"
class="sc-bdnxRM Row-sc-u7azg8-0 RoutingDiagram__RouteRow-sc-i2tbb-2 lmTMKd hLLNig hUDqOH"
>
<div
class="RoutingDiagram__DottedLine-sc-o1ook0-4 kkXINS"
class="RoutingDiagram__DottedLine-sc-i2tbb-4 cKqYfU"
>
<svg
class="RoutingDiagram__DotColor-sc-o1ook0-5 kgYqrO"
class="RoutingDiagram__DotColor-sc-i2tbb-5 fhSaBA"
>
dot_line.svg
</svg>
</div>
<div
class="Badge-sc-1mhw5si-0 RoutingDiagram__OpaqueBadge-sc-o1ook0-6 gayll OurGh"
class="Badge-sc-3epor3-0 RoutingDiagram__OpaqueBadge-sc-i2tbb-6 knpfHF gGARxH"
>
<div
class="Badge-sc-1mhw5si-0 RoutingDiagram__ProtocolBadge-sc-o1ook0-7 gayll bNVqMw"
class="Badge-sc-3epor3-0 RoutingDiagram__ProtocolBadge-sc-i2tbb-7 knpfHF lbdUti"
>
<div
class="theme__TextWrapper-sc-18nh1jk-0 cWOfab RoutingDiagram__BadgeText-sc-o1ook0-8 dYpdfO css-15li2d9"
class="theme__TextWrapper-sc-5lu8um-0 chxxqs RoutingDiagram__BadgeText-sc-i2tbb-8 ijjHig css-15li2d9"
>
V3
</div>
</div>
<div
class="theme__TextWrapper-sc-18nh1jk-0 cWOfab RoutingDiagram__BadgeText-sc-o1ook0-8 dYpdfO css-1aekuku"
class="theme__TextWrapper-sc-5lu8um-0 chxxqs RoutingDiagram__BadgeText-sc-i2tbb-8 ijjHig css-1aekuku"
style="min-width: auto;"
>
25%
</div>
</div>
<div
class="sc-bdnxRM Row-sc-nrd8cx-0 Row__AutoRow-sc-nrd8cx-3 iqvZFe itvFNV kkMfuq"
class="sc-bdnxRM Row-sc-u7azg8-0 Row__AutoRow-sc-u7azg8-3 iqvZFe hLLNig cUhARX"
style="justify-content: space-evenly; z-index: 2;"
width="100%"
>
@@ -102,45 +102,45 @@ exports[`renders multi route 1`] = `
exports[`renders single route 1`] = `
<DocumentFragment>
<div
class="RoutingDiagram__Wrapper-sc-o1ook0-0 ePDWDk css-vurnku"
class="RoutingDiagram__Wrapper-sc-i2tbb-0 ivndgC css-vurnku"
>
<div
class="sc-bdnxRM Row-sc-nrd8cx-0 RoutingDiagram__RouteContainerRow-sc-o1ook0-1 lmTMKd itvFNV ibRCpr"
class="sc-bdnxRM Row-sc-u7azg8-0 RoutingDiagram__RouteContainerRow-sc-i2tbb-1 lmTMKd hLLNig hDkZVB"
>
CurrencyLogo currency=USDC
<div
class="sc-bdnxRM Row-sc-nrd8cx-0 RoutingDiagram__RouteRow-sc-o1ook0-2 lmTMKd itvFNV fzMiot"
class="sc-bdnxRM Row-sc-u7azg8-0 RoutingDiagram__RouteRow-sc-i2tbb-2 lmTMKd hLLNig hUDqOH"
>
<div
class="RoutingDiagram__DottedLine-sc-o1ook0-4 kkXINS"
class="RoutingDiagram__DottedLine-sc-i2tbb-4 cKqYfU"
>
<svg
class="RoutingDiagram__DotColor-sc-o1ook0-5 kgYqrO"
class="RoutingDiagram__DotColor-sc-i2tbb-5 fhSaBA"
>
dot_line.svg
</svg>
</div>
<div
class="Badge-sc-1mhw5si-0 RoutingDiagram__OpaqueBadge-sc-o1ook0-6 gayll OurGh"
class="Badge-sc-3epor3-0 RoutingDiagram__OpaqueBadge-sc-i2tbb-6 knpfHF gGARxH"
>
<div
class="Badge-sc-1mhw5si-0 RoutingDiagram__ProtocolBadge-sc-o1ook0-7 gayll bNVqMw"
class="Badge-sc-3epor3-0 RoutingDiagram__ProtocolBadge-sc-i2tbb-7 knpfHF lbdUti"
>
<div
class="theme__TextWrapper-sc-18nh1jk-0 cWOfab RoutingDiagram__BadgeText-sc-o1ook0-8 dYpdfO css-15li2d9"
class="theme__TextWrapper-sc-5lu8um-0 chxxqs RoutingDiagram__BadgeText-sc-i2tbb-8 ijjHig css-15li2d9"
>
V3
</div>
</div>
<div
class="theme__TextWrapper-sc-18nh1jk-0 cWOfab RoutingDiagram__BadgeText-sc-o1ook0-8 dYpdfO css-1aekuku"
class="theme__TextWrapper-sc-5lu8um-0 chxxqs RoutingDiagram__BadgeText-sc-i2tbb-8 ijjHig css-1aekuku"
style="min-width: auto;"
>
100%
</div>
</div>
<div
class="sc-bdnxRM Row-sc-nrd8cx-0 Row__AutoRow-sc-nrd8cx-3 iqvZFe itvFNV kkMfuq"
class="sc-bdnxRM Row-sc-u7azg8-0 Row__AutoRow-sc-u7azg8-3 iqvZFe hLLNig cUhARX"
style="justify-content: space-evenly; z-index: 2;"
width="100%"
>
@@ -156,7 +156,7 @@ exports[`renders single route 1`] = `
exports[`renders when no routes are provided 1`] = `
<DocumentFragment>
<div
class="RoutingDiagram__Wrapper-sc-o1ook0-0 ePDWDk css-vurnku"
class="RoutingDiagram__Wrapper-sc-i2tbb-0 ivndgC css-vurnku"
/>
</DocumentFragment>
`;

View File

@@ -6,6 +6,7 @@ import useDebounce from 'hooks/useDebounce'
import { useOnClickOutside } from 'hooks/useOnClickOutside'
import useTheme from 'hooks/useTheme'
import useToggle from 'hooks/useToggle'
import useNativeCurrency from 'lib/hooks/useNativeCurrency'
import { getTokenFilter } from 'lib/hooks/useTokenList/filtering'
import { tokenComparator, useSortTokensByQuery } from 'lib/hooks/useTokenList/sorting'
import { KeyboardEvent, RefObject, useCallback, useEffect, useMemo, useRef, useState } from 'react'
@@ -17,13 +18,7 @@ import { Text } from 'rebass'
import { useAllTokenBalances } from 'state/wallet/hooks'
import styled from 'styled-components/macro'
import {
useAllTokens,
useIsUserAddedToken,
useNativeCurrency,
useSearchInactiveTokenLists,
useToken,
} from '../../hooks/Tokens'
import { useAllTokens, useIsUserAddedToken, useSearchInactiveTokenLists, useToken } from '../../hooks/Tokens'
import { ButtonText, CloseIcon, IconWrapper, ThemedText } from '../../theme'
import { isAddress } from '../../utils'
import Column from '../Column'

View File

@@ -2,11 +2,11 @@
import { t, Trans } from '@lingui/macro'
import { Percent } from '@uniswap/sdk-core'
import useActiveWeb3React from 'hooks/useActiveWeb3React'
import { AUTO_ROUTER_SUPPORTED_CHAINS } from 'lib/hooks/routing/clientSideSmartOrderRouter'
import { useContext, useRef, useState } from 'react'
import { Settings, X } from 'react-feather'
import ReactGA from 'react-ga'
import { Text } from 'rebass'
import { AUTO_ROUTER_SUPPORTED_CHAINS } from 'state/routing/clientSideSmartOrderRouter/constants'
import styled, { ThemeContext } from 'styled-components/macro'
import { useOnClickOutside } from '../../hooks/useOnClickOutside'

View File

@@ -1,7 +1,7 @@
import { Trans } from '@lingui/macro'
import { AbstractConnector } from '@web3-react/abstract-connector'
import { darken } from 'polished'
import styled from 'styled-components/macro'
import { AbstractConnector } from 'web3-react-abstract-connector'
import { injected } from '../../connectors'
import { SUPPORTED_WALLETS } from '../../constants/wallet'

View File

@@ -1,7 +1,4 @@
import { Trans } from '@lingui/macro'
import { AbstractConnector } from '@web3-react/abstract-connector'
import { UnsupportedChainIdError, useWeb3React } from '@web3-react/core'
import { WalletConnectConnector } from '@web3-react/walletconnect-connector'
import { AutoColumn } from 'components/Column'
import { PrivacyPolicy } from 'components/PrivacyPolicy'
import Row, { AutoRow, RowBetween } from 'components/Row'
@@ -10,6 +7,9 @@ import { useEffect, useState } from 'react'
import { ArrowLeft, ArrowRight, Info } from 'react-feather'
import ReactGA from 'react-ga'
import styled from 'styled-components/macro'
import { AbstractConnector } from 'web3-react-abstract-connector'
import { UnsupportedChainIdError, useWeb3React } from 'web3-react-core'
import { WalletConnectConnector } from 'web3-react-walletconnect-connector'
import MetamaskIcon from '../../assets/images/metamask.png'
import { ReactComponent as Close } from '../../assets/images/x.svg'

View File

@@ -1,7 +1,7 @@
import { Trans } from '@lingui/macro'
import { useWeb3React } from '@web3-react/core'
import { useEffect } from 'react'
import styled from 'styled-components/macro'
import { useWeb3React } from 'web3-react-core'
import { network } from '../../connectors'
import { NetworkContextName } from '../../constants/misc'

View File

@@ -1,12 +1,12 @@
// eslint-disable-next-line no-restricted-imports
import { t, Trans } from '@lingui/macro'
import { AbstractConnector } from '@web3-react/abstract-connector'
import { UnsupportedChainIdError, useWeb3React } from '@web3-react/core'
import { Connector } from '@web3-react/types'
import { darken } from 'polished'
import { useMemo } from 'react'
import { Activity } from 'react-feather'
import styled, { css } from 'styled-components/macro'
import { Connector } from 'widgets-web3-react/types'
import { AbstractConnector } from 'web3-react-abstract-connector'
import { UnsupportedChainIdError, useWeb3React } from 'web3-react-core'
import { NetworkContextName } from '../../constants/misc'
import useENSName from '../../hooks/useENSName'

View File

@@ -4,6 +4,8 @@ import ReactGA from 'react-ga'
import { RouteComponentProps } from 'react-router-dom'
import { getCLS, getFCP, getFID, getLCP, Metric } from 'web-vitals'
import { GOOGLE_ANALYTICS_CLIENT_ID_STORAGE_KEY } from './index'
function reportWebVitals({ name, delta, id }: Metric) {
ReactGA.timing({
category: 'Web Vitals',
@@ -31,5 +33,15 @@ export default function GoogleAnalyticsReporter({ location: { pathname, search }
useEffect(() => {
ReactGA.pageview(`${pathname}${search}`)
}, [pathname, search])
useEffect(() => {
// typed as 'any' in react-ga -.-
ReactGA.ga((tracker: any) => {
if (!tracker) return
const clientId = tracker.get('clientId')
window.localStorage.setItem(GOOGLE_ANALYTICS_CLIENT_ID_STORAGE_KEY, clientId)
})
}, [])
return null
}

View File

@@ -1,12 +1,17 @@
import ReactGA from 'react-ga'
import { isMobile } from 'utils/userAgent'
export const GOOGLE_ANALYTICS_CLIENT_ID_STORAGE_KEY = 'ga_client_id'
const GOOGLE_ANALYTICS_ID: string | undefined = process.env.REACT_APP_GOOGLE_ANALYTICS_ID
const storedClientId = window.localStorage.getItem(GOOGLE_ANALYTICS_CLIENT_ID_STORAGE_KEY)
if (typeof GOOGLE_ANALYTICS_ID === 'string') {
ReactGA.initialize(GOOGLE_ANALYTICS_ID, {
gaOptions: {
storage: 'none',
storeGac: false,
clientId: storedClientId ?? undefined,
},
})
ReactGA.set({

View File

@@ -1,10 +1,11 @@
import { TransactionResponse } from '@ethersproject/providers'
import { Trans } from '@lingui/macro'
import StakingRewardsJson from '@uniswap/liquidity-staker/build/StakingRewards.json'
import useActiveWeb3React from 'hooks/useActiveWeb3React'
import { ReactNode, useState } from 'react'
import styled from 'styled-components/macro'
import { useStakingContract } from '../../hooks/useContract'
import { useContract } from '../../hooks/useContract'
import { StakingInfo } from '../../state/stake/hooks'
import { TransactionType } from '../../state/transactions/actions'
import { useTransactionAdder } from '../../state/transactions/hooks'
@@ -15,6 +16,12 @@ import Modal from '../Modal'
import { LoadingView, SubmittedView } from '../ModalViews'
import { RowBetween } from '../Row'
const { abi: STAKING_REWARDS_ABI } = StakingRewardsJson
function useStakingContract(stakingAddress?: string, withSignerIfPossible?: boolean) {
return useContract(stakingAddress, STAKING_REWARDS_ABI, withSignerIfPossible)
}
const ContentWrapper = styled(AutoColumn)`
width: 100%;
padding: 1rem;

View File

@@ -1,14 +1,15 @@
import { TransactionResponse } from '@ethersproject/providers'
import { Trans } from '@lingui/macro'
import StakingRewardsJson from '@uniswap/liquidity-staker/build/StakingRewards.json'
import { CurrencyAmount, Token } from '@uniswap/sdk-core'
import { Pair } from '@uniswap/v2-sdk'
import useActiveWeb3React from 'hooks/useActiveWeb3React'
import { useV2LiquidityTokenPermit } from 'hooks/useV2LiquidityTokenPermit'
import { useCallback, useState } from 'react'
import styled from 'styled-components/macro'
import { ApprovalState, useApproveCallback } from '../../hooks/useApproveCallback'
import { usePairContract, useStakingContract, useV2RouterContract } from '../../hooks/useContract'
import { useV2LiquidityTokenPermit } from '../../hooks/useERC20Permit'
import { useContract, usePairContract, useV2RouterContract } from '../../hooks/useContract'
import useTransactionDeadline from '../../hooks/useTransactionDeadline'
import { StakingInfo, useDerivedStakeInfo } from '../../state/stake/hooks'
import { TransactionType } from '../../state/transactions/actions'
@@ -24,6 +25,12 @@ import { LoadingView, SubmittedView } from '../ModalViews'
import ProgressCircles from '../ProgressSteps'
import { RowBetween } from '../Row'
const { abi: STAKING_REWARDS_ABI } = StakingRewardsJson
function useStakingContract(stakingAddress?: string, withSignerIfPossible?: boolean) {
return useContract(stakingAddress, STAKING_REWARDS_ABI, withSignerIfPossible)
}
const HypotheticalRewardRate = styled.div<{ dim: boolean }>`
display: flex;
justify-content: space-between;

View File

@@ -1,10 +1,11 @@
import { TransactionResponse } from '@ethersproject/providers'
import { Trans } from '@lingui/macro'
import StakingRewardsJson from '@uniswap/liquidity-staker/build/StakingRewards.json'
import useActiveWeb3React from 'hooks/useActiveWeb3React'
import { ReactNode, useState } from 'react'
import styled from 'styled-components/macro'
import { useStakingContract } from '../../hooks/useContract'
import { useContract } from '../../hooks/useContract'
import { StakingInfo } from '../../state/stake/hooks'
import { TransactionType } from '../../state/transactions/actions'
import { useTransactionAdder } from '../../state/transactions/hooks'
@@ -16,6 +17,12 @@ import Modal from '../Modal'
import { LoadingView, SubmittedView } from '../ModalViews'
import { RowBetween } from '../Row'
const { abi: STAKING_REWARDS_ABI } = StakingRewardsJson
function useStakingContract(stakingAddress?: string, withSignerIfPossible?: boolean) {
return useContract(stakingAddress, STAKING_REWARDS_ABI, withSignerIfPossible)
}
const ContentWrapper = styled(AutoColumn)`
width: 100%;
padding: 1rem;

View File

@@ -3,6 +3,7 @@ import { Trade } from '@uniswap/router-sdk'
import { Currency, Percent, TradeType } from '@uniswap/sdk-core'
import { ReactNode, useCallback, useMemo } from 'react'
import { InterfaceTrade } from 'state/routing/types'
import { tradeMeaningfullyDiffers } from 'utils/tradeMeaningFullyDiffer'
import TransactionConfirmationModal, {
ConfirmationModalContent,
@@ -11,23 +12,6 @@ import TransactionConfirmationModal, {
import SwapModalFooter from './SwapModalFooter'
import SwapModalHeader from './SwapModalHeader'
/**
* Returns true if the trade requires a confirmation of details before we can submit it
* @param args either a pair of V2 trades or a pair of V3 trades
*/
function tradeMeaningfullyDiffers(
...args: [Trade<Currency, Currency, TradeType>, Trade<Currency, Currency, TradeType>]
): boolean {
const [tradeA, tradeB] = args
return (
tradeA.tradeType !== tradeB.tradeType ||
!tradeA.inputAmount.currency.equals(tradeB.inputAmount.currency) ||
!tradeA.inputAmount.equalTo(tradeB.inputAmount) ||
!tradeA.outputAmount.currency.equals(tradeB.outputAmount.currency) ||
!tradeA.outputAmount.equalTo(tradeB.outputAmount)
)
}
export default function ConfirmSwapModal({
trade,
originalTrade,

View File

@@ -16,7 +16,7 @@ const StyledPriceContainer = styled.button`
background-color: transparent;
border: none;
cursor: pointer;
align-items: center
align-items: center;
justify-content: flex-start;
padding: 0;
grid-template-columns: 1fr auto;

View File

@@ -1,5 +1,5 @@
import { Trans } from '@lingui/macro'
import { L2_CHAIN_IDS } from 'constants/chains'
import { SupportedChainId } from 'constants/chains'
import useActiveWeb3React from 'hooks/useActiveWeb3React'
import styled from 'styled-components/macro'
import { ThemedText } from 'theme'
@@ -38,7 +38,7 @@ const EmptyState = ({ HeaderContent, SubHeaderContent }: EmptyStateProps) => (
export default function ProposalEmptyState() {
const { chainId } = useActiveWeb3React()
if (chainId && L2_CHAIN_IDS.includes(chainId)) {
if (chainId && chainId !== SupportedChainId.MAINNET) {
return (
<EmptyState
HeaderContent={() => <Trans>Please connect to Layer 1 Ethereum</Trans>}

View File

@@ -1,4 +1,4 @@
import { FortmaticConnector as FortmaticConnectorCore } from '@web3-react/fortmatic-connector'
import { FortmaticConnector as FortmaticConnectorCore } from 'web3-react-fortmatic-connector'
export const OVERLAY_READY = 'OVERLAY_READY'

View File

@@ -1,6 +1,6 @@
import { AbstractConnector } from '@web3-react/abstract-connector'
import { ConnectorUpdate } from '@web3-react/types'
import invariant from 'tiny-invariant'
import { AbstractConnector } from 'web3-react-abstract-connector'
import { ConnectorUpdate } from 'web3-react-types'
interface NetworkConnectorArguments {
urls: { [chainId: number]: string }

View File

@@ -1,11 +1,11 @@
import { Web3Provider } from '@ethersproject/providers'
import { SafeAppConnector } from '@gnosis.pm/safe-apps-web3-react'
import { InjectedConnector } from '@web3-react/injected-connector'
import { PortisConnector } from '@web3-react/portis-connector'
import { WalletConnectConnector } from '@web3-react/walletconnect-connector'
import { WalletLinkConnector } from '@web3-react/walletlink-connector'
import { INFURA_NETWORK_URLS } from 'constants/chainInfo'
import { ALL_SUPPORTED_CHAIN_IDS, SupportedChainId } from 'constants/chains'
import { INFURA_NETWORK_URLS } from 'constants/infura'
import { InjectedConnector } from 'web3-react-injected-connector'
import { PortisConnector } from 'web3-react-portis-connector'
import { WalletConnectConnector } from 'web3-react-walletconnect-connector'
import { WalletLinkConnector } from 'web3-react-walletlink-connector'
import UNISWAP_LOGO_URL from '../assets/svg/logo.svg'
import getLibrary from '../utils/getLibrary'
@@ -53,5 +53,5 @@ export const walletlink = new WalletLinkConnector({
url: INFURA_NETWORK_URLS[SupportedChainId.MAINNET],
appName: 'Uniswap',
appLogoUrl: UNISWAP_LOGO_URL,
supportedChainIds: [SupportedChainId.MAINNET, SupportedChainId.POLYGON],
supportedChainIds: ALL_SUPPORTED_CHAIN_IDS,
})

View File

@@ -107,3 +107,8 @@ export const V3_MIGRATOR_ADDRESSES: AddressMap = constructSameAddressMap('0xA564
SupportedChainId.POLYGON_MUMBAI,
SupportedChainId.POLYGON,
])
export const TICK_LENS_ADDRESSES: AddressMap = {
[SupportedChainId.ARBITRUM_ONE]: '0xbfd8137f7d1516D3ea5cA83523914859ec47F573',
[SupportedChainId.ARBITRUM_RINKEBY]: '0xbfd8137f7d1516D3ea5cA83523914859ec47F573',
}

View File

@@ -7,40 +7,6 @@ import ms from 'ms.macro'
import { SupportedChainId, SupportedL1ChainId, SupportedL2ChainId } from './chains'
import { ARBITRUM_LIST, OPTIMISM_LIST } from './lists'
const INFURA_KEY = process.env.REACT_APP_INFURA_KEY
if (typeof INFURA_KEY === 'undefined') {
throw new Error(`REACT_APP_INFURA_KEY must be a defined environment variable`)
}
/**
* These are the network URLs used by the interface when there is not another available source of chain data
*/
export const INFURA_NETWORK_URLS: { [key in SupportedChainId]: string } = {
[SupportedChainId.MAINNET]: `https://mainnet.infura.io/v3/${INFURA_KEY}`,
[SupportedChainId.RINKEBY]: `https://rinkeby.infura.io/v3/${INFURA_KEY}`,
[SupportedChainId.ROPSTEN]: `https://ropsten.infura.io/v3/${INFURA_KEY}`,
[SupportedChainId.GOERLI]: `https://goerli.infura.io/v3/${INFURA_KEY}`,
[SupportedChainId.KOVAN]: `https://kovan.infura.io/v3/${INFURA_KEY}`,
[SupportedChainId.OPTIMISM]: `https://optimism-mainnet.infura.io/v3/${INFURA_KEY}`,
[SupportedChainId.OPTIMISTIC_KOVAN]: `https://optimism-kovan.infura.io/v3/${INFURA_KEY}`,
[SupportedChainId.ARBITRUM_ONE]: `https://arbitrum-mainnet.infura.io/v3/${INFURA_KEY}`,
[SupportedChainId.ARBITRUM_RINKEBY]: `https://arbitrum-rinkeby.infura.io/v3/${INFURA_KEY}`,
[SupportedChainId.POLYGON]: `https://polygon-mainnet.infura.io/v3/${INFURA_KEY}`,
[SupportedChainId.POLYGON_MUMBAI]: `https://polygon-mumbai.infura.io/v3/${INFURA_KEY}`,
}
/**
* This is used to call the add network RPC
*/
interface AddNetworkInfo {
readonly rpcUrl: string
readonly nativeCurrency: {
name: string // e.g. 'Goerli ETH',
symbol: string // e.g. 'gorETH',
decimals: number // e.g. 18,
}
}
export enum NetworkType {
L1,
L2,
@@ -56,7 +22,11 @@ interface BaseChainInfo {
readonly logoUrl: string
readonly label: string
readonly helpCenterUrl?: string
readonly addNetworkInfo: AddNetworkInfo
readonly nativeCurrency: {
name: string // e.g. 'Goerli ETH',
symbol: string // e.g. 'gorETH',
decimals: number // e.g. 18,
}
}
export interface L1ChainInfo extends BaseChainInfo {
@@ -83,10 +53,7 @@ export const CHAIN_INFO: ChainInfoMap = {
infoLink: 'https://info.uniswap.org/#/',
label: 'Ethereum',
logoUrl: ethereumLogoUrl,
addNetworkInfo: {
nativeCurrency: { name: 'Ether', symbol: 'ETH', decimals: 18 },
rpcUrl: INFURA_NETWORK_URLS[SupportedChainId.MAINNET],
},
nativeCurrency: { name: 'Ether', symbol: 'ETH', decimals: 18 },
},
[SupportedChainId.RINKEBY]: {
networkType: NetworkType.L1,
@@ -95,10 +62,7 @@ export const CHAIN_INFO: ChainInfoMap = {
infoLink: 'https://info.uniswap.org/#/',
label: 'Rinkeby',
logoUrl: ethereumLogoUrl,
addNetworkInfo: {
nativeCurrency: { name: 'Rinkeby Ether', symbol: 'rETH', decimals: 18 },
rpcUrl: INFURA_NETWORK_URLS[SupportedChainId.RINKEBY],
},
nativeCurrency: { name: 'Rinkeby Ether', symbol: 'rETH', decimals: 18 },
},
[SupportedChainId.ROPSTEN]: {
networkType: NetworkType.L1,
@@ -107,10 +71,7 @@ export const CHAIN_INFO: ChainInfoMap = {
infoLink: 'https://info.uniswap.org/#/',
label: 'Ropsten',
logoUrl: ethereumLogoUrl,
addNetworkInfo: {
nativeCurrency: { name: 'Ropsten Ether', symbol: 'ropETH', decimals: 18 },
rpcUrl: INFURA_NETWORK_URLS[SupportedChainId.ROPSTEN],
},
nativeCurrency: { name: 'Ropsten Ether', symbol: 'ropETH', decimals: 18 },
},
[SupportedChainId.KOVAN]: {
networkType: NetworkType.L1,
@@ -119,10 +80,7 @@ export const CHAIN_INFO: ChainInfoMap = {
infoLink: 'https://info.uniswap.org/#/',
label: 'Kovan',
logoUrl: ethereumLogoUrl,
addNetworkInfo: {
nativeCurrency: { name: 'Kovan Ether', symbol: 'kovETH', decimals: 18 },
rpcUrl: INFURA_NETWORK_URLS[SupportedChainId.KOVAN],
},
nativeCurrency: { name: 'Kovan Ether', symbol: 'kovETH', decimals: 18 },
},
[SupportedChainId.GOERLI]: {
networkType: NetworkType.L1,
@@ -131,10 +89,7 @@ export const CHAIN_INFO: ChainInfoMap = {
infoLink: 'https://info.uniswap.org/#/',
label: 'Görli',
logoUrl: ethereumLogoUrl,
addNetworkInfo: {
nativeCurrency: { name: 'Görli Ether', symbol: 'görETH', decimals: 18 },
rpcUrl: INFURA_NETWORK_URLS[SupportedChainId.GOERLI],
},
nativeCurrency: { name: 'Görli Ether', symbol: 'görETH', decimals: 18 },
},
[SupportedChainId.OPTIMISM]: {
networkType: NetworkType.L2,
@@ -148,10 +103,7 @@ export const CHAIN_INFO: ChainInfoMap = {
logoUrl: optimismLogoUrl,
statusPage: 'https://optimism.io/status',
helpCenterUrl: 'https://help.uniswap.org/en/collections/3137778-uniswap-on-optimistic-ethereum-oξ',
addNetworkInfo: {
nativeCurrency: { name: 'Ether', symbol: 'ETH', decimals: 18 },
rpcUrl: 'https://mainnet.optimism.io',
},
nativeCurrency: { name: 'Ether', symbol: 'ETH', decimals: 18 },
},
[SupportedChainId.OPTIMISTIC_KOVAN]: {
networkType: NetworkType.L2,
@@ -165,10 +117,7 @@ export const CHAIN_INFO: ChainInfoMap = {
logoUrl: optimismLogoUrl,
statusPage: 'https://optimism.io/status',
helpCenterUrl: 'https://help.uniswap.org/en/collections/3137778-uniswap-on-optimistic-ethereum-oξ',
addNetworkInfo: {
nativeCurrency: { name: 'Optimistic Kovan Ether', symbol: 'kovOpETH', decimals: 18 },
rpcUrl: 'https://kovan.optimism.io',
},
nativeCurrency: { name: 'Optimistic Kovan Ether', symbol: 'kovOpETH', decimals: 18 },
},
[SupportedChainId.ARBITRUM_ONE]: {
networkType: NetworkType.L2,
@@ -181,10 +130,7 @@ export const CHAIN_INFO: ChainInfoMap = {
logoUrl: arbitrumLogoUrl,
defaultListUrl: ARBITRUM_LIST,
helpCenterUrl: 'https://help.uniswap.org/en/collections/3137787-uniswap-on-arbitrum',
addNetworkInfo: {
nativeCurrency: { name: 'Ether', symbol: 'ETH', decimals: 18 },
rpcUrl: 'https://arb1.arbitrum.io/rpc',
},
nativeCurrency: { name: 'Ether', symbol: 'ETH', decimals: 18 },
},
[SupportedChainId.ARBITRUM_RINKEBY]: {
networkType: NetworkType.L2,
@@ -197,10 +143,7 @@ export const CHAIN_INFO: ChainInfoMap = {
logoUrl: arbitrumLogoUrl,
defaultListUrl: ARBITRUM_LIST,
helpCenterUrl: 'https://help.uniswap.org/en/collections/3137787-uniswap-on-arbitrum',
addNetworkInfo: {
nativeCurrency: { name: 'Rinkeby Arbitrum Ether', symbol: 'rinkArbETH', decimals: 18 },
rpcUrl: 'https://rinkeby.arbitrum.io/rpc',
},
nativeCurrency: { name: 'Rinkeby Arbitrum Ether', symbol: 'rinkArbETH', decimals: 18 },
},
[SupportedChainId.POLYGON]: {
networkType: NetworkType.L1,
@@ -211,10 +154,7 @@ export const CHAIN_INFO: ChainInfoMap = {
infoLink: 'https://info.uniswap.org/#/polygon/',
label: 'Polygon',
logoUrl: polygonMaticLogo,
addNetworkInfo: {
rpcUrl: 'https://polygon-rpc.com/',
nativeCurrency: { name: 'Polygon Matic', symbol: 'MATIC', decimals: 18 },
},
nativeCurrency: { name: 'Polygon Matic', symbol: 'MATIC', decimals: 18 },
},
[SupportedChainId.POLYGON_MUMBAI]: {
networkType: NetworkType.L1,
@@ -225,9 +165,6 @@ export const CHAIN_INFO: ChainInfoMap = {
infoLink: 'https://info.uniswap.org/#/polygon/',
label: 'Polygon Mumbai',
logoUrl: polygonMaticLogo,
addNetworkInfo: {
nativeCurrency: { name: 'Polygon Mumbai Matic', symbol: 'mMATIC', decimals: 18 },
rpcUrl: 'https://rpc-endpoints.superfluid.dev/mumbai',
},
nativeCurrency: { name: 'Polygon Mumbai Matic', symbol: 'mMATIC', decimals: 18 },
},
}

View File

@@ -18,6 +18,20 @@ export enum SupportedChainId {
POLYGON_MUMBAI = 80001,
}
export const CHAIN_IDS_TO_NAMES = {
[SupportedChainId.MAINNET]: 'mainnet',
[SupportedChainId.ROPSTEN]: 'ropsten',
[SupportedChainId.RINKEBY]: 'rinkeby',
[SupportedChainId.GOERLI]: 'goerli',
[SupportedChainId.KOVAN]: 'kovan',
[SupportedChainId.POLYGON]: 'polygon',
[SupportedChainId.POLYGON_MUMBAI]: 'polygon_mumbai',
[SupportedChainId.ARBITRUM_ONE]: 'arbitrum',
[SupportedChainId.ARBITRUM_RINKEBY]: 'arbitrum_rinkeby',
[SupportedChainId.OPTIMISM]: 'optimism',
[SupportedChainId.OPTIMISTIC_KOVAN]: 'optimistic_kovan',
}
/**
* Array of all the supported chain IDs
*/

23
src/constants/infura.ts Normal file
View File

@@ -0,0 +1,23 @@
import { SupportedChainId } from './chains'
const INFURA_KEY = process.env.REACT_APP_INFURA_KEY
if (typeof INFURA_KEY === 'undefined') {
throw new Error(`REACT_APP_INFURA_KEY must be a defined environment variable`)
}
/**
* These are the network URLs used by the interface when there is not another available source of chain data
*/
export const INFURA_NETWORK_URLS: { [key in SupportedChainId]: string } = {
[SupportedChainId.MAINNET]: `https://mainnet.infura.io/v3/${INFURA_KEY}`,
[SupportedChainId.RINKEBY]: `https://rinkeby.infura.io/v3/${INFURA_KEY}`,
[SupportedChainId.ROPSTEN]: `https://ropsten.infura.io/v3/${INFURA_KEY}`,
[SupportedChainId.GOERLI]: `https://goerli.infura.io/v3/${INFURA_KEY}`,
[SupportedChainId.KOVAN]: `https://kovan.infura.io/v3/${INFURA_KEY}`,
[SupportedChainId.OPTIMISM]: `https://optimism-mainnet.infura.io/v3/${INFURA_KEY}`,
[SupportedChainId.OPTIMISTIC_KOVAN]: `https://optimism-kovan.infura.io/v3/${INFURA_KEY}`,
[SupportedChainId.ARBITRUM_ONE]: `https://arbitrum-mainnet.infura.io/v3/${INFURA_KEY}`,
[SupportedChainId.ARBITRUM_RINKEBY]: `https://arbitrum-rinkeby.infura.io/v3/${INFURA_KEY}`,
[SupportedChainId.POLYGON]: `https://polygon-mainnet.infura.io/v3/${INFURA_KEY}`,
[SupportedChainId.POLYGON_MUMBAI]: `https://polygon-mumbai.infura.io/v3/${INFURA_KEY}`,
}

View File

@@ -32,11 +32,11 @@ export const SUPPORTED_LOCALES = [
'vi-VN',
'zh-CN',
'zh-TW',
] as const
]
export type SupportedLocale = typeof SUPPORTED_LOCALES[number] | 'pseudo'
// eslint-disable-next-line import/first
import * as enUS from '../locales/en-US'
import * as enUS from 'locales/en-US'
export const DEFAULT_LOCALE: SupportedLocale = 'en-US'
export const DEFAULT_CATALOG = enUS

View File

@@ -1,4 +1,4 @@
import { AbstractConnector } from '@web3-react/abstract-connector'
import { AbstractConnector } from 'web3-react-abstract-connector'
import INJECTED_ICON_URL from '../assets/images/arrow-right.svg'
import COINBASE_ICON_URL from '../assets/images/coinbaseWalletIcon.svg'

View File

@@ -1,20 +1,15 @@
import { arrayify } from '@ethersproject/bytes'
import { parseBytes32String } from '@ethersproject/strings'
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 { NEVER_RELOAD, useSingleCallResult } from 'lib/hooks/multicall'
import { useCurrencyFromMap, useTokenFromMap } from 'lib/hooks/useCurrency'
import { getTokenFilter } from 'lib/hooks/useTokenList/filtering'
import { useMemo } from 'react'
import { nativeOnChain } from '../constants/tokens'
import { useAllLists, useCombinedActiveList, useInactiveListUrls } from '../state/lists/hooks'
import { WrappedTokenInfo } from '../state/lists/wrappedTokenInfo'
import { useUserAddedTokens } from '../state/user/hooks'
import { isAddress } from '../utils'
import { TokenAddressMap, useUnsupportedTokenList } from './../state/lists/hooks'
import { useBytes32TokenContract, useTokenContract } from './useContract'
// reduce token map into standard address <-> Token mapping, optionally include user added tokens
function useTokensFromMap(tokenMap: TokenAddressMap, includeUserAdded: boolean): { [address: string]: Token } {
@@ -159,95 +154,15 @@ export function useIsUserAddedToken(currency: Currency | undefined | null): bool
return !!userAddedTokens.find((token) => currency.equals(token))
}
// parse a name or symbol from a token response
const BYTES32_REGEX = /^0x[a-fA-F0-9]{64}$/
function parseStringOrBytes32(str: string | undefined, bytes32: string | undefined, defaultValue: string): string {
return str && str.length > 0
? str
: // need to check for proper bytes string and valid terminator
bytes32 && BYTES32_REGEX.test(bytes32) && arrayify(bytes32)[31] === 0
? parseBytes32String(bytes32)
: defaultValue
}
// undefined if invalid or does not exist
// null if loading or null was passed
// otherwise returns the token
export function useToken(tokenAddress?: string | null): Token | undefined | null {
const { chainId } = useActiveWeb3React()
export function useToken(tokenAddress?: string | null): Token | null | undefined {
const tokens = useAllTokens()
const address = isAddress(tokenAddress)
const tokenContract = useTokenContract(address ? address : undefined, false)
const tokenContractBytes32 = useBytes32TokenContract(address ? address : undefined, false)
const token: Token | undefined = address ? tokens[address] : undefined
const tokenName = useSingleCallResult(token ? undefined : tokenContract, 'name', undefined, NEVER_RELOAD)
const tokenNameBytes32 = useSingleCallResult(
token ? undefined : tokenContractBytes32,
'name',
undefined,
NEVER_RELOAD
)
const symbol = useSingleCallResult(token ? undefined : tokenContract, 'symbol', undefined, NEVER_RELOAD)
const symbolBytes32 = useSingleCallResult(token ? undefined : tokenContractBytes32, 'symbol', undefined, NEVER_RELOAD)
const decimals = useSingleCallResult(token ? undefined : tokenContract, 'decimals', undefined, NEVER_RELOAD)
return useMemo(() => {
if (token) return token
if (tokenAddress === null) return null
if (!chainId || !address) return undefined
if (decimals.loading || symbol.loading || tokenName.loading) return null
if (decimals.result) {
return new Token(
chainId,
address,
decimals.result[0],
parseStringOrBytes32(symbol.result?.[0], symbolBytes32.result?.[0], 'UNKNOWN'),
parseStringOrBytes32(tokenName.result?.[0], tokenNameBytes32.result?.[0], 'Unknown Token')
)
}
return undefined
}, [
address,
chainId,
decimals.loading,
decimals.result,
symbol.loading,
symbol.result,
symbolBytes32.result,
token,
tokenAddress,
tokenName.loading,
tokenName.result,
tokenNameBytes32.result,
])
return useTokenFromMap(tokens, tokenAddress)
}
export function useNativeCurrency(): Currency {
const { chainId } = useActiveWeb3React()
return useMemo(
() =>
chainId
? nativeOnChain(chainId)
: // display mainnet when not connected
nativeOnChain(SupportedChainId.MAINNET),
[chainId]
)
}
export function useCurrency(currencyId: string | null | undefined): Currency | null | undefined {
const nativeCurrency = useNativeCurrency()
const isNative = Boolean(nativeCurrency && currencyId?.toUpperCase() === 'ETH')
const token = useToken(isNative ? undefined : currencyId)
if (currencyId === null || currencyId === undefined) return currencyId
// this case so we use our builtin wrapped token instead of wrapped tokens on token lists
const wrappedNative = nativeCurrency?.wrapped
if (wrappedNative?.address?.toUpperCase() === currencyId?.toUpperCase()) return wrappedNative
return isNative ? nativeCurrency : token
export function useCurrency(currencyId?: string | null): Currency | null | undefined {
const tokens = useAllTokens()
return useCurrencyFromMap(tokens, currencyId)
}

View File

@@ -1,19 +1,20 @@
/* eslint-disable react-hooks/rules-of-hooks */
import { Web3Provider } from '@ethersproject/providers'
import { useWeb3React } from '@web3-react/core'
import { default as useWidgetsWeb3React } from 'lib/hooks/useActiveWeb3React'
import { useWeb3React } from 'web3-react-core'
import { NetworkContextName } from '../constants/misc'
export default function useActiveWeb3React() {
const widgetsContext = useWidgetsWeb3React()
if (process.env.REACT_APP_IS_WIDGET) {
return useWidgetsWeb3React()
}
const interfaceContext = useWeb3React<Web3Provider>()
const interfaceNetworkContext = useWeb3React<Web3Provider>(
process.env.REACT_APP_IS_WIDGET ? undefined : NetworkContextName
)
if (process.env.REACT_APP_IS_WIDGET) {
return widgetsContext
}
if (interfaceContext.active) {
return interfaceContext
}

View File

@@ -1,9 +1,8 @@
import { Currency, Token } from '@uniswap/sdk-core'
import useActiveWeb3React from 'hooks/useActiveWeb3React'
import useCurrencyLogoURIs from 'lib/hooks/useCurrencyLogoURIs'
import { useCallback, useState } from 'react'
import { getTokenLogoURL } from './../components/CurrencyLogo/index'
export default function useAddTokenToMetamask(currencyToAdd: Currency | undefined): {
addToken: () => void
success: boolean | undefined
@@ -13,6 +12,7 @@ export default function useAddTokenToMetamask(currencyToAdd: Currency | undefine
const token: Token | undefined = currencyToAdd?.wrapped
const [success, setSuccess] = useState<boolean | undefined>()
const logoURL = useCurrencyLogoURIs(token)[0]
const addToken = useCallback(() => {
if (library && library.provider.isMetaMask && library.provider.request && token) {
@@ -26,7 +26,7 @@ export default function useAddTokenToMetamask(currencyToAdd: Currency | undefine
address: token.address,
symbol: token.symbol,
decimals: token.decimals,
image: getTokenLogoURL(token.address),
image: logoURL,
},
},
})
@@ -37,7 +37,7 @@ export default function useAddTokenToMetamask(currencyToAdd: Currency | undefine
} else {
setSuccess(false)
}
}, [library, token])
}, [library, logoURL, token])
return { addToken, success }
}

View File

@@ -1,69 +1,25 @@
import { MaxUint256 } from '@ethersproject/constants'
import { TransactionResponse } from '@ethersproject/providers'
import { Protocol, Trade } from '@uniswap/router-sdk'
import { Trade } from '@uniswap/router-sdk'
import { Currency, CurrencyAmount, Percent, TradeType } from '@uniswap/sdk-core'
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 useActiveWeb3React from 'hooks/useActiveWeb3React'
import { useCallback, useMemo } from 'react'
import { getTxOptimizedSwapRouter, SwapRouterVersion } from 'utils/getTxOptimizedSwapRouter'
import { Trade as V2Trade } from '@uniswap/v2-sdk'
import { Trade as V3Trade } from '@uniswap/v3-sdk'
import useSwapApproval, { useSwapApprovalOptimizedTrade } from 'lib/hooks/swap/useSwapApproval'
import { ApprovalState, useApproval } from 'lib/hooks/useApproval'
import { useCallback } from 'react'
import { SWAP_ROUTER_ADDRESSES, V2_ROUTER_ADDRESS, V3_ROUTER_ADDRESS } from '../constants/addresses'
import { TransactionType } from '../state/transactions/actions'
import { useHasPendingApproval, useTransactionAdder } from '../state/transactions/hooks'
import { calculateGasMargin } from '../utils/calculateGasMargin'
import { useTokenContract } from './useContract'
import { useTokenAllowance } from './useTokenAllowance'
export { ApprovalState } from 'lib/hooks/useApproval'
export enum ApprovalState {
UNKNOWN = 'UNKNOWN',
NOT_APPROVED = 'NOT_APPROVED',
PENDING = 'PENDING',
APPROVED = 'APPROVED',
}
export function useApprovalState(amountToApprove?: CurrencyAmount<Currency>, spender?: string) {
const { account } = useActiveWeb3React()
const token = amountToApprove?.currency?.isToken ? amountToApprove.currency : undefined
const currentAllowance = useTokenAllowance(token, account ?? undefined, spender)
const pendingApproval = useHasPendingApproval(token?.address, spender)
return useMemo(() => {
if (!amountToApprove || !spender) return ApprovalState.UNKNOWN
if (amountToApprove.currency.isNative) return ApprovalState.APPROVED
// we might not have enough data to know whether or not we need to approve
if (!currentAllowance) return ApprovalState.UNKNOWN
// amountToApprove will be defined if currentAllowance is
return currentAllowance.lessThan(amountToApprove)
? pendingApproval
? ApprovalState.PENDING
: ApprovalState.NOT_APPROVED
: ApprovalState.APPROVED
}, [amountToApprove, currentAllowance, pendingApproval, spender])
}
/** Returns approval state for all known swap routers */
export function useAllApprovalStates(
trade: Trade<Currency, Currency, TradeType> | undefined,
allowedSlippage: Percent
) {
const { chainId } = useActiveWeb3React()
const amountToApprove = useMemo(
() => (trade && trade.inputAmount.currency.isToken ? trade.maximumAmountIn(allowedSlippage) : undefined),
[trade, allowedSlippage]
)
const v2ApprovalState = useApprovalState(amountToApprove, chainId ? V2_ROUTER_ADDRESS[chainId] : undefined)
const v3ApprovalState = useApprovalState(amountToApprove, chainId ? V3_ROUTER_ADDRESS[chainId] : undefined)
const v2V3ApprovalState = useApprovalState(amountToApprove, chainId ? SWAP_ROUTER_ADDRESSES[chainId] : undefined)
return useMemo(
() => ({ v2: v2ApprovalState, v3: v3ApprovalState, v2V3: v2V3ApprovalState }),
[v2ApprovalState, v2V3ApprovalState, v3ApprovalState]
)
function useGetAndTrackApproval(getApproval: ReturnType<typeof useApproval>[1]) {
const addTransaction = useTransactionAdder()
return useCallback(() => {
return getApproval().then((pending) => {
if (pending) {
const { response, tokenAddress, spenderAddress: spender } = pending
addTransaction(response, { type: TransactionType.APPROVAL, tokenAddress, spender })
}
})
}, [addTransaction, getApproval])
}
// returns a variable indicating the state of the approval and a function which approves if necessary or early returns
@@ -71,69 +27,17 @@ export function useApproveCallback(
amountToApprove?: CurrencyAmount<Currency>,
spender?: string
): [ApprovalState, () => Promise<void>] {
const { chainId } = useActiveWeb3React()
const token = amountToApprove?.currency?.isToken ? amountToApprove.currency : undefined
// check the current approval status
const approvalState = useApprovalState(amountToApprove, spender)
const tokenContract = useTokenContract(token?.address)
const addTransaction = useTransactionAdder()
const approve = useCallback(async (): Promise<void> => {
if (approvalState !== ApprovalState.NOT_APPROVED) {
console.error('approve was called unnecessarily')
return
}
if (!chainId) {
console.error('no chainId')
return
}
if (!token) {
console.error('no token')
return
}
if (!tokenContract) {
console.error('tokenContract is null')
return
}
if (!amountToApprove) {
console.error('missing amount to approve')
return
}
if (!spender) {
console.error('no spender')
return
}
let useExact = false
const estimatedGas = await tokenContract.estimateGas.approve(spender, MaxUint256).catch(() => {
// general fallback for tokens who restrict approval amounts
useExact = true
return tokenContract.estimateGas.approve(spender, amountToApprove.quotient.toString())
})
return tokenContract
.approve(spender, useExact ? amountToApprove.quotient.toString() : MaxUint256, {
gasLimit: calculateGasMargin(estimatedGas),
})
.then((response: TransactionResponse) => {
addTransaction(response, { type: TransactionType.APPROVAL, tokenAddress: token.address, spender })
})
.catch((error: Error) => {
console.debug('Failed to approve token', error)
throw error
})
}, [approvalState, token, tokenContract, amountToApprove, spender, addTransaction, chainId])
return [approvalState, approve]
const [approval, getApproval] = useApproval(amountToApprove, spender, useHasPendingApproval)
return [approval, useGetAndTrackApproval(getApproval)]
}
export function useApprovalOptimizedTrade(
trade: Trade<Currency, Currency, TradeType> | undefined,
allowedSlippage: Percent
) {
return useSwapApprovalOptimizedTrade(trade, allowedSlippage, useHasPendingApproval)
}
// wraps useApproveCallback in the context of a swap
export function useApproveCallbackFromTrade(
trade:
| V2Trade<Currency, Currency, TradeType>
@@ -141,85 +45,7 @@ export function useApproveCallbackFromTrade(
| Trade<Currency, Currency, TradeType>
| undefined,
allowedSlippage: Percent
) {
const { chainId } = useActiveWeb3React()
const amountToApprove = useMemo(
() => (trade && trade.inputAmount.currency.isToken ? trade.maximumAmountIn(allowedSlippage) : undefined),
[trade, allowedSlippage]
)
const approveCallback = useApproveCallback(
amountToApprove,
chainId
? trade instanceof V2Trade
? V2_ROUTER_ADDRESS[chainId]
: trade instanceof V3Trade
? V3_ROUTER_ADDRESS[chainId]
: SWAP_ROUTER_ADDRESSES[chainId]
: undefined
)
// TODO: remove L162-168 after testing is done. This error will help detect mistakes in the logic.
if (
(Trade instanceof V2Trade && approveCallback[0] !== ApprovalState.APPROVED) ||
(trade instanceof V3Trade && approveCallback[0] !== ApprovalState.APPROVED)
) {
throw new Error('Trying to approve legacy router')
}
return approveCallback
}
export function useApprovalOptimizedTrade(
trade: Trade<Currency, Currency, TradeType> | undefined,
allowedSlippage: Percent
):
| V2Trade<Currency, Currency, TradeType>
| V3Trade<Currency, Currency, TradeType>
| Trade<Currency, Currency, TradeType>
| undefined {
const onlyV2Routes = trade?.routes.every((route) => route.protocol === Protocol.V2)
const onlyV3Routes = trade?.routes.every((route) => route.protocol === Protocol.V3)
const tradeHasSplits = (trade?.routes.length ?? 0) > 1
const approvalStates = useAllApprovalStates(trade, allowedSlippage)
const optimizedSwapRouter = useMemo(
() => getTxOptimizedSwapRouter({ onlyV2Routes, onlyV3Routes, tradeHasSplits, approvalStates }),
[approvalStates, tradeHasSplits, onlyV2Routes, onlyV3Routes]
)
return useMemo(() => {
if (!trade) return undefined
try {
switch (optimizedSwapRouter) {
case SwapRouterVersion.V2V3:
return trade
case SwapRouterVersion.V2:
const pairs = trade.swaps[0].route.pools.filter((pool) => pool instanceof Pair) as Pair[]
const v2Route = new V2Route(pairs, trade.inputAmount.currency, trade.outputAmount.currency)
return new V2Trade(v2Route, trade.inputAmount, trade.tradeType)
case SwapRouterVersion.V3:
return V3Trade.createUncheckedTradeWithMultipleRoutes({
routes: trade.swaps.map(({ route, inputAmount, outputAmount }) => ({
route: new V3Route(
route.pools.filter((p) => p instanceof Pool) as Pool[],
inputAmount.currency,
outputAmount.currency
),
inputAmount,
outputAmount,
})),
tradeType: trade.tradeType,
})
default:
return undefined
}
} catch (e) {
// TODO(#2989): remove try-catch
console.debug(e)
return undefined
}
}, [trade, optimizedSwapRouter])
): [ApprovalState, () => Promise<void>] {
const [approval, getApproval] = useSwapApproval(trade, allowedSlippage, useHasPendingApproval)
return [approval, useGetAndTrackApproval(getApproval)]
}

View File

@@ -1,5 +1,5 @@
import useActiveWeb3React from 'hooks/useActiveWeb3React'
import { AUTO_ROUTER_SUPPORTED_CHAINS } from 'state/routing/clientSideSmartOrderRouter/constants'
import { AUTO_ROUTER_SUPPORTED_CHAINS } from 'lib/hooks/routing/clientSideSmartOrderRouter'
export default function useAutoRouterSupported(): boolean {
const { chainId } = useActiveWeb3React()

View File

@@ -4,11 +4,10 @@ import { SUPPORTED_GAS_ESTIMATE_CHAIN_IDS } from 'constants/chains'
import { L2_CHAIN_IDS } from 'constants/chains'
import useActiveWeb3React from 'hooks/useActiveWeb3React'
import JSBI from 'jsbi'
import useNativeCurrency from 'lib/hooks/useNativeCurrency'
import { useMemo } from 'react'
import { InterfaceTrade } from 'state/routing/types'
import { useUserSlippageToleranceWithDefault } from '../state/user/hooks'
import { useNativeCurrency } from './Tokens'
import useGasPrice from './useGasPrice'
import useUSDCPrice, { useUSDCValue } from './useUSDCPrice'
@@ -29,7 +28,10 @@ function guesstimateGas(trade: Trade<Currency, Currency, TradeType> | undefined)
const MIN_AUTO_SLIPPAGE_TOLERANCE = new Percent(5, 1000) // 0.5%
const MAX_AUTO_SLIPPAGE_TOLERANCE = new Percent(25, 100) // 25%
export default function useSwapSlippageTolerance(
/**
* Returns slippage tolerance based on values from current trade, gas estimates from api, and active network.
*/
export default function useAutoSlippageTolerance(
trade: InterfaceTrade<Currency, Currency, TradeType> | undefined
): Percent {
const { chainId } = useActiveWeb3React()
@@ -41,7 +43,7 @@ export default function useSwapSlippageTolerance(
const nativeCurrency = useNativeCurrency()
const nativeCurrencyPrice = useUSDCPrice(nativeCurrency ?? undefined)
const defaultSlippageTolerance = useMemo(() => {
return useMemo(() => {
if (!trade || onL2) return ONE_TENTHS_PERCENT
const nativeGasCost =
@@ -73,6 +75,4 @@ export default function useSwapSlippageTolerance(
return V3_SWAP_DEFAULT_SLIPPAGE
}, [trade, onL2, nativeGasPrice, gasEstimate, nativeCurrency, nativeCurrencyPrice, chainId, outputDollarValue])
return useUserSlippageToleranceWithDefault(defaultSlippageTolerance)
}

View File

@@ -1,4 +1,5 @@
import { Currency, CurrencyAmount, TradeType } from '@uniswap/sdk-core'
import { useMemo } from 'react'
import { InterfaceTrade, TradeState } from 'state/routing/types'
import { useRoutingAPITrade } from 'state/routing/useRoutingAPITrade'
@@ -24,7 +25,10 @@ export function useBestTrade(
const autoRouterSupported = useAutoRouterSupported()
const isWindowVisible = useIsWindowVisible()
const [debouncedAmount, debouncedOtherCurrency] = useDebounce([amountSpecified, otherCurrency], 200)
const [debouncedAmount, debouncedOtherCurrency] = useDebounce(
useMemo(() => [amountSpecified, otherCurrency], [amountSpecified, otherCurrency]),
200
)
const routingAPITrade = useRoutingAPITrade(
tradeType,
@@ -56,9 +60,12 @@ export function useBestTrade(
)
// only return gas estimate from api if routing api trade is used
return {
...(useFallback ? bestV3Trade : routingAPITrade),
...(debouncing ? { state: TradeState.SYNCING } : {}),
...(isLoading ? { state: TradeState.LOADING } : {}),
}
return useMemo(
() => ({
...(useFallback ? bestV3Trade : routingAPITrade),
...(debouncing ? { state: TradeState.SYNCING } : {}),
...(isLoading ? { state: TradeState.LOADING } : {}),
}),
[bestV3Trade, debouncing, isLoading, routingAPITrade, useFallback]
)
}

View File

@@ -1,14 +1,11 @@
import { Contract } from '@ethersproject/contracts'
import { abi as GOVERNANCE_ABI } from '@uniswap/governance/build/GovernorAlpha.json'
import { abi as UNI_ABI } from '@uniswap/governance/build/Uni.json'
import { abi as STAKING_REWARDS_ABI } from '@uniswap/liquidity-staker/build/StakingRewards.json'
import { abi as MERKLE_DISTRIBUTOR_ABI } from '@uniswap/merkle-distributor/build/MerkleDistributor.json'
import { abi as IUniswapV2PairABI } from '@uniswap/v2-core/build/IUniswapV2Pair.json'
import { abi as IUniswapV2Router02ABI } from '@uniswap/v2-periphery/build/IUniswapV2Router02.json'
import { abi as QuoterABI } from '@uniswap/v3-periphery/artifacts/contracts/lens/Quoter.sol/Quoter.json'
import { abi as MulticallABI } from '@uniswap/v3-periphery/artifacts/contracts/lens/UniswapInterfaceMulticall.sol/UniswapInterfaceMulticall.json'
import { abi as NFTPositionManagerABI } from '@uniswap/v3-periphery/artifacts/contracts/NonfungiblePositionManager.sol/NonfungiblePositionManager.json'
import { abi as V2MigratorABI } from '@uniswap/v3-periphery/artifacts/contracts/V3Migrator.sol/V3Migrator.json'
import IUniswapV2PairJson from '@uniswap/v2-core/build/IUniswapV2Pair.json'
import IUniswapV2Router02Json from '@uniswap/v2-periphery/build/IUniswapV2Router02.json'
import QuoterJson from '@uniswap/v3-periphery/artifacts/contracts/lens/Quoter.sol/Quoter.json'
import TickLensJson from '@uniswap/v3-periphery/artifacts/contracts/lens/TickLens.sol/TickLens.json'
import UniswapInterfaceMulticallJson from '@uniswap/v3-periphery/artifacts/contracts/lens/UniswapInterfaceMulticall.sol/UniswapInterfaceMulticall.json'
import NonfungiblePositionManagerJson from '@uniswap/v3-periphery/artifacts/contracts/NonfungiblePositionManager.sol/NonfungiblePositionManager.json'
import V3MigratorJson from '@uniswap/v3-periphery/artifacts/contracts/V3Migrator.sol/V3Migrator.json'
import ARGENT_WALLET_DETECTOR_ABI from 'abis/argent-wallet-detector.json'
import EIP_2612 from 'abis/eip_2612.json'
import ENS_PUBLIC_RESOLVER_ABI from 'abis/ens-public-resolver.json'
@@ -17,30 +14,34 @@ import ERC20_ABI from 'abis/erc20.json'
import ERC20_BYTES32_ABI from 'abis/erc20_bytes32.json'
import ERC721_ABI from 'abis/erc721.json'
import ERC1155_ABI from 'abis/erc1155.json'
import GOVERNOR_BRAVO_ABI from 'abis/governor-bravo.json'
import { ArgentWalletDetector, EnsPublicResolver, EnsRegistrar, Erc20, Erc721, Erc1155, Weth } from 'abis/types'
import WETH_ABI from 'abis/weth.json'
import {
ARGENT_WALLET_DETECTOR_ADDRESS,
ENS_REGISTRAR_ADDRESSES,
GOVERNANCE_ALPHA_V0_ADDRESSES,
GOVERNANCE_ALPHA_V1_ADDRESSES,
GOVERNANCE_BRAVO_ADDRESSES,
MERKLE_DISTRIBUTOR_ADDRESS,
MULTICALL_ADDRESS,
NONFUNGIBLE_POSITION_MANAGER_ADDRESSES,
QUOTER_ADDRESSES,
TICK_LENS_ADDRESSES,
V2_ROUTER_ADDRESS,
V3_MIGRATOR_ADDRESSES,
} from 'constants/addresses'
import { UNI, WRAPPED_NATIVE_CURRENCY } from 'constants/tokens'
import { WRAPPED_NATIVE_CURRENCY } from 'constants/tokens'
import useActiveWeb3React from 'hooks/useActiveWeb3React'
import { useMemo } from 'react'
import { NonfungiblePositionManager, Quoter, UniswapInterfaceMulticall } from 'types/v3'
import { NonfungiblePositionManager, Quoter, TickLens, UniswapInterfaceMulticall } from 'types/v3'
import { V3Migrator } from 'types/v3/V3Migrator'
import { getContract } from '../utils'
const { abi: IUniswapV2PairABI } = IUniswapV2PairJson
const { abi: IUniswapV2Router02ABI } = IUniswapV2Router02Json
const { abi: QuoterABI } = QuoterJson
const { abi: TickLensABI } = TickLensJson
const { abi: MulticallABI } = UniswapInterfaceMulticallJson
const { abi: NFTPositionManagerABI } = NonfungiblePositionManagerJson
const { abi: V2MigratorABI } = V3MigratorJson
// returns null on errors
export function useContract<T extends Contract = Contract>(
addressOrAddressMap: string | { [chainId: number]: string } | undefined,
@@ -121,33 +122,6 @@ export function useInterfaceMulticall() {
return useContract<UniswapInterfaceMulticall>(MULTICALL_ADDRESS, MulticallABI, false) as UniswapInterfaceMulticall
}
export function useMerkleDistributorContract() {
return useContract(MERKLE_DISTRIBUTOR_ADDRESS, MERKLE_DISTRIBUTOR_ABI, true)
}
export function useGovernanceV0Contract(): Contract | null {
return useContract(GOVERNANCE_ALPHA_V0_ADDRESSES, GOVERNANCE_ABI, false)
}
export function useGovernanceV1Contract(): Contract | null {
return useContract(GOVERNANCE_ALPHA_V1_ADDRESSES, GOVERNANCE_ABI, false)
}
export function useGovernanceBravoContract(): Contract | null {
return useContract(GOVERNANCE_BRAVO_ADDRESSES, GOVERNOR_BRAVO_ABI, true)
}
export const useLatestGovernanceContract = useGovernanceBravoContract
export function useUniContract() {
const { chainId } = useActiveWeb3React()
return useContract(chainId ? UNI[chainId]?.address : undefined, UNI_ABI, true)
}
export function useStakingContract(stakingAddress?: string, withSignerIfPossible?: boolean) {
return useContract(stakingAddress, STAKING_REWARDS_ABI, withSignerIfPossible)
}
export function useV3NFTPositionManagerContract(withSignerIfPossible?: boolean): NonfungiblePositionManager | null {
return useContract<NonfungiblePositionManager>(
NONFUNGIBLE_POSITION_MANAGER_ADDRESSES,
@@ -159,3 +133,9 @@ export function useV3NFTPositionManagerContract(withSignerIfPossible?: boolean):
export function useV3Quoter() {
return useContract<Quoter>(QUOTER_ADDRESSES, QuoterABI)
}
export function useTickLens(): TickLens | null {
const { chainId } = useActiveWeb3React()
const address = chainId ? TICK_LENS_ADDRESSES[chainId] : undefined
return useContract(address, TickLensABI) as TickLens | null
}

View File

@@ -1,5 +1,9 @@
import { useEffect, useState } from 'react'
/**
* Debounces updates to a value.
* Non-primitives *must* wrap the value in useMemo, or the value will be updated due to referential inequality.
*/
// modified from https://usehooks.com/useDebounce/
export default function useDebounce<T>(value: T, delay: number): T {
const [debouncedValue, setDebouncedValue] = useState<T>(value)

View File

@@ -1,6 +1,7 @@
import { BigNumber } from '@ethersproject/bignumber'
import { splitSignature } from '@ethersproject/bytes'
import { Trade } from '@uniswap/router-sdk'
import { Currency, CurrencyAmount, Percent, Token, TradeType } from '@uniswap/sdk-core'
import { Currency, CurrencyAmount, Percent, TradeType } from '@uniswap/sdk-core'
import { Trade as V2Trade } from '@uniswap/v2-sdk'
import { Trade as V3Trade } from '@uniswap/v3-sdk'
import useActiveWeb3React from 'hooks/useActiveWeb3React'
@@ -12,9 +13,8 @@ import { SWAP_ROUTER_ADDRESSES, V3_ROUTER_ADDRESS } from '../constants/addresses
import { DAI, UNI, USDC } from '../constants/tokens'
import { useEIP2612Contract } from './useContract'
import useIsArgentWallet from './useIsArgentWallet'
import useTransactionDeadline from './useTransactionDeadline'
enum PermitType {
export enum PermitType {
AMOUNT = 1,
ALLOWED = 2,
}
@@ -22,7 +22,7 @@ enum PermitType {
// 20 minutes to submit after signing
const PERMIT_VALIDITY_BUFFER = 20 * 60
interface PermitInfo {
export interface PermitInfo {
type: PermitType
name: string
// version is optional, and if omitted, will not be included in the domain
@@ -116,9 +116,10 @@ const PERMIT_ALLOWED_TYPE = [
{ name: 'allowed', type: 'bool' },
]
function useERC20Permit(
export function useERC20Permit(
currencyAmount: CurrencyAmount<Currency> | null | undefined,
spender: string | null | undefined,
transactionDeadline: BigNumber | undefined,
overridePermitInfo: PermitInfo | undefined | null
): {
signatureData: SignatureData | null
@@ -126,7 +127,6 @@ function useERC20Permit(
gatherPermitSignature: null | (() => Promise<void>)
} {
const { account, chainId, library } = useActiveWeb3React()
const transactionDeadline = useTransactionDeadline()
const tokenAddress = currencyAmount?.currency?.isToken ? currencyAmount.currency.address : undefined
const eip2612Contract = useEIP2612Contract(tokenAddress)
const isArgentWallet = useIsArgentWallet()
@@ -259,26 +259,14 @@ function useERC20Permit(
])
}
const REMOVE_V2_LIQUIDITY_PERMIT_INFO: PermitInfo = {
version: '1',
name: 'Uniswap V2',
type: PermitType.AMOUNT,
}
export function useV2LiquidityTokenPermit(
liquidityAmount: CurrencyAmount<Token> | null | undefined,
spender: string | null | undefined
) {
return useERC20Permit(liquidityAmount, spender, REMOVE_V2_LIQUIDITY_PERMIT_INFO)
}
export function useERC20PermitFromTrade(
trade:
| V2Trade<Currency, Currency, TradeType>
| V3Trade<Currency, Currency, TradeType>
| Trade<Currency, Currency, TradeType>
| undefined,
allowedSlippage: Percent
allowedSlippage: Percent,
transactionDeadline: BigNumber | undefined
) {
const { chainId } = useActiveWeb3React()
const swapRouterAddress = chainId
@@ -294,5 +282,5 @@ export function useERC20PermitFromTrade(
[trade, allowedSlippage]
)
return useERC20Permit(amountToApprove, swapRouterAddress, null)
return useERC20Permit(amountToApprove, swapRouterAddress, transactionDeadline, null)
}

View File

@@ -1,30 +1,141 @@
import { skipToken } from '@reduxjs/toolkit/query/react'
import { Currency } from '@uniswap/sdk-core'
import { FeeAmount, Pool, TICK_SPACINGS, tickToPrice } from '@uniswap/v3-sdk'
import { ChainId } from '@uniswap/smart-order-router'
import { FeeAmount, nearestUsableTick, Pool, TICK_SPACINGS, tickToPrice } from '@uniswap/v3-sdk'
import { ZERO_ADDRESS } from 'constants/misc'
import JSBI from 'jsbi'
import { useSingleContractMultipleData } from 'lib/hooks/multicall'
import ms from 'ms.macro'
import { useMemo } from 'react'
import { useEffect, useMemo, useState } from 'react'
import { useAllV3TicksQuery } from 'state/data/enhanced'
import { AllV3TicksQuery } from 'state/data/generated'
import computeSurroundingTicks from 'utils/computeSurroundingTicks'
import { useTickLens } from './useContract'
import { PoolState, usePool } from './usePools'
const PRICE_FIXED_DIGITS = 8
const CHAIN_IDS_MISSING_SUBGRAPH_DATA = [ChainId.ARBITRUM_ONE, ChainId.ARBITRUM_RINKEBY]
export interface TickData {
tick: number
liquidityNet: JSBI
liquidityGross: JSBI
}
// Tick with fields parsed to JSBIs, and active liquidity computed.
export interface TickProcessed {
tickIdx: number
tick: number
liquidityActive: JSBI
liquidityNet: JSBI
price0: string
}
const REFRESH_FREQUENCY = { blocksPerFetch: 2 }
const getActiveTick = (tickCurrent: number | undefined, feeAmount: FeeAmount | undefined) =>
tickCurrent && feeAmount ? Math.floor(tickCurrent / TICK_SPACINGS[feeAmount]) * TICK_SPACINGS[feeAmount] : undefined
// Fetches all ticks for a given pool
export function useAllV3Ticks(
const bitmapIndex = (tick: number, tickSpacing: number) => {
return Math.floor(tick / tickSpacing / 256)
}
function useTicksFromTickLens(
currencyA: Currency | undefined,
currencyB: Currency | undefined,
feeAmount: FeeAmount | undefined,
numSurroundingTicks: number | undefined = 125
) {
const [tickDataLatestSynced, setTickDataLatestSynced] = useState<TickData[]>([])
const [poolState, pool] = usePool(currencyA, currencyB, feeAmount)
const tickSpacing = feeAmount && TICK_SPACINGS[feeAmount]
// Find nearest valid tick for pool in case tick is not initialized.
const activeTick = pool?.tickCurrent && tickSpacing ? nearestUsableTick(pool?.tickCurrent, tickSpacing) : undefined
const poolAddress =
currencyA && currencyB && feeAmount && poolState === PoolState.EXISTS
? Pool.getAddress(currencyA?.wrapped, currencyB?.wrapped, feeAmount)
: undefined
// it is also possible to grab all tick data but it is extremely slow
// bitmapIndex(nearestUsableTick(TickMath.MIN_TICK, tickSpacing), tickSpacing)
const minIndex = useMemo(
() =>
tickSpacing && activeTick ? bitmapIndex(activeTick - numSurroundingTicks * tickSpacing, tickSpacing) : undefined,
[tickSpacing, activeTick, numSurroundingTicks]
)
const maxIndex = useMemo(
() =>
tickSpacing && activeTick ? bitmapIndex(activeTick + numSurroundingTicks * tickSpacing, tickSpacing) : undefined,
[tickSpacing, activeTick, numSurroundingTicks]
)
const tickLensArgs: [string, number][] = useMemo(
() =>
maxIndex && minIndex && poolAddress && poolAddress !== ZERO_ADDRESS
? new Array(maxIndex - minIndex + 1)
.fill(0)
.map((_, i) => i + minIndex)
.map((wordIndex) => [poolAddress, wordIndex])
: [],
[minIndex, maxIndex, poolAddress]
)
const tickLens = useTickLens()
const callStates = useSingleContractMultipleData(
tickLensArgs.length > 0 ? tickLens : undefined,
'getPopulatedTicksInWord',
tickLensArgs,
REFRESH_FREQUENCY
)
const isError = useMemo(() => callStates.some(({ error }) => error), [callStates])
const isLoading = useMemo(() => callStates.some(({ loading }) => loading), [callStates])
const IsSyncing = useMemo(() => callStates.some(({ syncing }) => syncing), [callStates])
const isValid = useMemo(() => callStates.some(({ valid }) => valid), [callStates])
const tickData: TickData[] = useMemo(
() =>
callStates
.map(({ result }) => result?.populatedTicks)
.reduce(
(accumulator, current) => [
...accumulator,
...(current?.map((tickData: TickData) => {
return {
tick: tickData.tick,
liquidityNet: JSBI.BigInt(tickData.liquidityNet),
liquidityGross: JSBI.BigInt(tickData.liquidityGross),
}
}) ?? []),
],
[]
),
[callStates]
)
// reset on input change
useEffect(() => {
setTickDataLatestSynced([])
}, [currencyA, currencyB, feeAmount])
// return the latest synced tickData even if we are still loading the newest data
useEffect(() => {
if (!IsSyncing && !isLoading && !isError && isValid) {
setTickDataLatestSynced(tickData.sort((a, b) => a.tick - b.tick))
}
}, [isError, isLoading, IsSyncing, tickData, isValid])
return useMemo(
() => ({ isLoading, IsSyncing, isError, isValid, tickData: tickDataLatestSynced }),
[isLoading, IsSyncing, isError, isValid, tickDataLatestSynced]
)
}
function useTicksFromSubgraph(
currencyA: Currency | undefined,
currencyB: Currency | undefined,
feeAmount: FeeAmount | undefined
@@ -32,19 +143,34 @@ export function useAllV3Ticks(
const poolAddress =
currencyA && currencyB && feeAmount ? Pool.getAddress(currencyA?.wrapped, currencyB?.wrapped, feeAmount) : undefined
const { isLoading, isError, error, isUninitialized, data } = useAllV3TicksQuery(
poolAddress ? { poolAddress: poolAddress?.toLowerCase(), skip: 0 } : skipToken,
{
pollingInterval: ms`30s`,
}
)
return useAllV3TicksQuery(poolAddress ? { poolAddress: poolAddress?.toLowerCase(), skip: 0 } : skipToken, {
pollingInterval: ms`30s`,
})
}
// Fetches all ticks for a given pool
function useAllV3Ticks(
currencyA: Currency | undefined,
currencyB: Currency | undefined,
feeAmount: FeeAmount | undefined
): {
isLoading: boolean
isUninitialized: boolean
isError: boolean
error: unknown
ticks: TickData[] | undefined
} {
const useSubgraph = currencyA ? !CHAIN_IDS_MISSING_SUBGRAPH_DATA.includes(currencyA.chainId) : true
const tickLensTickData = useTicksFromTickLens(!useSubgraph ? currencyA : undefined, currencyB, feeAmount)
const subgraphTickData = useTicksFromSubgraph(useSubgraph ? currencyA : undefined, currencyB, feeAmount)
return {
isLoading,
isUninitialized,
isError,
error,
ticks: data?.ticks as AllV3TicksQuery['ticks'],
isLoading: useSubgraph ? subgraphTickData.isLoading : tickLensTickData.isLoading,
isUninitialized: useSubgraph ? subgraphTickData.isUninitialized : false,
isError: useSubgraph ? subgraphTickData.isError : tickLensTickData.isError,
error: useSubgraph ? subgraphTickData.error : tickLensTickData.isError,
ticks: useSubgraph ? subgraphTickData.data?.ticks : tickLensTickData.tickData,
}
}
@@ -94,7 +220,7 @@ export function usePoolActiveLiquidity(
// find where the active tick would be to partition the array
// if the active tick is initialized, the pivot will be an element
// if not, take the previous tick as pivot
const pivot = ticks.findIndex(({ tickIdx }) => tickIdx > activeTick) - 1
const pivot = ticks.findIndex(({ tick }) => tick > activeTick) - 1
if (pivot < 0) {
// consider setting a local error
@@ -111,9 +237,8 @@ export function usePoolActiveLiquidity(
const activeTickProcessed: TickProcessed = {
liquidityActive: JSBI.BigInt(pool[1]?.liquidity ?? 0),
tickIdx: activeTick,
liquidityNet:
Number(ticks[pivot].tickIdx) === activeTick ? JSBI.BigInt(ticks[pivot].liquidityNet) : JSBI.BigInt(0),
tick: activeTick,
liquidityNet: Number(ticks[pivot].tick) === activeTick ? JSBI.BigInt(ticks[pivot].liquidityNet) : JSBI.BigInt(0),
price0: tickToPrice(token0, token1, activeTick).toFixed(PRICE_FIXED_DIGITS),
}

View File

@@ -1,6 +1,6 @@
import { Interface } from '@ethersproject/abi'
import { Currency, Token } from '@uniswap/sdk-core'
import { abi as IUniswapV3PoolStateABI } from '@uniswap/v3-core/artifacts/contracts/interfaces/pool/IUniswapV3PoolState.sol/IUniswapV3PoolState.json'
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'
@@ -10,6 +10,8 @@ import { useMemo } from 'react'
import { V3_CORE_FACTORY_ADDRESSES } from '../constants/addresses'
import { IUniswapV3PoolStateInterface } from '../types/v3/IUniswapV3PoolState'
const { abi: IUniswapV3PoolStateABI } = IUniswapV3PoolStateJson
const POOL_STATE_INTERFACE = new Interface(IUniswapV3PoolStateABI) as IUniswapV3PoolStateInterface
export enum PoolState {

View File

@@ -0,0 +1,184 @@
import { BigNumber } from '@ethersproject/bignumber'
import { SwapRouter, Trade } from '@uniswap/router-sdk'
import { Currency, Percent, TradeType } from '@uniswap/sdk-core'
import { Router as V2SwapRouter, Trade as V2Trade } from '@uniswap/v2-sdk'
import { FeeOptions, SwapRouter as V3SwapRouter, Trade as V3Trade } from '@uniswap/v3-sdk'
import { SWAP_ROUTER_ADDRESSES, V3_ROUTER_ADDRESS } from 'constants/addresses'
import useActiveWeb3React from 'hooks/useActiveWeb3React'
import { useMemo } from 'react'
import approveAmountCalldata from 'utils/approveAmountCalldata'
import { useArgentWalletContract } from './useArgentWalletContract'
import { useV2RouterContract } from './useContract'
import useENS from './useENS'
import { SignatureData } from './useERC20Permit'
export type AnyTrade =
| V2Trade<Currency, Currency, TradeType>
| V3Trade<Currency, Currency, TradeType>
| Trade<Currency, Currency, TradeType>
interface SwapCall {
address: string
calldata: string
value: string
}
/**
* Returns the swap calls that can be used to make the trade
* @param trade trade to execute
* @param allowedSlippage user allowed slippage
* @param recipientAddressOrName the ENS name or address of the recipient of the swap output
* @param signatureData the signature data of the permit of the input token amount, if available
*/
export function useSwapCallArguments(
trade: AnyTrade | undefined,
allowedSlippage: Percent,
recipientAddressOrName: string | null | undefined,
signatureData: SignatureData | null | undefined,
deadline: BigNumber | undefined,
feeOptions: FeeOptions | undefined
): SwapCall[] {
const { account, chainId, library } = useActiveWeb3React()
const { address: recipientAddress } = useENS(recipientAddressOrName)
const recipient = recipientAddressOrName === null ? account : recipientAddress
const routerContract = useV2RouterContract()
const argentWalletContract = useArgentWalletContract()
return useMemo(() => {
if (!trade || !recipient || !library || !account || !chainId || !deadline) return []
if (trade instanceof V2Trade) {
if (!routerContract) return []
const swapMethods = []
swapMethods.push(
V2SwapRouter.swapCallParameters(trade, {
feeOnTransfer: false,
allowedSlippage,
recipient,
deadline: deadline.toNumber(),
})
)
if (trade.tradeType === TradeType.EXACT_INPUT) {
swapMethods.push(
V2SwapRouter.swapCallParameters(trade, {
feeOnTransfer: true,
allowedSlippage,
recipient,
deadline: deadline.toNumber(),
})
)
}
return swapMethods.map(({ methodName, args, value }) => {
if (argentWalletContract && trade.inputAmount.currency.isToken) {
return {
address: argentWalletContract.address,
calldata: argentWalletContract.interface.encodeFunctionData('wc_multiCall', [
[
approveAmountCalldata(trade.maximumAmountIn(allowedSlippage), routerContract.address),
{
to: routerContract.address,
value,
data: routerContract.interface.encodeFunctionData(methodName, args),
},
],
]),
value: '0x0',
}
} else {
return {
address: routerContract.address,
calldata: routerContract.interface.encodeFunctionData(methodName, args),
value,
}
}
})
} else {
// swap options shared by v3 and v2+v3 swap routers
const sharedSwapOptions = {
fee: feeOptions,
recipient,
slippageTolerance: allowedSlippage,
...(signatureData
? {
inputTokenPermit:
'allowed' in signatureData
? {
expiry: signatureData.deadline,
nonce: signatureData.nonce,
s: signatureData.s,
r: signatureData.r,
v: signatureData.v as any,
}
: {
deadline: signatureData.deadline,
amount: signatureData.amount,
s: signatureData.s,
r: signatureData.r,
v: signatureData.v as any,
},
}
: {}),
}
const swapRouterAddress = chainId
? trade instanceof V3Trade
? V3_ROUTER_ADDRESS[chainId]
: SWAP_ROUTER_ADDRESSES[chainId]
: undefined
if (!swapRouterAddress) return []
const { value, calldata } =
trade instanceof V3Trade
? V3SwapRouter.swapCallParameters(trade, {
...sharedSwapOptions,
deadline: deadline.toString(),
})
: SwapRouter.swapCallParameters(trade, {
...sharedSwapOptions,
deadlineOrPreviousBlockhash: deadline.toString(),
})
if (argentWalletContract && trade.inputAmount.currency.isToken) {
return [
{
address: argentWalletContract.address,
calldata: argentWalletContract.interface.encodeFunctionData('wc_multiCall', [
[
approveAmountCalldata(trade.maximumAmountIn(allowedSlippage), swapRouterAddress),
{
to: swapRouterAddress,
value,
data: calldata,
},
],
]),
value: '0x0',
},
]
}
return [
{
address: swapRouterAddress,
calldata,
value,
},
]
}
}, [
account,
allowedSlippage,
argentWalletContract,
chainId,
deadline,
feeOptions,
library,
recipient,
routerContract,
signatureData,
trade,
])
}

View File

@@ -1,289 +1,17 @@
import { BigNumber } from '@ethersproject/bignumber'
// eslint-disable-next-line no-restricted-imports
import { t, Trans } from '@lingui/macro'
import { SwapRouter, Trade } from '@uniswap/router-sdk'
import { Currency, Percent, TradeType } from '@uniswap/sdk-core'
import { Router as V2SwapRouter, Trade as V2Trade } from '@uniswap/v2-sdk'
import { SwapRouter as V3SwapRouter, Trade as V3Trade } from '@uniswap/v3-sdk'
import { Percent, TradeType } from '@uniswap/sdk-core'
import useActiveWeb3React from 'hooks/useActiveWeb3React'
import { SwapCallbackState, useSwapCallback as useLibSwapCallBack } from 'lib/hooks/swap/useSwapCallback'
import { ReactNode, useMemo } from 'react'
import { SWAP_ROUTER_ADDRESSES, V3_ROUTER_ADDRESS } from '../constants/addresses'
import { TransactionType } from '../state/transactions/actions'
import { useTransactionAdder } from '../state/transactions/hooks'
import approveAmountCalldata from '../utils/approveAmountCalldata'
import { calculateGasMargin } from '../utils/calculateGasMargin'
import { currencyId } from '../utils/currencyId'
import isZero from '../utils/isZero'
import { useArgentWalletContract } from './useArgentWalletContract'
import { useV2RouterContract } from './useContract'
import useENS from './useENS'
import { SignatureData } from './useERC20Permit'
import { AnyTrade } from './useSwapCallArguments'
import useTransactionDeadline from './useTransactionDeadline'
type AnyTrade =
| V2Trade<Currency, Currency, TradeType>
| V3Trade<Currency, Currency, TradeType>
| Trade<Currency, Currency, TradeType>
enum SwapCallbackState {
INVALID,
LOADING,
VALID,
}
interface SwapCall {
address: string
calldata: string
value: string
}
interface SwapCallEstimate {
call: SwapCall
}
interface SuccessfulCall extends SwapCallEstimate {
call: SwapCall
gasEstimate: BigNumber
}
interface FailedCall extends SwapCallEstimate {
call: SwapCall
error: Error
}
/**
* Returns the swap calls that can be used to make the trade
* @param trade trade to execute
* @param allowedSlippage user allowed slippage
* @param recipientAddressOrName the ENS name or address of the recipient of the swap output
* @param signatureData the signature data of the permit of the input token amount, if available
*/
function useSwapCallArguments(
trade: AnyTrade | undefined, // trade to execute, required
allowedSlippage: Percent, // in bips
recipientAddressOrName: string | null, // the ENS name or address of the recipient of the trade, or null if swap should be returned to sender
signatureData: SignatureData | null | undefined
): SwapCall[] {
const { account, chainId, library } = useActiveWeb3React()
const { address: recipientAddress } = useENS(recipientAddressOrName)
const recipient = recipientAddressOrName === null ? account : recipientAddress
const deadline = useTransactionDeadline()
const routerContract = useV2RouterContract()
const argentWalletContract = useArgentWalletContract()
return useMemo(() => {
if (!trade || !recipient || !library || !account || !chainId || !deadline) return []
if (trade instanceof V2Trade) {
if (!routerContract) return []
const swapMethods = []
swapMethods.push(
V2SwapRouter.swapCallParameters(trade, {
feeOnTransfer: false,
allowedSlippage,
recipient,
deadline: deadline.toNumber(),
})
)
if (trade.tradeType === TradeType.EXACT_INPUT) {
swapMethods.push(
V2SwapRouter.swapCallParameters(trade, {
feeOnTransfer: true,
allowedSlippage,
recipient,
deadline: deadline.toNumber(),
})
)
}
return swapMethods.map(({ methodName, args, value }) => {
if (argentWalletContract && trade.inputAmount.currency.isToken) {
return {
address: argentWalletContract.address,
calldata: argentWalletContract.interface.encodeFunctionData('wc_multiCall', [
[
approveAmountCalldata(trade.maximumAmountIn(allowedSlippage), routerContract.address),
{
to: routerContract.address,
value,
data: routerContract.interface.encodeFunctionData(methodName, args),
},
],
]),
value: '0x0',
}
} else {
return {
address: routerContract.address,
calldata: routerContract.interface.encodeFunctionData(methodName, args),
value,
}
}
})
} else {
// swap options shared by v3 and v2+v3 swap routers
const sharedSwapOptions = {
recipient,
slippageTolerance: allowedSlippage,
...(signatureData
? {
inputTokenPermit:
'allowed' in signatureData
? {
expiry: signatureData.deadline,
nonce: signatureData.nonce,
s: signatureData.s,
r: signatureData.r,
v: signatureData.v as any,
}
: {
deadline: signatureData.deadline,
amount: signatureData.amount,
s: signatureData.s,
r: signatureData.r,
v: signatureData.v as any,
},
}
: {}),
}
const swapRouterAddress = chainId
? trade instanceof V3Trade
? V3_ROUTER_ADDRESS[chainId]
: SWAP_ROUTER_ADDRESSES[chainId]
: undefined
if (!swapRouterAddress) return []
const { value, calldata } =
trade instanceof V3Trade
? V3SwapRouter.swapCallParameters(trade, {
...sharedSwapOptions,
deadline: deadline.toString(),
})
: SwapRouter.swapCallParameters(trade, {
...sharedSwapOptions,
deadlineOrPreviousBlockhash: deadline.toString(),
})
if (argentWalletContract && trade.inputAmount.currency.isToken) {
return [
{
address: argentWalletContract.address,
calldata: argentWalletContract.interface.encodeFunctionData('wc_multiCall', [
[
approveAmountCalldata(trade.maximumAmountIn(allowedSlippage), swapRouterAddress),
{
to: swapRouterAddress,
value,
data: calldata,
},
],
]),
value: '0x0',
},
]
}
return [
{
address: swapRouterAddress,
calldata,
value,
},
]
}
}, [
trade,
recipient,
library,
account,
chainId,
deadline,
routerContract,
allowedSlippage,
argentWalletContract,
signatureData,
])
}
/**
* This is hacking out the revert reason from the ethers provider thrown error however it can.
* This object seems to be undocumented by ethers.
* @param error an error from the ethers provider
*/
function swapErrorToUserReadableMessage(error: any): ReactNode {
let reason: string | undefined
while (Boolean(error)) {
reason = error.reason ?? error.message ?? reason
error = error.error ?? error.data?.originalError
}
if (reason?.indexOf('execution reverted: ') === 0) reason = reason.substr('execution reverted: '.length)
switch (reason) {
case 'UniswapV2Router: EXPIRED':
return (
<Trans>
The transaction could not be sent because the deadline has passed. Please check that your transaction deadline
is not too low.
</Trans>
)
case 'UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT':
case 'UniswapV2Router: EXCESSIVE_INPUT_AMOUNT':
return (
<Trans>
This transaction will not succeed either due to price movement or fee on transfer. Try increasing your
slippage tolerance.
</Trans>
)
case 'TransferHelper: TRANSFER_FROM_FAILED':
return <Trans>The input token cannot be transferred. There may be an issue with the input token.</Trans>
case 'UniswapV2: TRANSFER_FAILED':
return <Trans>The output token cannot be transferred. There may be an issue with the output token.</Trans>
case 'UniswapV2: K':
return (
<Trans>
The Uniswap invariant x*y=k was not satisfied by the swap. This usually means one of the tokens you are
swapping incorporates custom behavior on transfer.
</Trans>
)
case 'Too little received':
case 'Too much requested':
case 'STF':
return (
<Trans>
This transaction will not succeed due to price movement. Try increasing your slippage tolerance. Note: fee on
transfer and rebase tokens are incompatible with Uniswap V3.
</Trans>
)
case 'TF':
return (
<Trans>
The output token cannot be transferred. There may be an issue with the output token. Note: fee on transfer and
rebase tokens are incompatible with Uniswap V3.
</Trans>
)
default:
if (reason?.indexOf('undefined is not an object') !== -1) {
console.error(error, reason)
return (
<Trans>
An error occurred when trying to execute this swap. You may need to increase your slippage tolerance. If
that does not work, there may be an incompatibility with the token you are trading. Note: fee on transfer
and rebase tokens are incompatible with Uniswap V3.
</Trans>
)
}
return (
<Trans>
Unknown error{reason ? `: "${reason}"` : ''}. Try increasing your slippage tolerance. Note: fee on transfer
and rebase tokens are incompatible with Uniswap V3.
</Trans>
)
}
}
// returns a function that will execute a swap, if the parameters are all valid
// and the user has approved the slippage adjusted input amount for the trade
export function useSwapCallback(
@@ -292,139 +20,56 @@ export function useSwapCallback(
recipientAddressOrName: string | null, // the ENS name or address of the recipient of the trade, or null if swap should be returned to sender
signatureData: SignatureData | undefined | null
): { state: SwapCallbackState; callback: null | (() => Promise<string>); error: ReactNode | null } {
const { account, chainId, library } = useActiveWeb3React()
const { account } = useActiveWeb3React()
const swapCalls = useSwapCallArguments(trade, allowedSlippage, recipientAddressOrName, signatureData)
const deadline = useTransactionDeadline()
const addTransaction = useTransactionAdder()
const { address: recipientAddress } = useENS(recipientAddressOrName)
const recipient = recipientAddressOrName === null ? account : recipientAddress
return useMemo(() => {
if (!trade || !library || !account || !chainId) {
return { state: SwapCallbackState.INVALID, callback: null, error: <Trans>Missing dependencies</Trans> }
const {
state,
callback: libCallback,
error,
} = useLibSwapCallBack({ trade, allowedSlippage, recipientAddressOrName: recipient, signatureData, deadline })
const callback = useMemo(() => {
if (!libCallback || !trade) {
return null
}
if (!recipient) {
if (recipientAddressOrName !== null) {
return { state: SwapCallbackState.INVALID, callback: null, error: <Trans>Invalid recipient</Trans> }
} else {
return { state: SwapCallbackState.LOADING, callback: null, error: null }
}
}
return {
state: SwapCallbackState.VALID,
callback: async function onSwap(): Promise<string> {
const estimatedCalls: SwapCallEstimate[] = await Promise.all(
swapCalls.map((call) => {
const { address, calldata, value } = call
const tx =
!value || isZero(value)
? { from: account, to: address, data: calldata }
: {
from: account,
to: address,
data: calldata,
value,
}
return library
.estimateGas(tx)
.then((gasEstimate) => {
return {
call,
gasEstimate,
}
})
.catch((gasError) => {
console.debug('Gas estimate failed, trying eth_call to extract error', call)
return library
.call(tx)
.then((result) => {
console.debug('Unexpected successful call after failed estimate gas', call, gasError, result)
return { call, error: <Trans>Unexpected issue with estimating the gas. Please try again.</Trans> }
})
.catch((callError) => {
console.debug('Call threw error', call, callError)
return { call, error: swapErrorToUserReadableMessage(callError) }
})
})
})
return () =>
libCallback().then((response) => {
addTransaction(
response,
trade.tradeType === TradeType.EXACT_INPUT
? {
type: TransactionType.SWAP,
tradeType: TradeType.EXACT_INPUT,
inputCurrencyId: currencyId(trade.inputAmount.currency),
inputCurrencyAmountRaw: trade.inputAmount.quotient.toString(),
expectedOutputCurrencyAmountRaw: trade.outputAmount.quotient.toString(),
outputCurrencyId: currencyId(trade.outputAmount.currency),
minimumOutputCurrencyAmountRaw: trade.minimumAmountOut(allowedSlippage).quotient.toString(),
}
: {
type: TransactionType.SWAP,
tradeType: TradeType.EXACT_OUTPUT,
inputCurrencyId: currencyId(trade.inputAmount.currency),
maximumInputCurrencyAmountRaw: trade.maximumAmountIn(allowedSlippage).quotient.toString(),
outputCurrencyId: currencyId(trade.outputAmount.currency),
outputCurrencyAmountRaw: trade.outputAmount.quotient.toString(),
expectedInputCurrencyAmountRaw: trade.inputAmount.quotient.toString(),
}
)
return response.hash
})
}, [addTransaction, allowedSlippage, libCallback, trade])
// a successful estimation is a bignumber gas estimate and the next call is also a bignumber gas estimate
let bestCallOption: SuccessfulCall | SwapCallEstimate | undefined = estimatedCalls.find(
(el, ix, list): el is SuccessfulCall =>
'gasEstimate' in el && (ix === list.length - 1 || 'gasEstimate' in list[ix + 1])
)
// check if any calls errored with a recognizable error
if (!bestCallOption) {
const errorCalls = estimatedCalls.filter((call): call is FailedCall => 'error' in call)
if (errorCalls.length > 0) throw errorCalls[errorCalls.length - 1].error
const firstNoErrorCall = estimatedCalls.find<SwapCallEstimate>(
(call): call is SwapCallEstimate => !('error' in call)
)
if (!firstNoErrorCall) throw new Error(t`Unexpected error. Could not estimate gas for the swap.`)
bestCallOption = firstNoErrorCall
}
const {
call: { address, calldata, value },
} = bestCallOption
return library
.getSigner()
.sendTransaction({
from: account,
to: address,
data: calldata,
// let the wallet try if we can't estimate the gas
...('gasEstimate' in bestCallOption ? { gasLimit: calculateGasMargin(bestCallOption.gasEstimate) } : {}),
...(value && !isZero(value) ? { value } : {}),
})
.then((response) => {
addTransaction(
response,
trade.tradeType === TradeType.EXACT_INPUT
? {
type: TransactionType.SWAP,
tradeType: TradeType.EXACT_INPUT,
inputCurrencyId: currencyId(trade.inputAmount.currency),
inputCurrencyAmountRaw: trade.inputAmount.quotient.toString(),
expectedOutputCurrencyAmountRaw: trade.outputAmount.quotient.toString(),
outputCurrencyId: currencyId(trade.outputAmount.currency),
minimumOutputCurrencyAmountRaw: trade.minimumAmountOut(allowedSlippage).quotient.toString(),
}
: {
type: TransactionType.SWAP,
tradeType: TradeType.EXACT_OUTPUT,
inputCurrencyId: currencyId(trade.inputAmount.currency),
maximumInputCurrencyAmountRaw: trade.maximumAmountIn(allowedSlippage).quotient.toString(),
outputCurrencyId: currencyId(trade.outputAmount.currency),
outputCurrencyAmountRaw: trade.outputAmount.quotient.toString(),
expectedInputCurrencyAmountRaw: trade.inputAmount.quotient.toString(),
}
)
return response.hash
})
.catch((error) => {
// if the user rejected the tx, pass this along
if (error?.code === 4001) {
throw new Error(t`Transaction rejected.`)
} else {
// otherwise, the error was unexpected and we need to convey that
console.error(`Swap failed`, error, address, calldata, value)
throw new Error(t`Swap failed: ${swapErrorToUserReadableMessage(error)}`)
}
})
},
error: null,
}
}, [trade, library, account, chainId, recipient, recipientAddressOrName, swapCalls, addTransaction, allowedSlippage])
return {
state,
callback,
error,
}
}

View File

@@ -1,7 +1,7 @@
import { Currency, CurrencyAmount, Price, Token, TradeType } from '@uniswap/sdk-core'
import useActiveWeb3React from 'hooks/useActiveWeb3React'
import tryParseCurrencyAmount from 'lib/utils/tryParseCurrencyAmount'
import { useMemo } from 'react'
import { tryParseAmount } from 'state/swap/hooks'
import { SupportedChainId } from '../constants/chains'
import { DAI_OPTIMISM, USDC, USDC_ARBITRUM, USDC_POLYGON } from '../constants/tokens'
@@ -87,7 +87,7 @@ export function useStablecoinAmountFromFiatValue(fiatValue: string | null | unde
try {
// parse USD string into CurrencyAmount based on stablecoin decimals
return tryParseAmount(parsedForDecimals, stablecoin)
return tryParseCurrencyAmount(parsedForDecimals, stablecoin)
} catch (error) {
return undefined
}

View File

@@ -0,0 +1,18 @@
import { CurrencyAmount, Token } from '@uniswap/sdk-core'
import { PermitInfo, PermitType, useERC20Permit } from './useERC20Permit'
import useTransactionDeadline from './useTransactionDeadline'
const REMOVE_V2_LIQUIDITY_PERMIT_INFO: PermitInfo = {
version: '1',
name: 'Uniswap V2',
type: PermitType.AMOUNT,
}
export function useV2LiquidityTokenPermit(
liquidityAmount: CurrencyAmount<Token> | null | undefined,
spender: string | null | undefined
) {
const transactionDeadline = useTransactionDeadline()
return useERC20Permit(liquidityAmount, spender, transactionDeadline, REMOVE_V2_LIQUIDITY_PERMIT_INFO)
}

View File

@@ -1,12 +1,14 @@
import { Interface } from '@ethersproject/abi'
import { Currency, CurrencyAmount } from '@uniswap/sdk-core'
import { abi as IUniswapV2PairABI } from '@uniswap/v2-core/build/IUniswapV2Pair.json'
import IUniswapV2PairJson from '@uniswap/v2-core/build/IUniswapV2Pair.json'
import { computePairAddress, Pair } from '@uniswap/v2-sdk'
import { useMultipleContractSingleData } from 'lib/hooks/multicall'
import { useMemo } from 'react'
import { V2_FACTORY_ADDRESSES } from '../constants/addresses'
const { abi: IUniswapV2PairABI } = IUniswapV2PairJson
const PAIR_INTERFACE = new Interface(IUniswapV2PairABI)
export enum PairState {

View File

@@ -1,14 +1,14 @@
import { Trans } from '@lingui/macro'
import { Currency } from '@uniswap/sdk-core'
import useActiveWeb3React from 'hooks/useActiveWeb3React'
import useNativeCurrency from 'lib/hooks/useNativeCurrency'
import tryParseCurrencyAmount from 'lib/utils/tryParseCurrencyAmount'
import { useMemo } from 'react'
import { WRAPPED_NATIVE_CURRENCY } from '../constants/tokens'
import { tryParseAmount } from '../state/swap/hooks'
import { TransactionType } from '../state/transactions/actions'
import { useTransactionAdder } from '../state/transactions/hooks'
import { useCurrencyBalance } from '../state/wallet/hooks'
import { useNativeCurrency } from './Tokens'
import { useWETHContract } from './useContract'
export enum WrapType {
@@ -61,7 +61,10 @@ export default function useWrapCallback(
const wethContract = useWETHContract()
const balance = useCurrencyBalance(account ?? undefined, inputCurrency ?? undefined)
// we can always parse the amount typed as the input currency, since wrapping is 1:1
const inputAmount = useMemo(() => tryParseAmount(typedValue, inputCurrency ?? undefined), [inputCurrency, typedValue])
const inputAmount = useMemo(
() => tryParseCurrencyAmount(typedValue, inputCurrency ?? undefined),
[inputCurrency, typedValue]
)
const addTransaction = useTransactionAdder()
return useMemo(() => {

View File

@@ -1,6 +1,6 @@
import { useWeb3React } from '@web3-react/core'
import type { EthereumProvider } from 'lib/ethereum'
import { useEffect, useState } from 'react'
import { useWeb3React } from 'web3-react-core'
import { gnosisSafe, injected } from '../connectors'
import { IS_IN_IFRAME } from '../constants/misc'

View File

@@ -3,13 +3,13 @@ import 'inter-ui'
import 'polyfills'
import 'components/analytics'
import { createWeb3ReactRoot, Web3ReactProvider } from '@web3-react/core'
import { BlockUpdater } from 'lib/hooks/useBlockNumber'
import { MulticallUpdater } from 'lib/state/multicall'
import { StrictMode } from 'react'
import ReactDOM from 'react-dom'
import { Provider } from 'react-redux'
import { HashRouter } from 'react-router-dom'
import { createWeb3ReactRoot, Web3ReactProvider } from 'web3-react-core'
import Blocklist from './components/Blocklist'
import { NetworkContextName } from './constants/misc'
@@ -73,4 +73,4 @@ ReactDOM.render(
if (process.env.REACT_APP_SERVICE_WORKER !== 'false') {
serviceWorkerRegistration.register()
}
export { INFURA_NETWORK_URLS } from 'constants/chainInfo'
export { INFURA_NETWORK_URLS } from 'constants/infura'

View File

@@ -1,12 +0,0 @@
import * as React from 'react'
function SvgCheck(props: React.SVGProps<SVGSVGElement>) {
return (
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg" {...props}>
<circle cx={10} cy={10} r={10} />
<path d="M14 7l-5.5 5.5L6 10" stroke="#fff" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round" />
</svg>
)
}
export default SvgCheck

View File

@@ -1,19 +0,0 @@
import * as React from 'react'
function SvgExpando(props: React.SVGProps<SVGSVGElement>) {
return (
<svg
viewBox="0 0 24 24"
fill="none"
strokeWidth={2}
strokeLinecap="round"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<path className="expando_svg__left" d="M18 15l-6-6" />
<path className="expando_svg__right" d="M12 9l-6 6" />
</svg>
)
}
export default SvgExpando

View File

@@ -1,18 +0,0 @@
import * as React from 'react'
function SvgLogo(props: React.SVGProps<SVGSVGElement>) {
return (
<svg viewBox="0 0 14 15" xmlns="http://www.w3.org/2000/svg" {...props}>
<path d="M4.152 1.551c-.188-.029-.196-.032-.107-.045.17-.026.57.009.846.074.644.152 1.23.542 1.856 1.235l.166.184.238-.038c1.002-.16 2.02-.033 2.873.358.235.108.605.322.65.377.016.018.043.13.06.251.064.418.033.737-.096.976-.07.13-.074.171-.027.283a.274.274 0 00.246.154c.212 0 .44-.34.545-.814l.042-.189.083.094c.457.514.815 1.214.876 1.712l.016.13-.076-.118a1.462 1.462 0 00-.435-.453c-.306-.201-.63-.27-1.486-.315-.774-.04-1.212-.106-1.646-.247-.739-.24-1.111-.558-1.989-1.702-.39-.509-.63-.79-.87-1.016-.545-.515-1.08-.785-1.765-.89z" />
<path d="M10.85 2.686c.019-.34.065-.565.159-.77a.825.825 0 01.077-.148c.005 0-.011.06-.036.133-.068.2-.08.472-.032.789.06.402.093.46.52.894.201.204.434.46.519.571l.154.2-.154-.143c-.188-.175-.62-.517-.716-.566-.064-.032-.074-.032-.113.007-.037.036-.044.09-.05.346-.007.399-.062.655-.194.91-.071.14-.082.11-.018-.047.048-.116.053-.168.053-.554 0-.775-.094-.962-.637-1.28a5.971 5.971 0 00-.504-.26 1.912 1.912 0 01-.246-.12c.015-.015.545.139.758.22.318.122.37.137.409.123.025-.01.038-.085.05-.305zM4.517 4.013c-.381-.522-.618-1.323-.566-1.922l.015-.185.087.015c.164.03.445.134.577.214.361.218.518.505.677 1.243.047.216.108.46.136.544.045.133.217.444.356.646.1.146.034.215-.188.195-.339-.03-.798-.345-1.094-.75zM10.386 7.9c-1.784-.713-2.412-1.333-2.412-2.378 0-.154.005-.28.012-.28.006 0 .075.05.153.113.362.288.767.411 1.889.574.66.096 1.03.173 1.373.286 1.09.359 1.763 1.087 1.924 2.08.046.288.02.828-.057 1.113-.06.225-.242.63-.29.646-.014.005-.027-.046-.03-.116-.018-.372-.208-.735-.526-1.007-.362-.309-.848-.555-2.036-1.03zM9.134 8.197a3.133 3.133 0 00-.086-.375l-.046-.135.085.095c.117.13.21.297.288.52.06.17.066.22.066.496 0 .271-.008.328-.064.48a1.518 1.518 0 01-.376.596c-.326.33-.745.512-1.35.588-.105.013-.411.035-.68.049-.679.035-1.126.108-1.527.248a.324.324 0 01-.115.027c-.016-.016.258-.178.483-.286.318-.153.635-.236 1.345-.353.35-.058.713-.129.805-.157.868-.264 1.315-.947 1.172-1.793z" />
<path d="M9.952 9.641c-.237-.506-.292-.995-.162-1.451.014-.05.036-.089.05-.089.013 0 .07.03.124.067.11.073.328.196.912.512.728.395 1.144.7 1.426 1.05.247.305.4.654.474 1.078.042.24.017.82-.045 1.062-.196.764-.65 1.364-1.3 1.714-.095.051-.18.093-.19.093-.009 0 .026-.087.077-.194.219-.454.244-.895.079-1.386-.102-.301-.308-.668-.724-1.289-.484-.72-.602-.913-.721-1.167zM3.25 12.374c.663-.556 1.486-.95 2.237-1.072a3.51 3.51 0 011.161.045c.48.122.91.396 1.133.721.218.319.312.596.41 1.214.038.243.08.488.092.543.073.32.216.576.392.704.28.204.764.217 1.239.033a.618.618 0 01.155-.048c.017.017-.222.176-.39.26a1.334 1.334 0 01-.648.156c-.435 0-.796-.22-1.098-.668a5.3 5.3 0 01-.296-.588c-.318-.721-.475-.94-.844-1.181-.322-.21-.737-.247-1.049-.095-.41.2-.524.72-.23 1.05a.911.911 0 00.512.266.545.545 0 00.619-.544c0-.217-.084-.34-.295-.436-.289-.129-.598.022-.597.291 0 .115.051.187.167.24.074.033.076.035.015.023-.264-.055-.326-.372-.114-.582.256-.252.784-.141.965.204.076.145.085.433.019.607-.15.39-.582.595-1.022.483-.3-.076-.421-.158-.782-.527-.627-.642-.87-.767-1.774-.907l-.174-.027.197-.165z" />
<path
fillRule="evenodd"
clipRule="evenodd"
d="M.308.884C2.402 3.41 3.845 4.452 4.005 4.672c.132.182.082.346-.144.474a1.381 1.381 0 01-.515.143c-.147 0-.198-.056-.198-.056-.085-.08-.133-.066-.57-.837A132.96 132.96 0 001.45 2.67c-.032-.03-.031-.03 1.067 1.923.177.407.035.556.035.614 0 .118-.033.18-.179.343-.244.27-.353.574-.432 1.203-.088.705-.336 1.203-1.024 2.056-.402.499-.468.59-.57.792-.128.253-.163.395-.177.714-.015.339.014.557.118.88.09.284.186.47.429.844.21.323.33.563.33.657 0 .074.014.074.34.001.776-.174 1.407-.48 1.762-.857.22-.233.271-.361.273-.68.001-.208-.006-.252-.063-.372-.092-.195-.26-.358-.63-.61-.486-.33-.694-.595-.75-.96-.048-.3.007-.511.275-1.07.278-.58.347-.827.394-1.41.03-.377.071-.526.18-.646.114-.124.216-.166.498-.204.459-.063.75-.18.99-.4a.853.853 0 00.31-.652l.01-.21-.117-.134C4.098 4.004.026.5 0 .5-.005.5.133.673.308.884zm.976 9.815a.37.37 0 00-.115-.489c-.15-.1-.385-.052-.385.077 0 .04.022.069.072.094.084.043.09.091.024.19-.067.099-.061.186.015.246.123.095.297.043.389-.118zM4.925 5.999c-.215.065-.424.292-.49.53-.039.145-.016.4.043.478.096.127.188.16.439.159.49-.003.916-.212.966-.474.04-.214-.147-.51-.405-.641a.965.965 0 00-.553-.052zm.574.445c.075-.107.042-.222-.087-.3-.244-.149-.615-.026-.615.204 0 .115.193.24.37.24.118 0 .28-.07.332-.144z"
/>
</svg>
)
}
export default SvgLogo

View File

@@ -1,17 +0,0 @@
import * as React from 'react'
function SvgSpinner(props: React.SVGProps<SVGSVGElement>) {
return (
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" {...props}>
<mask id="spinner_svg__a">
<path fill="#fff" strokeWidth={0} d="M0 0h24v24H0z" />
<path fill="#000" strokeWidth={0} d="M0 0h12v12H0z" />
<circle cx={2} cy={12} r={1} fill="#fff" strokeWidth={0} />
<circle cx={12} cy={2} r={1} fill="#fff" strokeWidth={0} />
</mask>
<circle cx={12} cy={12} r={10} mask="url(#spinner_svg__a)" />
</svg>
)
}
export default SvgSpinner

View File

@@ -0,0 +1,5 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M2 7C2 5.89543 2.89543 5 4 5H20C21.1046 5 22 5.89543 22 7V18C22 19.1046 21.1046 20 20 20H4C2.89543 20 2 19.1046 2 18V7Z" stroke="currentColor" stroke-width="2"/>
<path d="M4 19H20C21.1046 19 22 18.1046 22 17V14C22 12.8954 21.1046 12 20 12H16C15.4477 12 14.9935 12.4624 14.7645 12.965C14.4438 13.6688 13.789 14.5 12 14.5C10.29 14.5 9.48213 13.7406 9.1936 13.0589C8.96576 12.5206 8.49905 12 7.91447 12H4C2.89543 12 2 12.8954 2 14V17C2 18.1046 2.89543 19 4 19Z" fill="currentColor"/>
<path d="M22 13V11C22 9.89543 21.1034 9 19.9989 9C14.0294 9 9.97062 9 4.00115 9C2.89658 9 2 9.89543 2 11V13" stroke="currentColor" stroke-width="2"/>
</svg>

After

Width:  |  Height:  |  Size: 743 B

View File

@@ -1,6 +1,6 @@
import { AlertTriangle, LargeIcon } from 'lib/icons'
import { AlertTriangle, Icon, LargeIcon } from 'lib/icons'
import styled, { Color, css, keyframes, ThemedText } from 'lib/theme'
import { ReactNode } from 'react'
import { ReactNode, useMemo } from 'react'
import Button from './Button'
import Row from './Row'
@@ -9,6 +9,10 @@ const StyledButton = styled(Button)`
border-radius: ${({ theme }) => theme.borderRadius}em;
flex-grow: 1;
transition: background-color 0.25s ease-out, flex-grow 0.25s ease-out, padding 0.25s ease-out;
:disabled {
margin: -1px;
}
`
const UpdateRow = styled(Row)``
@@ -24,7 +28,7 @@ const grow = keyframes`
}
`
const updatedCss = css`
const updateCss = css`
border: 1px solid ${({ theme }) => theme.outline};
padding: calc(0.25em - 1px);
padding-left: calc(0.75em - 1px);
@@ -41,19 +45,19 @@ const updatedCss = css`
}
`
export const Overlay = styled(Row)<{ updated?: boolean }>`
export const Overlay = styled(Row)<{ update?: boolean }>`
border-radius: ${({ theme }) => theme.borderRadius}em;
flex-direction: row-reverse;
min-height: 3.5em;
transition: padding 0.25s ease-out;
${({ updated }) => updated && updatedCss}
${({ update }) => update && updateCss}
`
export interface ActionButtonProps {
color?: Color
disabled?: boolean
updated?: { message: ReactNode; action: ReactNode }
update?: { message: ReactNode; action: ReactNode; icon?: Icon }
onClick: () => void
onUpdate?: () => void
children: ReactNode
@@ -62,22 +66,23 @@ export interface ActionButtonProps {
export default function ActionButton({
color = 'accent',
disabled,
updated,
update,
onClick,
onUpdate,
children,
}: ActionButtonProps) {
const textColor = useMemo(() => (color === 'accent' && !disabled ? 'onAccent' : 'currentColor'), [color, disabled])
return (
<Overlay updated={Boolean(updated)} flex align="stretch">
<StyledButton color={color} disabled={disabled} onClick={updated ? onUpdate : onClick}>
<ThemedText.TransitionButton buttonSize={updated ? 'medium' : 'large'} color="currentColor">
{updated ? updated.action : children}
<Overlay update={Boolean(update)} flex align="stretch">
<StyledButton color={color} disabled={disabled} onClick={update ? onUpdate : onClick}>
<ThemedText.TransitionButton buttonSize={update ? 'medium' : 'large'} color={textColor}>
{update ? update.action : children}
</ThemedText.TransitionButton>
</StyledButton>
{updated && (
{update && (
<UpdateRow gap={0.5}>
<LargeIcon icon={AlertTriangle} />
<ThemedText.Subhead2>{updated?.message}</ThemedText.Subhead2>
<LargeIcon color="currentColor" icon={update.icon || AlertTriangle} />
<ThemedText.Subhead2>{update?.message}</ThemedText.Subhead2>
</UpdateRow>
)}
</Overlay>

View File

@@ -0,0 +1,40 @@
import { Trans } from '@lingui/macro'
import Row from 'lib/components/Row'
import { Logo } from 'lib/icons'
import styled, { brand, ThemedText } from 'lib/theme'
import ExternalLink from './ExternalLink'
const UniswapA = styled(ExternalLink)`
color: ${({ theme }) => theme.secondary};
cursor: pointer;
text-decoration: none;
${Logo} {
fill: ${({ theme }) => theme.secondary};
height: 1em;
transition: transform 0.25s ease, fill 0.25s ease;
width: 1em;
will-change: transform;
}
:hover ${Logo} {
fill: ${brand};
transform: rotate(-5deg);
}
`
export default function BrandedFooter() {
return (
<Row justify="center">
<UniswapA href={`https://app.uniswap.org/`}>
<Row gap={0.25}>
<Logo />
<ThemedText.Caption>
<Trans>Powered by the Uniswap protocol</Trans>
</ThemedText.Caption>
</Row>
</UniswapA>
</Row>
)
}

View File

@@ -12,7 +12,6 @@ const Column = styled.div<{
css?: ReturnType<typeof css>
}>`
align-items: ${({ align }) => align ?? 'center'};
background-color: inherit;
color: ${({ color, theme }) => color && theme[color]};
display: ${({ flex }) => (flex ? 'flex' : 'grid')};
flex-direction: column;

View File

@@ -106,7 +106,7 @@ export default function Dialog({ color, children, onClose = () => void 0 }: Dial
context.element &&
createPortal(
<ThemeProvider>
<Modal className="dialog" color={color} ref={dialog}>
<Modal color={color} ref={dialog}>
<OnCloseContext.Provider value={onClose}>{children}</OnCloseContext.Provider>
</Modal>
</ThemeProvider>,

View File

@@ -34,7 +34,7 @@ export default class ErrorBoundary extends React.Component<ErrorBoundaryProps, E
<Dialog color="dialog">
<ErrorDialog
error={this.state.error}
header={<Trans>Reload the page to try again</Trans>}
header={<Trans>Something went wrong.</Trans>}
action={<Trans>Reload the page</Trans>}
onAction={() => window.location.reload()}
/>

View File

@@ -117,7 +117,10 @@ export default function ErrorDialog({ header, error, action, onAction }: ErrorDi
<Rule />
<ErrorColumn>
<Column gap={0.5} ref={setDetails} css={scrollbar}>
<ThemedText.Code>{error.message}</ThemedText.Code>
<ThemedText.Code>
{error.name}
{error.message ? `: ${error.message}` : ''}
</ThemedText.Code>
</Column>
</ErrorColumn>
<ActionButton onClick={onAction}>{action}</ActionButton>

View File

@@ -0,0 +1,30 @@
import { SUPPORTED_LOCALES } from 'constants/locales'
import { WidgetProps } from 'lib/components/Widget'
import { IntegrationError } from 'lib/errors'
import { PropsWithChildren, useEffect } from 'react'
export default function WidgetsPropsValidator(props: PropsWithChildren<WidgetProps>) {
const { jsonRpcEndpoint, provider } = props
useEffect(() => {
if (!provider && !jsonRpcEndpoint) {
throw new IntegrationError('This widget requires a provider or jsonRpcEndpoint.')
}
}, [provider, jsonRpcEndpoint])
const { width } = props
useEffect(() => {
if (width && width < 300) {
throw new IntegrationError(`Set widget width to at least 300px. (You set it to ${width}.)`)
}
}, [width])
const { locale } = props
useEffect(() => {
if (locale && locale !== 'pseudo' && !SUPPORTED_LOCALES.includes(locale)) {
console.warn('Unsupported locale: ', locale)
}
}, [locale])
return <>{props.children}</>
}

View File

@@ -0,0 +1,17 @@
import { HTMLProps } from 'react'
/**
* Outbound link
*/
export default function ExternalLink({
target = '_blank',
href,
rel = 'noopener noreferrer',
...rest
}: Omit<HTMLProps<HTMLAnchorElement>, 'as' | 'ref' | 'onClick'> & { href: string }) {
return (
<a target={target} rel={rel} href={href} {...rest}>
{rest.children}
</a>
)
}

View File

@@ -1,26 +1,9 @@
import { largeIconCss, Logo } from 'lib/icons'
import { largeIconCss } from 'lib/icons'
import styled, { ThemedText } from 'lib/theme'
import { ReactElement, ReactNode } from 'react'
import Row from './Row'
const UniswapA = styled.a`
cursor: pointer;
${Logo} {
fill: ${({ theme }) => theme.secondary};
height: 1.5em;
transition: transform 0.25s ease;
width: 1.5em;
will-change: transform;
:hover {
fill: ${({ theme }) => theme.onHover(theme.secondary)};
transform: rotate(-5deg);
}
}
`
const HeaderRow = styled(Row)`
height: 1.75em;
margin: 0 0.75em 0.75em;
@@ -30,21 +13,13 @@ const HeaderRow = styled(Row)`
export interface HeaderProps {
title?: ReactElement
logo?: boolean
children: ReactNode
}
export default function Header({ title, logo, children }: HeaderProps) {
export default function Header({ title, children }: HeaderProps) {
return (
<HeaderRow iconSize={1.2}>
<Row gap={0.5}>
{logo && (
<UniswapA href={`https://app.uniswap.org/`}>
<Logo />
</UniswapA>
)}
{title && <ThemedText.Subhead1>{title}</ThemedText.Subhead1>}
</Row>
<Row gap={0.5}>{title && <ThemedText.Subhead1>{title}</ThemedText.Subhead1>}</Row>
<Row gap={1}>{children}</Row>
</HeaderRow>
)

View File

@@ -1,3 +1,4 @@
import JSBI from 'jsbi'
import styled, { css } from 'lib/theme'
import { forwardRef, HTMLProps, useCallback, useEffect, useState } from 'react'
@@ -67,23 +68,31 @@ export const StringInput = forwardRef<HTMLInputElement, StringInputProps>(functi
})
interface NumericInputProps extends Omit<HTMLProps<HTMLInputElement>, 'onChange' | 'as' | 'value'> {
value: number | undefined
onChange: (input: number | undefined) => void
value: string
onChange: (input: string) => void
}
interface EnforcedNumericInputProps extends NumericInputProps {
// Validates nextUserInput; returns stringified value or undefined if valid, or null if invalid
enforcer: (nextUserInput: string) => string | undefined | null
// Validates nextUserInput; returns stringified value, or null if invalid
enforcer: (nextUserInput: string) => string | null
}
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 NumericInput = forwardRef<HTMLInputElement, EnforcedNumericInputProps>(function NumericInput(
{ value, onChange, enforcer, pattern, ...props }: EnforcedNumericInputProps,
ref
) {
// Allow value/onChange to use number by preventing a trailing decimal separator from triggering onChange
const [state, setState] = useState(value ?? '')
useEffect(() => {
if (+state !== value) {
if (!isNumericallyEqual(state, value)) {
setState(value ?? '')
}
}, [value, state, setState])
@@ -93,8 +102,8 @@ const NumericInput = forwardRef<HTMLInputElement, EnforcedNumericInputProps>(fun
const nextInput = enforcer(event.target.value.replace(/,/g, '.'))
if (nextInput !== null) {
setState(nextInput ?? '')
if (nextInput === undefined || +nextInput !== value) {
onChange(nextInput === undefined ? undefined : +nextInput)
if (!isNumericallyEqual(nextInput, value)) {
onChange(nextInput)
}
}
},
@@ -114,6 +123,7 @@ const NumericInput = forwardRef<HTMLInputElement, EnforcedNumericInputProps>(fun
pattern={pattern}
placeholder={props.placeholder || '0'}
minLength={1}
maxLength={79}
spellCheck="false"
ref={ref as any}
{...props}
@@ -125,7 +135,7 @@ const integerRegexp = /^\d*$/
const integerEnforcer = (nextUserInput: string) => {
if (nextUserInput === '' || integerRegexp.test(nextUserInput)) {
const nextInput = parseInt(nextUserInput)
return isNaN(nextInput) ? undefined : nextInput.toString()
return isNaN(nextInput) ? '' : nextInput.toString()
}
return null
}
@@ -136,7 +146,7 @@ export const IntegerInput = forwardRef(function IntegerInput(props: NumericInput
const decimalRegexp = /^\d*(?:[.])?\d*$/
const decimalEnforcer = (nextUserInput: string) => {
if (nextUserInput === '') {
return undefined
return ''
} else if (nextUserInput === '.') {
return '0.'
} else if (decimalRegexp.test(nextUserInput)) {
@@ -153,7 +163,7 @@ export const inputCss = css`
border: 1px solid ${({ theme }) => theme.container};
border-radius: ${({ theme }) => theme.borderRadius}em;
cursor: text;
padding: calc(0.75em - 1px);
padding: calc(0.5em - 1px);
:hover:not(:focus-within) {
background-color: ${({ theme }) => theme.onHover(theme.container)};

View File

@@ -11,6 +11,7 @@ export const BoundaryProvider = BoundaryContext.Provider
const PopoverContainer = styled.div<{ show: boolean }>`
background-color: ${({ theme }) => theme.dialog};
border: 1px solid ${({ theme }) => theme.outline};
border-radius: 0.5em;
opacity: ${(props) => (props.show ? 1 : 0)};
padding: 8px;
@@ -32,7 +33,6 @@ const Arrow = styled.div`
background: ${({ theme }) => theme.dialog};
border: 1px solid ${({ theme }) => theme.outline};
content: '';
height: 8px;
position: absolute;
transform: rotate(45deg);
@@ -40,32 +40,36 @@ const Arrow = styled.div`
}
&.arrow-top {
bottom: -5px;
bottom: -4px;
::before {
border-radius: 1px;
border-left: none;
border-top: none;
}
}
&.arrow-bottom {
top: -5px;
top: -5px; // includes -1px from border
::before {
border-bottom: none;
border-right: none;
border-radius: 1px;
}
}
&.arrow-left {
right: -5px;
right: -4px;
::before {
border-bottom: none;
border-left: none;
border-radius: 1px;
}
}
&.arrow-right {
left: -5px;
left: -5px; // includes -1px from border
::before {
border-radius: 1px;
border-right: none;
border-top: none;
}
@@ -77,10 +81,11 @@ export interface PopoverProps {
show: boolean
children: React.ReactNode
placement: Placement
offset?: number
contained?: true
}
export default function Popover({ content, show, children, placement, contained }: PopoverProps) {
export default function Popover({ content, show, children, placement, offset, contained }: PopoverProps) {
const boundary = useContext(BoundaryContext)
const reference = useRef<HTMLDivElement>(null)
@@ -90,8 +95,8 @@ export default function Popover({ content, show, children, placement, contained
const options = useMemo((): Options => {
const modifiers: Options['modifiers'] = [
{ name: 'offset', options: { offset: [5, 5] } },
{ name: 'arrow', options: { element: arrow, padding: 6 } },
{ name: 'offset', options: { offset: [4, offset || 4] } },
{ name: 'arrow', options: { element: arrow, padding: 4 } },
]
if (contained) {
modifiers.push(
@@ -118,7 +123,7 @@ export default function Popover({ content, show, children, placement, contained
strategy: 'absolute',
modifiers,
}
}, [arrow, boundary, placement, contained])
}, [offset, arrow, contained, placement, boundary])
const { styles, attributes } = usePopper(reference.current, popover, options)

View File

@@ -1,8 +1,7 @@
import { Trans } from '@lingui/macro'
import { Currency } from '@uniswap/sdk-core'
import { AlertTriangle, ArrowRight, CheckCircle, Spinner, Trash2 } from 'lib/icons'
import { DAI, ETH, UNI, USDC } from 'lib/mocks'
import styled, { ThemedText } from 'lib/theme'
import { Token } from 'lib/types'
import { useMemo, useState } from 'react'
import Button from './Button'
@@ -13,7 +12,7 @@ import TokenImg from './TokenImg'
interface ITokenAmount {
value: number
token: Token
token: Currency
}
export enum TransactionStatus {
@@ -28,25 +27,6 @@ interface ITransaction {
status: TransactionStatus
}
// TODO: integrate with web3-react context
export const mockTxs: ITransaction[] = [
{
input: { value: 4170.15, token: USDC },
output: { value: 4167.44, token: DAI },
status: TransactionStatus.SUCCESS,
},
{
input: { value: 1.23, token: ETH },
output: { value: 4125.02, token: DAI },
status: TransactionStatus.PENDING,
},
{
input: { value: 10, token: UNI },
output: { value: 2125.02, token: ETH },
status: TransactionStatus.ERROR,
},
]
const TransactionRow = styled(Row)`
padding: 0.5em 1em;
@@ -94,7 +74,7 @@ function Transaction({ tx }: { tx: ITransaction }) {
}
export default function RecentTransactionsDialog() {
const [txs, setTxs] = useState(mockTxs)
const [txs, setTxs] = useState([])
return (
<>

View File

@@ -8,7 +8,7 @@ const Row = styled.div<{
pad?: number
gap?: number
flex?: true
grow?: true
grow?: true | 'first' | 'last'
children?: ReactNode
theme: Theme
}>`
@@ -19,7 +19,12 @@ const Row = styled.div<{
flex-grow: ${({ grow }) => grow && 1};
gap: ${({ gap }) => gap && `${gap}em`};
grid-auto-flow: column;
grid-template-columns: ${({ grow, children }) => (grow ? `repeat(${Children.count(children)}, 1fr)` : '')};
grid-template-columns: ${({ grow, children }) => {
if (grow === 'first') return '1fr'
if (grow === 'last') return `repeat(${Children.count(children) - 1}, auto) 1fr`
if (grow) return `repeat(${Children.count(children)}, 1fr)`
return undefined
}};
justify-content: ${({ justify }) => justify ?? 'space-between'};
padding: ${({ pad }) => pad && `0 ${pad}em`};
`

View File

@@ -1,16 +1,24 @@
import { Trans } from '@lingui/macro'
import { CurrencyAmount, Token } from '@uniswap/sdk-core'
import { useLingui } from '@lingui/react'
import { useUSDCValue } from 'hooks/useUSDCPrice'
import { useAtomValue } from 'jotai/utils'
import { inputAtom, useUpdateInputToken, useUpdateInputValue } from 'lib/state/swap'
import { loadingOpacityCss } from 'lib/css/loading'
import { useSwapAmount, useSwapCurrency, useSwapInfo } from 'lib/hooks/swap'
import { usePrefetchCurrencyColor } from 'lib/hooks/useCurrencyColor'
import { Field, independentFieldAtom } from 'lib/state/swap'
import styled, { ThemedText } from 'lib/theme'
import { useMemo } from 'react'
import { TradeState } from 'state/routing/types'
import { formatCurrencyAmount } from 'utils/formatCurrencyAmount'
import Column from '../Column'
import Row from '../Row'
import TokenImg from '../TokenImg'
import TokenInput from './TokenInput'
const mockToken = new Token(1, '0x8b3192f5eebd8579568a2ed41e6feb402f93f73f', 9, 'STM', 'Saitama')
const mockCurrencyAmount = CurrencyAmount.fromRawAmount(mockToken, '134108514895957704114061')
const LoadingSpan = styled.span<{ $loading: boolean }>`
${loadingOpacityCss};
`
const InputColumn = styled(Column)<{ approved?: boolean }>`
margin: 0.75em;
@@ -27,31 +35,59 @@ interface InputProps {
}
export default function Input({ disabled }: InputProps) {
const input = useAtomValue(inputAtom)
const setValue = useUpdateInputValue(inputAtom)
const setToken = useUpdateInputToken(inputAtom)
const balance = mockCurrencyAmount
const { i18n } = useLingui()
const {
trade: { state: tradeState },
currencyBalances: { [Field.INPUT]: balance },
currencyAmounts: { [Field.INPUT]: inputCurrencyAmount },
} = useSwapInfo()
const inputUSDC = useUSDCValue(inputCurrencyAmount)
const [swapInputAmount, updateSwapInputAmount] = useSwapAmount(Field.INPUT)
const [swapInputCurrency, updateSwapInputCurrency] = useSwapCurrency(Field.INPUT)
// extract eagerly in case of reversal
usePrefetchCurrencyColor(swapInputCurrency)
const isTradeLoading = useMemo(
() => TradeState.LOADING === tradeState || TradeState.SYNCING === tradeState,
[tradeState]
)
const isDependentField = useAtomValue(independentFieldAtom) !== Field.INPUT
const isLoading = isDependentField && isTradeLoading
//TODO(ianlapham): mimic logic from app swap page
const mockApproved = true
const onMax = useMemo(() => {
if (balance?.greaterThan(0)) {
return () => updateSwapInputAmount(balance.toExact())
}
return
}, [balance, updateSwapInputAmount])
return (
<InputColumn gap={0.5} approved={input.approved !== false}>
<InputColumn gap={0.5} approved={mockApproved}>
<Row>
<ThemedText.Subhead2 color="secondary">
<Trans>Trading</Trans>
</ThemedText.Subhead2>
</Row>
<TokenInput
input={input}
currency={swapInputCurrency}
amount={(swapInputAmount !== undefined ? swapInputAmount : inputCurrencyAmount?.toSignificant(6)) ?? ''}
disabled={disabled}
onMax={balance ? () => setValue(1234) : undefined}
onChangeInput={setValue}
onChangeToken={setToken}
onMax={onMax}
onChangeInput={updateSwapInputAmount}
onChangeCurrency={updateSwapInputCurrency}
loading={isLoading}
>
<ThemedText.Body2 color="secondary">
<Row>
{input.usdc ? `~ $${input.usdc.toLocaleString('en')}` : '-'}
<LoadingSpan $loading={isLoading}>{inputUSDC ? `$${inputUSDC.toFixed(2)}` : '-'}</LoadingSpan>
{balance && (
<ThemedText.Body2 color={input.value && balance.lessThan(input.value) ? 'error' : undefined}>
Balance: <span style={{ userSelect: 'text' }}>{balance.toExact()}</span>
<ThemedText.Body2 color={inputCurrencyAmount?.greaterThan(balance) ? 'error' : undefined}>
Balance: <span style={{ userSelect: 'text' }}>{formatCurrencyAmount(balance, 4, i18n.locale)}</span>
</ThemedText.Body2>
)}
</Row>

View File

@@ -1,10 +1,18 @@
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 useColor, { usePrefetchColor } from 'lib/hooks/useColor'
import { inputAtom, outputAtom, useUpdateInputToken, useUpdateInputValue } from 'lib/state/swap'
import BrandedFooter from 'lib/components/BrandedFooter'
import { loadingOpacityCss } from 'lib/css/loading'
import { useSwapAmount, useSwapCurrency, useSwapInfo } from 'lib/hooks/swap'
import useCurrencyColor from 'lib/hooks/useCurrencyColor'
import { Field, independentFieldAtom } from 'lib/state/swap'
import styled, { DynamicThemeProvider, ThemedText } from 'lib/theme'
import { ReactNode, useMemo } from 'react'
import { TradeState } from 'state/routing/types'
import { computeFiatValuePriceImpact } from 'utils/computeFiatValuePriceImpact'
import { formatCurrencyAmount } from 'utils/formatCurrencyAmount'
import Column from '../Column'
import Row from '../Row'
@@ -12,16 +20,21 @@ import TokenInput from './TokenInput'
export const colorAtom = atom<string | undefined>(undefined)
const LoadingSpan = styled.span<{ $loading: boolean }>`
${loadingOpacityCss};
`
const OutputColumn = styled(Column)<{ hasColor: boolean | null }>`
background-color: ${({ theme }) => theme.module};
border-radius: ${({ theme }) => theme.borderRadius - 0.25}em;
padding: 0.75em;
padding-bottom: 0.5em;
position: relative;
// Set transitions to reduce color flashes when switching color/token.
// When color loads, transition the background so that it transitions from the empty or last state, but not _to_ the empty state.
transition: ${({ hasColor }) => (hasColor ? 'background-color 0.25s ease-out' : undefined)};
* {
> {
// When color is loading, delay the color/stroke so that it seems to transition from the last state.
transition: ${({ hasColor }) => (hasColor === null ? 'color 0.25s ease-in, stroke 0.25s ease-in' : undefined)};
}
@@ -33,32 +46,47 @@ interface OutputProps {
}
export default function Output({ disabled, children }: OutputProps) {
const input = useAtomValue(inputAtom)
const output = useAtomValue(outputAtom)
const setValue = useUpdateInputValue(outputAtom)
const setToken = useUpdateInputToken(outputAtom)
const balance = 123.45
const { i18n } = useLingui()
const {
trade: { state: tradeState },
currencyBalances: { [Field.OUTPUT]: balance },
currencyAmounts: { [Field.INPUT]: inputCurrencyAmount, [Field.OUTPUT]: outputCurrencyAmount },
} = useSwapInfo()
const [swapOutputAmount, updateSwapOutputAmount] = useSwapAmount(Field.OUTPUT)
const [swapOutputCurrency, updateSwapOutputCurrency] = useSwapCurrency(Field.OUTPUT)
//loading status of the trade
const isTradeLoading = useMemo(
() => TradeState.LOADING === tradeState || TradeState.SYNCING === tradeState,
[tradeState]
)
const isDependentField = useAtomValue(independentFieldAtom) !== Field.OUTPUT
const isLoading = isDependentField && isTradeLoading
const overrideColor = useAtomValue(colorAtom)
const dynamicColor = useColor(output.token)
usePrefetchColor(input.token) // extract eagerly in case of reversal
const dynamicColor = useCurrencyColor(swapOutputCurrency)
const color = overrideColor || dynamicColor
const hasColor = output.token ? Boolean(color) || null : false
const change = useMemo(() => {
if (input.usdc && output.usdc) {
const change = output.usdc / input.usdc - 1
const percent = (change * 100).toPrecision(3)
return change > 0 ? ` (+${percent}%)` : `(${percent}%)`
// 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 computedChange = computeFiatValuePriceImpact(inputUSDC, outputUSDC)
return computedChange ? parseFloat(computedChange.multiply(-1)?.toSignificant(3)) : undefined
}, [inputUSDC, outputUSDC])
const usdc = useMemo(() => {
if (outputUSDC) {
return `$${outputUSDC.toFixed(2)} (${priceImpact && priceImpact > 0 ? '+' : ''}${priceImpact}%)`
}
return ''
}, [input, output])
const usdc = useMemo(() => {
if (output.usdc) {
return `~ $${output.usdc.toLocaleString('en')}${change}`
}
return '-'
}, [change, output])
}, [priceImpact, outputUSDC])
return (
<DynamicThemeProvider color={color}>
@@ -68,19 +96,27 @@ export default function Output({ disabled, children }: OutputProps) {
<Trans>For</Trans>
</ThemedText.Subhead2>
</Row>
<TokenInput input={output} disabled={disabled} onChangeInput={setValue} onChangeToken={setToken}>
<TokenInput
currency={swapOutputCurrency}
amount={(swapOutputAmount !== undefined ? swapOutputAmount : outputCurrencyAmount?.toSignificant(6)) ?? ''}
disabled={disabled}
onChangeInput={updateSwapOutputAmount}
onChangeCurrency={updateSwapOutputCurrency}
loading={isLoading}
>
<ThemedText.Body2 color="secondary">
<Row>
{usdc}
<LoadingSpan $loading={isLoading}>{usdc}</LoadingSpan>
{balance && (
<span>
Balance: <span style={{ userSelect: 'text' }}>{balance}</span>
Balance: <span style={{ userSelect: 'text' }}>{formatCurrencyAmount(balance, 4, i18n.locale)}</span>
</span>
)}
</Row>
</ThemedText.Body2>
</TokenInput>
{children}
<BrandedFooter />
</OutputColumn>
</DynamicThemeProvider>
)

View File

@@ -1,6 +1,5 @@
import { useAtom } from 'jotai'
import { useSwitchSwapCurrencies } from 'lib/hooks/swap'
import { ArrowDown as ArrowDownIcon, ArrowUp as ArrowUpIcon } from 'lib/icons'
import { stateAtom } from 'lib/state/swap'
import styled, { Layer } from 'lib/theme'
import { useCallback, useState } from 'react'
@@ -47,16 +46,12 @@ const StyledReverseButton = styled(Button)<{ turns: number }>`
`
export default function ReverseButton({ disabled }: { disabled?: boolean }) {
const [state, setState] = useAtom(stateAtom)
const [turns, setTurns] = useState(0)
const switchCurrencies = useSwitchSwapCurrencies()
const onClick = useCallback(() => {
const { input, output } = state
setState((state) => {
state.input = output
state.output = input
})
switchCurrencies()
setTurns((turns) => ++turns)
}, [state, setState])
}, [switchCurrencies])
return (
<ReverseRow justify="center">

View File

@@ -1,9 +1,12 @@
import { t, Trans } from '@lingui/macro'
import { Trans } from '@lingui/macro'
import { Percent } from '@uniswap/sdk-core'
import { useAtom } from 'jotai'
import { Check, LargeIcon } from 'lib/icons'
import { MaxSlippage, maxSlippageAtom } from 'lib/state/swap'
import styled, { ThemedText } from 'lib/theme'
import { ReactNode, useCallback, useRef } from 'react'
import Popover from 'lib/components/Popover'
import { TooltipHandlers, useTooltip } from 'lib/components/Tooltip'
import { AlertTriangle, Check, Icon, LargeIcon, XOctagon } from 'lib/icons'
import { autoSlippageAtom, MAX_VALID_SLIPPAGE, maxSlippageAtom, MIN_HIGH_SLIPPAGE } from 'lib/state/settings'
import styled, { Color, ThemedText } from 'lib/theme'
import { memo, PropsWithChildren, ReactNode, useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { BaseButton, TextButton } from '../../Button'
import Column from '../../Column'
@@ -14,77 +17,156 @@ import { Label, optionCss } from './components'
const tooltip = (
<Trans>Your transaction will revert if the price changes unfavorably by more than this percentage.</Trans>
)
const highSlippage = <Trans>High slippage increases the risk of price movement</Trans>
const invalidSlippage = <Trans>Please enter a valid slippage %</Trans>
const StyledOption = styled(TextButton)<{ selected: boolean }>`
const placeholder = '0.10'
const Button = styled(TextButton)<{ selected: boolean }>`
${({ selected }) => optionCss(selected)}
`
const StyledInputOption = styled(BaseButton)<{ selected: boolean }>`
const Custom = styled(BaseButton)<{ selected: boolean }>`
${({ selected }) => optionCss(selected)}
${inputCss}
border-color: ${({ selected, theme }) => (selected ? theme.active : 'transparent')} !important;
padding: calc(0.5em - 1px) 0.625em;
padding: calc(0.75em - 3px) 0.625em;
`
interface OptionProps<T> {
value: T
interface OptionProps extends Partial<TooltipHandlers> {
wrapper: typeof Button | typeof Custom
selected: boolean
onSelect: (value: T) => void
onSelect: () => void
icon?: ReactNode
}
function Option<T>({ value, selected, onSelect }: OptionProps<T>) {
function Option({
wrapper: Wrapper,
children,
selected,
onSelect,
icon,
...tooltipHandlers
}: PropsWithChildren<OptionProps>) {
return (
<StyledOption selected={selected} onClick={() => onSelect(value)}>
<Row>
<ThemedText.Subhead2>{value}%</ThemedText.Subhead2>
{selected && <LargeIcon icon={Check} />}
<Wrapper selected={selected} onClick={onSelect} {...tooltipHandlers}>
<Row gap={0.5}>
{children}
{icon ? icon : <LargeIcon icon={selected ? Check : undefined} size={1.25} />}
</Row>
</StyledOption>
</Wrapper>
)
}
function InputOption<T>({ value, children, selected, onSelect }: OptionProps<T> & { children: ReactNode }) {
return (
<StyledInputOption color="container" selected={selected} onClick={() => onSelect(value)}>
<ThemedText.Subhead2>
<Row>{children}</Row>
</ThemedText.Subhead2>
</StyledInputOption>
)
enum WarningState {
NONE,
HIGH_SLIPPAGE,
INVALID_SLIPPAGE,
}
const Warning = memo(function Warning({ state, showTooltip }: { state: WarningState; showTooltip: boolean }) {
let icon: Icon
let color: Color
let content: ReactNode
let show = showTooltip
switch (state) {
case WarningState.INVALID_SLIPPAGE:
icon = XOctagon
color = 'error'
content = invalidSlippage
show = true
break
case WarningState.HIGH_SLIPPAGE:
icon = AlertTriangle
color = 'warning'
content = highSlippage
break
case WarningState.NONE:
return null
}
return (
<Popover
key={state}
content={<ThemedText.Caption>{content}</ThemedText.Caption>}
show={show}
placement="top"
offset={16}
contained
>
<LargeIcon icon={icon} color={color} size={1.25} />
</Popover>
)
})
export default function MaxSlippageSelect() {
const { P01, P05, CUSTOM } = MaxSlippage
const [{ value: maxSlippage, custom }, setMaxSlippage] = useAtom(maxSlippageAtom)
const input = useRef<HTMLInputElement>(null)
const focus = useCallback(() => input.current?.focus(), [input])
const onInputSelect = useCallback(
(custom) => {
focus()
if (custom !== undefined) {
setMaxSlippage({ value: CUSTOM, custom })
const [autoSlippage, setAutoSlippage] = useAtom(autoSlippageAtom)
const [maxSlippage, setMaxSlippage] = useAtom(maxSlippageAtom)
const maxSlippageInput = useMemo(() => maxSlippage?.toString() || '', [maxSlippage])
const [warning, setWarning] = useState(WarningState.NONE)
const [showTooltip, setShowTooltip, tooltipProps] = useTooltip()
const processInput = useCallback(
(input: number | undefined) => {
const numerator = input && Math.floor(input * 100)
if (numerator) {
const percent = new Percent(numerator, 10_000)
if (percent.greaterThan(MAX_VALID_SLIPPAGE)) {
setWarning(WarningState.INVALID_SLIPPAGE)
setAutoSlippage(true)
setMaxSlippage(input)
} else if (percent.greaterThan(MIN_HIGH_SLIPPAGE)) {
setWarning(WarningState.HIGH_SLIPPAGE)
setAutoSlippage(false)
setMaxSlippage(input)
} else {
setWarning(WarningState.NONE)
setAutoSlippage(false)
setMaxSlippage(input)
}
} else {
setAutoSlippage(true)
setMaxSlippage(undefined)
}
},
[CUSTOM, focus, setMaxSlippage]
[setAutoSlippage, setMaxSlippage]
)
const onInputSelect = useCallback(() => {
focus()
processInput(maxSlippage)
}, [focus, maxSlippage, processInput])
useEffect(() => processInput(maxSlippage), [maxSlippage, processInput]) // processes any warnings on mount
useEffect(() => setShowTooltip(true), [warning, setShowTooltip]) // enables the tooltip if a warning is set
return (
<Column gap={0.75}>
<Label name={<Trans>Max slippage</Trans>} tooltip={tooltip} />
<Row gap={0.5} grow>
<Option value={P01} onSelect={setMaxSlippage} selected={maxSlippage === P01} />
<Option value={P05} onSelect={setMaxSlippage} selected={maxSlippage === P05} />
<InputOption value={custom} onSelect={onInputSelect} selected={maxSlippage === CUSTOM}>
<DecimalInput
size={custom === undefined ? undefined : 5}
value={custom}
onChange={(custom) => setMaxSlippage({ value: CUSTOM, custom })}
placeholder={t`Custom`}
ref={input}
/>
%
</InputOption>
<Row gap={0.5} grow="last">
<Option wrapper={Button} selected={autoSlippage} onSelect={() => setAutoSlippage(true)}>
<ThemedText.ButtonMedium>
<Trans>Auto</Trans>
</ThemedText.ButtonMedium>
</Option>
<Option
wrapper={Custom}
selected={!autoSlippage}
onSelect={onInputSelect}
icon={<Warning state={warning} showTooltip={showTooltip} />}
{...tooltipProps}
>
<Row color={warning === WarningState.INVALID_SLIPPAGE ? 'error' : undefined}>
<DecimalInput
size={Math.max(maxSlippageInput.length, 3)}
value={maxSlippageInput}
onChange={(input) => processInput(+input)}
placeholder={placeholder}
ref={input}
/>
%
</Row>
</Option>
</Row>
</Column>
)

View File

@@ -1,6 +1,6 @@
import { Trans } from '@lingui/macro'
import { useAtom } from 'jotai'
import { mockTogglableAtom } from 'lib/state/swap'
import { mockTogglableAtom } from 'lib/state/settings'
import Row from '../../Row'
import Toggle from '../../Toggle'

View File

@@ -1,6 +1,5 @@
import { Trans } from '@lingui/macro'
import { useAtom } from 'jotai'
import { TRANSACTION_TTL_DEFAULT, transactionTtlAtom } from 'lib/state/swap'
import { useDefaultTransactionTtl, useTransactionTtl } from 'lib/hooks/useTransactionDeadline'
import styled, { ThemedText } from 'lib/theme'
import { useRef } from 'react'
@@ -16,17 +15,20 @@ const Input = styled(Row)`
`
export default function TransactionTtlInput() {
const [transactionTtl, setTransactionTtl] = useAtom(transactionTtlAtom)
const [ttl, setTtl] = useTransactionTtl()
const defaultTtl = useDefaultTransactionTtl()
const placeholder = defaultTtl.toString()
const input = useRef<HTMLInputElement>(null)
return (
<Column gap={0.75}>
<Label name={<Trans>Transaction deadline</Trans>} tooltip={tooltip} />
<ThemedText.Body1>
<Input onClick={() => input.current?.focus()}>
<Input justify="start" onClick={() => input.current?.focus()}>
<IntegerInput
placeholder={TRANSACTION_TTL_DEFAULT.toString()}
value={transactionTtl}
onChange={(value) => setTransactionTtl(value ?? 0)}
placeholder={placeholder}
value={ttl?.toString() ?? ''}
onChange={(value) => setTtl(value ? parseFloat(value) : 0)}
size={Math.max(ttl?.toString().length || 0, placeholder.length)}
ref={input}
/>
<Trans>minutes</Trans>

View File

@@ -12,11 +12,15 @@ export const optionCss = (selected: boolean) => css`
color: ${({ theme }) => theme.primary} !important;
display: grid;
grid-gap: 0.25em;
padding: 0.5em 0.625em;
padding: calc(0.75em - 1px) 0.625em;
:enabled:hover {
border-color: ${({ theme }) => theme.onHover(selected ? theme.active : theme.outline)};
}
:enabled:focus-within {
border-color: ${({ theme }) => theme.active};
}
`
export function value(Value: AnyStyledComponent) {

View File

@@ -2,7 +2,7 @@ import { Trans } from '@lingui/macro'
import { useResetAtom } from 'jotai/utils'
import useScrollbar from 'lib/hooks/useScrollbar'
import { Settings as SettingsIcon } from 'lib/icons'
import { settingsAtom } from 'lib/state/swap'
import { settingsAtom } from 'lib/state/settings'
import styled, { ThemedText } from 'lib/theme'
import React, { useState } from 'react'

View File

@@ -1,56 +1,8 @@
import { useUpdateAtom } from 'jotai/utils'
import { DAI, ETH } from 'lib/mocks'
import { transactionAtom } from 'lib/state/swap'
import { useEffect } from 'react'
import { useSelect } from 'react-cosmos/fixture'
import invariant from 'tiny-invariant'
import { Modal } from '../Dialog'
import { StatusDialog } from './Status'
function Fixture() {
const setTransaction = useUpdateAtom(transactionAtom)
const [state] = useSelect('state', {
options: ['PENDING', 'ERROR', 'SUCCESS'],
})
useEffect(() => {
setTransaction({
input: { token: ETH, value: 1 },
output: { token: DAI, value: 4200 },
receipt: '',
timestamp: Date.now(),
})
}, [setTransaction])
useEffect(() => {
switch (state) {
case 'PENDING':
setTransaction({
input: { token: ETH, value: 1 },
output: { token: DAI, value: 4200 },
receipt: '',
timestamp: Date.now(),
})
break
case 'ERROR':
setTransaction((tx) => {
invariant(tx)
tx.status = new Error(
'Swap failed: Unknown error: "Error: Lorem ipsum dolor sit amet, consectetur adipiscing elit. Praesent pulvinar, risus eu pretium condimentum, tellus dui fermentum turpis, id gravida metus justo ac lorem. Etiam vitae dapibus eros, nec elementum ipsum. Duis condimentum, felis vel tempor ultricies, eros diam tempus odio, at tempor urna odio id massa. Aliquam laoreet turpis justo, auctor accumsan est pellentesque at. Integer et dolor feugiat, sodales tortor non, cursus augue. Phasellus id suscipit justo, in ultricies tortor. Aenean libero nibh, egestas sit amet vehicula sit amet, tempor ac ligula. Cras at tempor lectus. Mauris sollicitudin est velit, nec consectetur lorem dapibus ut. Praesent magna ex, faucibus ac fermentum malesuada, molestie at ex. Phasellus bibendum lorem nec dolor dignissim eleifend. Nam dignissim varius velit, at volutpat justo pretium id."'
)
tx.elapsedMs = Date.now() - tx.timestamp
})
break
case 'SUCCESS':
setTransaction((tx) => {
invariant(tx)
tx.status = true
tx.elapsedMs = Date.now() - tx.timestamp
})
break
}
}, [setTransaction, state])
return <StatusDialog onClose={() => void 0} />
return null
// TODO(zzmp): Mock <StatusDialog tx={} onClose={() => void 0} />
}
export default (

View File

@@ -1,9 +1,8 @@
import { Trans } from '@lingui/macro'
import { useAtomValue } from 'jotai/utils'
import ErrorDialog, { StatusHeader } from 'lib/components/Error/ErrorDialog'
import useInterval from 'lib/hooks/useInterval'
import { CheckCircle, Clock, Spinner } from 'lib/icons'
import { Transaction, transactionAtom } from 'lib/state/swap'
import { SwapTransactionInfo, Transaction } from 'lib/state/transactions'
import styled, { ThemedText } from 'lib/theme'
import { useCallback, useMemo, useState } from 'react'
@@ -24,17 +23,17 @@ const TransactionRow = styled(Row)`
flex-direction: row-reverse;
`
function ElapsedTime({ tx }: { tx: Transaction | null }) {
function ElapsedTime({ tx }: { tx: Transaction<SwapTransactionInfo> }) {
const [elapsedMs, setElapsedMs] = useState(0)
useInterval(
() => {
if (tx?.elapsedMs) {
setElapsedMs(tx.elapsedMs)
} else if (tx?.timestamp) {
setElapsedMs(Date.now() - tx.timestamp)
if (tx.info.response.timestamp) {
setElapsedMs(tx.info.response.timestamp - tx.addedTime)
} else {
setElapsedMs(Date.now() - tx.addedTime)
}
},
elapsedMs === tx?.elapsedMs ? null : 1000
elapsedMs === tx.info.response.timestamp ? null : 1000
)
const toElapsedTime = useCallback((ms: number) => {
let sec = Math.floor(ms / 1000)
@@ -63,22 +62,23 @@ const EtherscanA = styled.a`
text-decoration: none;
`
interface TransactionStatusProps extends StatusProps {
tx: Transaction | null
interface TransactionStatusProps {
tx: Transaction<SwapTransactionInfo>
onClose: () => void
}
function TransactionStatus({ tx, onClose }: TransactionStatusProps) {
const Icon = useMemo(() => {
return tx?.status ? CheckCircle : Spinner
}, [tx?.status])
return tx.receipt?.status ? CheckCircle : Spinner
}, [tx.receipt?.status])
const heading = useMemo(() => {
return tx?.status ? <Trans>Transaction submitted</Trans> : <Trans>Transaction pending</Trans>
}, [tx?.status])
return tx.receipt?.status ? <Trans>Transaction submitted</Trans> : <Trans>Transaction pending</Trans>
}, [tx.receipt?.status])
return (
<Column flex padded gap={0.75} align="stretch" style={{ height: '100%' }}>
<StatusHeader icon={Icon} iconColor={tx?.status && 'success'}>
<StatusHeader icon={Icon} iconColor={tx.receipt?.status ? 'success' : undefined}>
<ThemedText.Subhead1>{heading}</ThemedText.Subhead1>
{tx ? <Summary input={tx.input} output={tx.output} /> : <div style={{ height: '1.25em' }} />}
<Summary input={tx.info.inputCurrencyAmount} output={tx.info.outputCurrencyAmount} />
</StatusHeader>
<TransactionRow flex>
<ThemedText.ButtonSmall>
@@ -95,15 +95,14 @@ function TransactionStatus({ tx, onClose }: TransactionStatusProps) {
)
}
interface StatusProps {
onClose: () => void
}
export default function TransactionStatusDialog({ onClose }: StatusProps) {
const tx = useAtomValue(transactionAtom)
return tx?.status instanceof Error ? (
<ErrorDialog header={errorMessage} error={tx.status} action={<Trans>Dismiss</Trans>} onAction={onClose} />
export default function TransactionStatusDialog({ tx, onClose }: TransactionStatusProps) {
return tx.receipt?.status === 0 ? (
<ErrorDialog
header={errorMessage}
error={new Error('TODO(zzmp)')}
action={<Trans>Dismiss</Trans>}
onAction={onClose}
/>
) : (
<TransactionStatus tx={tx} onClose={onClose} />
)

View File

@@ -1,43 +1,50 @@
import { tokens } from '@uniswap/default-token-list'
import { SupportedChainId } from 'constants/chains'
import { nativeOnChain } from 'constants/tokens'
import { useUpdateAtom } from 'jotai/utils'
import { DAI, ETH } from 'lib/mocks'
import { Field, outputAtom, stateAtom } from 'lib/state/swap'
import { useEffect, useState } from 'react'
import { useValue } from 'react-cosmos/fixture'
import { useSwapInfo } from 'lib/hooks/swap'
import { SwapInfoUpdater } from 'lib/hooks/swap/useSwapInfo'
import { Field, swapAtom } from 'lib/state/swap'
import { useEffect } from 'react'
import { WrappedTokenInfo } from 'state/lists/wrappedTokenInfo'
import invariant from 'tiny-invariant'
import { Modal } from '../Dialog'
import { SummaryDialog } from './Summary'
const ETH = nativeOnChain(SupportedChainId.MAINNET)
const UNI = (function () {
const token = tokens.find(({ symbol }) => symbol === 'UNI')
invariant(token)
return new WrappedTokenInfo(token)
})()
function Fixture() {
const setState = useUpdateAtom(stateAtom)
const [, setInitialized] = useState(false)
// eslint-disable-next-line react-hooks/exhaustive-deps
const setState = useUpdateAtom(swapAtom)
const {
allowedSlippage,
trade: { trade },
} = useSwapInfo()
useEffect(() => {
setState({
activeInput: Field.INPUT,
input: { token: ETH, value: 1, usdc: 4195 },
output: { token: DAI, value: 4200, usdc: 4200 },
swap: {
lpFee: 0.0005,
integratorFee: 0.00025,
priceImpact: 0.01,
slippageTolerance: 0.5,
minimumReceived: 4190,
},
independentField: Field.INPUT,
amount: '1',
[Field.INPUT]: ETH,
[Field.OUTPUT]: UNI,
})
setInitialized(true)
})
}, [setState])
const setOutput = useUpdateAtom(outputAtom)
const [price] = useValue('output value', { defaultValue: 4200 })
useEffect(() => {
setState((state) => ({ ...state, output: { token: DAI, value: price, usdc: price } }))
}, [price, setOutput, setState])
return (
return trade ? (
<Modal color="dialog">
<SummaryDialog onConfirm={() => void 0} />
<SummaryDialog onConfirm={() => void 0} trade={trade} allowedSlippage={allowedSlippage} />
</Modal>
)
) : null
}
export default <Fixture />
export default (
<>
<SwapInfoUpdater />
<Fixture />
</>
)

View File

@@ -1,57 +1,119 @@
import { t } from '@lingui/macro'
import { State } from 'lib/state/swap'
import { ThemedText } from 'lib/theme'
import { Token } from 'lib/types'
import { useLingui } from '@lingui/react'
import { Trade } from '@uniswap/router-sdk'
import { Currency, Percent, TradeType } from '@uniswap/sdk-core'
import { ALLOWED_PRICE_IMPACT_HIGH, ALLOWED_PRICE_IMPACT_MEDIUM } from 'constants/misc'
import { useAtomValue } from 'jotai/utils'
import { MIN_HIGH_SLIPPAGE } from 'lib/state/settings'
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 } from 'utils/prices'
import Row from '../../Row'
const Value = styled.span<{ color?: Color }>`
color: ${({ color, theme }) => color && theme[color]};
white-space: nowrap;
`
interface DetailProps {
label: string
value: string
color?: Color
}
function Detail({ label, value }: DetailProps) {
function Detail({ label, value, color }: DetailProps) {
return (
<ThemedText.Caption>
<Row gap={2}>
<span>{label}</span>
<span style={{ whiteSpace: 'nowrap' }}>{value}</span>
<Value color={color}>{value}</Value>
</Row>
</ThemedText.Caption>
)
}
interface DetailsProps {
swap: Required<State>['swap']
input: Token
output: Token
trade: Trade<Currency, Currency, TradeType>
allowedSlippage: Percent
}
export default function Details({
input: { symbol: inputSymbol },
output: { symbol: outputSymbol },
swap,
}: DetailsProps) {
const integrator = window.location.hostname
const details = useMemo((): [string, string][] => {
return [
[t`Liquidity provider fee`, `${swap.lpFee} ${inputSymbol}`],
[t`${integrator} fee`, swap.integratorFee && `${swap.integratorFee} ${inputSymbol}`],
[t`Price impact`, `${swap.priceImpact}%`],
[t`Maximum sent`, swap.maximumSent && `${swap.maximumSent} ${inputSymbol}`],
[t`Minimum received`, swap.minimumReceived && `${swap.minimumReceived} ${outputSymbol}`],
[t`Slippage tolerance`, `${swap.slippageTolerance}%`],
].filter(isDetail)
export default function Details({ trade, allowedSlippage }: DetailsProps) {
const { inputAmount, outputAmount } = trade
const inputCurrency = inputAmount.currency
const outputCurrency = outputAmount.currency
const priceImpact = useMemo(() => computeRealizedPriceImpact(trade), [trade])
function isDetail(detail: unknown[]): detail is [string, string] {
return Boolean(detail[1])
const lpFeeAmount = useMemo(() => computeRealizedLPFeeAmount(trade), [trade])
const integrator = window.location.hostname
const feeOptions = useAtomValue(feeOptionsAtom)
const { i18n } = useLingui()
const details = useMemo(() => {
const rows = []
// @TODO(ianlapham): Check that provider fee is even a valid list item
if (feeOptions) {
const parsedConvenienceFee = formatCurrencyAmount(outputAmount.multiply(feeOptions.fee), 6, i18n.locale)
rows.push([
t`${integrator} fee`,
`${parsedConvenienceFee} ${outputCurrency.symbol || currencyId(outputCurrency)}`,
])
}
}, [inputSymbol, outputSymbol, swap, integrator])
const priceImpactRow = [t`Price impact`, `${priceImpact.toFixed(2)}%`]
if (priceImpact.greaterThan(ALLOWED_PRICE_IMPACT_HIGH)) {
priceImpactRow.push('error')
} else if (priceImpact.greaterThan(ALLOWED_PRICE_IMPACT_MEDIUM)) {
priceImpactRow.push('warning')
}
rows.push(priceImpactRow)
if (lpFeeAmount) {
const localizedFeeAmount = formatCurrencyAmount(lpFeeAmount, 6, i18n.locale)
rows.push([
t`Liquidity provider fee`,
`${localizedFeeAmount} ${inputCurrency.symbol || currencyId(inputCurrency)}`,
])
}
if (trade.tradeType === TradeType.EXACT_OUTPUT) {
const localizedMaxSent = formatCurrencyAmount(trade.maximumAmountIn(allowedSlippage), 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)
rows.push([t`Minimum received`, `${localizedMaxSent} ${outputCurrency.symbol}`])
}
const slippageToleranceRow = [t`Slippage tolerance`, `${allowedSlippage.toFixed(2)}%`]
if (allowedSlippage.greaterThan(MIN_HIGH_SLIPPAGE)) {
slippageToleranceRow.push('warning')
}
rows.push(slippageToleranceRow)
return rows
}, [
feeOptions,
priceImpact,
lpFeeAmount,
trade,
allowedSlippage,
outputAmount,
i18n.locale,
integrator,
outputCurrency,
inputCurrency,
])
return (
<>
{details.map(([label, detail]) => (
<Detail key={label} label={label} value={detail} />
{details.map(([label, detail, color]) => (
<Detail key={label} label={label} value={detail} color={color as Color} />
))}
</>
)

View File

@@ -1,8 +1,12 @@
import { useLingui } from '@lingui/react'
import { Currency, CurrencyAmount } from '@uniswap/sdk-core'
import { useUSDCValue } from 'hooks/useUSDCPrice'
import { ArrowRight } from 'lib/icons'
import { Input } from 'lib/state/swap'
import styled from 'lib/theme'
import { ThemedText } from 'lib/theme'
import { useMemo } from 'react'
import { computeFiatValuePriceImpact } from 'utils/computeFiatValuePriceImpact'
import { formatCurrencyAmount } from 'utils/formatCurrencyAmount'
import Column from '../../Column'
import Row from '../../Row'
@@ -13,31 +17,35 @@ const Percent = styled.span<{ gain: boolean }>`
`
interface TokenValueProps {
input: Required<Pick<Input, 'token' | 'value'>> & Input
input: CurrencyAmount<Currency>
usdc?: boolean
change?: number
}
function TokenValue({ input, usdc, change }: TokenValueProps) {
const { i18n } = useLingui()
const percent = useMemo(() => {
if (change) {
const percent = (change * 100).toPrecision(3)
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.token} />
<TokenImg token={input.currency} />
<ThemedText.Body2>
{input.value} {input.token.symbol}
{formatCurrencyAmount(input, 6, i18n.locale)} {input.currency.symbol}
</ThemedText.Body2>
</Row>
{usdc && input.usdc && (
{usdc && usdcAmount && (
<Row justify="flex-start">
<ThemedText.Caption color="secondary">
~ ${input.usdc.toLocaleString('en')}
${formatCurrencyAmount(usdcAmount, 2, i18n.locale)}
{change && <Percent gain={change > 0}> {percent}</Percent>}
</ThemedText.Caption>
</Row>
@@ -47,23 +55,25 @@ function TokenValue({ input, usdc, change }: TokenValueProps) {
}
interface SummaryProps {
input: Required<Pick<Input, 'token' | 'value'>> & Input
output: Required<Pick<Input, 'token' | 'value'>> & Input
input: CurrencyAmount<Currency>
output: CurrencyAmount<Currency>
usdc?: boolean
}
export default function Summary({ input, output, usdc }: SummaryProps) {
const change = useMemo(() => {
if (usdc && input.usdc && output.usdc) {
return output.usdc / input.usdc - 1
}
return undefined
}, [usdc, input.usdc, output.usdc])
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])
return (
<Row gap={usdc ? 1 : 0.25}>
<TokenValue input={input} usdc={usdc} />
<ArrowRight />
<TokenValue input={output} usdc={usdc} change={change} />
<TokenValue input={output} usdc={usdc} change={priceImpact} />
</Row>
)
}

View File

@@ -1,11 +1,20 @@
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 { ALLOWED_PRICE_IMPACT_HIGH, ALLOWED_PRICE_IMPACT_MEDIUM } from 'constants/misc'
import { useAtomValue } from 'jotai/utils'
import { IconButton } from 'lib/components/Button'
import useScrollbar from 'lib/hooks/useScrollbar'
import { Expando, Info } from 'lib/icons'
import { Input, inputAtom, outputAtom, swapAtom } from 'lib/state/swap'
import { AlertTriangle, Expando, Info } from 'lib/icons'
import { MIN_HIGH_SLIPPAGE } from 'lib/state/settings'
import { Field, independentFieldAtom } from 'lib/state/swap'
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 } from 'utils/prices'
import { tradeMeaningfullyDiffers } from 'utils/tradeMeaningFullyDiffer'
import ActionButton from '../../ActionButton'
import Column from '../../Column'
@@ -17,12 +26,6 @@ import Summary from './Summary'
export default Summary
function asInput(input: Input): (Required<Pick<Input, 'token' | 'value'>> & Input) | undefined {
return input.token && input.value ? (input as Required<Pick<Input, 'token' | 'value'>>) : undefined
}
const updated = { message: <Trans>Price updated</Trans>, action: <Trans>Accept</Trans> }
const SummaryColumn = styled(Column)``
const ExpandoColumn = styled(Column)``
const DetailsColumn = styled(Column)``
@@ -47,6 +50,7 @@ const Body = styled(Column)<{ open: boolean }>`
${Column} {
height: 100%;
grid-template-rows: repeat(auto-fill, 1em);
padding: ${({ open }) => (open ? '0.5em 0' : 0)};
transition: padding 0.25s;
@@ -64,6 +68,7 @@ const Body = styled(Column)<{ open: boolean }>`
${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 }) =>
@@ -74,29 +79,43 @@ const Body = styled(Column)<{ open: boolean }>`
}
`
const priceUpdate = { message: <Trans>Price updated</Trans>, action: <Trans>Accept</Trans> }
interface SummaryDialogProps {
trade: Trade<Currency, Currency, TradeType>
allowedSlippage: Percent
onConfirm: () => void
}
export function SummaryDialog({ onConfirm }: SummaryDialogProps) {
const swap = useAtomValue(swapAtom)
const partialInput = useAtomValue(inputAtom)
const partialOutput = useAtomValue(outputAtom)
const input = asInput(partialInput)
const output = asInput(partialOutput)
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 price = useMemo(() => {
return input && output ? output.value / input.value : undefined
}, [input, output])
const [confirmedPrice, confirmPrice] = useState(price)
const independentField = useAtomValue(independentFieldAtom)
const [open, setOpen] = useState(true)
const warning = useMemo(() => {
if (priceImpact.greaterThan(ALLOWED_PRICE_IMPACT_HIGH)) return 'error'
if (priceImpact.greaterThan(ALLOWED_PRICE_IMPACT_MEDIUM)) return 'warning'
if (allowedSlippage.greaterThan(MIN_HIGH_SLIPPAGE)) return 'warning'
return
}, [allowedSlippage, priceImpact])
const [confirmedTrade, setConfirmedTrade] = useState(trade)
const doesTradeDiffer = useMemo(
() => Boolean(trade && confirmedTrade && tradeMeaningfullyDiffers(trade, confirmedTrade)),
[confirmedTrade, trade]
)
const [open, setOpen] = useState(false)
const [details, setDetails] = useState<HTMLDivElement | null>(null)
const scrollbar = useScrollbar(details)
if (!(input && output && swap)) {
const { i18n } = useLingui()
if (!(inputAmount && outputAmount && inputCurrency && outputCurrency)) {
return null
}
@@ -105,15 +124,16 @@ export function SummaryDialog({ onConfirm }: SummaryDialogProps) {
<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={input} output={output} usdc={true} />
<Summary input={inputAmount} output={outputAmount} usdc={true} />
<ThemedText.Caption>
1 {input.token.symbol} = {price} {output.token.symbol}
{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}>
<Info color="secondary" />
{warning ? <AlertTriangle color={warning} /> : <Info color="secondary" />}
<ThemedText.Subhead2 color="secondary">
<Trans>Swap details</Trans>
</ThemedText.Subhead2>
@@ -124,26 +144,29 @@ export function SummaryDialog({ onConfirm }: SummaryDialogProps) {
<Rule />
<DetailsColumn>
<Column gap={0.5} ref={setDetails} css={scrollbar}>
<Details input={input.token} output={output.token} swap={swap} />
<Details trade={trade} allowedSlippage={allowedSlippage} />
</Column>
</DetailsColumn>
<Estimate color="secondary">
<Trans>Output is estimated.</Trans>{' '}
{swap?.minimumReceived && (
<Trans>Output is estimated.</Trans>
{independentField === Field.INPUT && (
<Trans>
You will receive at least {swap.minimumReceived} {output.token.symbol} or the transaction will revert.
You will receive at least{' '}
{formatCurrencyAmount(trade.minimumAmountOut(allowedSlippage), 6, i18n.locale)} {outputCurrency.symbol}{' '}
or the transaction will revert.
</Trans>
)}
{swap?.maximumSent && (
{independentField === Field.OUTPUT && (
<Trans>
You will send at most {swap.maximumSent} {input.token.symbol} or the transaction will revert.
You will send at most {formatCurrencyAmount(trade.maximumAmountIn(allowedSlippage), 6, i18n.locale)}{' '}
{inputCurrency.symbol} or the transaction will revert.
</Trans>
)}
</Estimate>
<ActionButton
onClick={onConfirm}
onUpdate={() => confirmPrice(price)}
updated={price === confirmedPrice ? undefined : updated}
onUpdate={() => setConfirmedTrade(trade)}
update={doesTradeDiffer ? priceUpdate : undefined}
>
<Trans>Confirm swap</Trans>
</ActionButton>

View File

@@ -1,8 +1,8 @@
import { useAtom } from 'jotai'
import { tokens } from '@uniswap/default-token-list'
import { DAI, USDC } from 'constants/tokens'
import { useUpdateAtom } from 'jotai/utils'
import { inputAtom, outputAtom, swapAtom } from 'lib/state/swap'
import { useEffect } from 'react'
import { useValue } from 'react-cosmos/fixture'
import { useSelect, useValue } from 'react-cosmos/fixture'
import Swap from '.'
import { colorAtom } from './Output'
@@ -17,42 +17,6 @@ const validateColor = (() => {
})()
function Fixture() {
const [input, setInput] = useAtom(inputAtom)
const [output, setOutput] = useAtom(outputAtom)
const [swap, setSwap] = useAtom(swapAtom)
const [priceFetched] = useValue('price fetched', { defaultValue: false })
useEffect(() => {
if (priceFetched && input.token && output.token) {
const inputValue = input.value || 1
const inputUsdc = input.usdc || inputValue
const outputValue = output.value || 1
const outputUsdc = output.usdc || outputValue
if (!(inputValue === input.value && inputUsdc === input.usdc)) {
setInput({ ...input, value: inputValue, usdc: inputUsdc })
}
if (!(outputValue === output.value && outputUsdc === output.usdc)) {
setOutput({ ...output, value: outputValue, usdc: outputUsdc })
}
if (!swap || swap.minimumReceived !== outputValue * 0.995) {
setSwap({
lpFee: 0.0005,
priceImpact: 0.01,
slippageTolerance: 0.5,
minimumReceived: outputValue * 0.995,
})
}
} else if (swap) {
setSwap(undefined)
}
}, [input, output, priceFetched, setInput, setOutput, setSwap, swap])
const [tokenApproved] = useValue('token approved', { defaultValue: true })
useEffect(() => {
if (tokenApproved !== input.approved) {
setInput({ ...input, approved: tokenApproved })
}
}, [input, setInput, tokenApproved])
const setColor = useUpdateAtom(colorAtom)
const [color] = useValue('token color', { defaultValue: '' })
useEffect(() => {
@@ -61,7 +25,54 @@ function Fixture() {
}
}, [color, setColor])
return <Swap />
const [convenienceFee] = useValue('convenienceFee', { defaultValue: 100 })
const FEE_RECIPIENT_OPTIONS = [
'',
'0x1D9Cd50Dde9C19073B81303b3d930444d11552f7',
'0x0dA5533d5a9aA08c1792Ef2B6a7444E149cCB0AD',
'0xE6abE059E5e929fd17bef158902E73f0FEaCD68c',
]
const [convenienceFeeRecipient] = useSelect('convenienceFeeRecipient', {
options: FEE_RECIPIENT_OPTIONS,
defaultValue: FEE_RECIPIENT_OPTIONS[1],
})
const optionsToAddressMap: Record<string, string> = {
none: '',
Native: 'NATIVE',
DAI: DAI.address,
USDC: USDC.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', {
options: addressOptions,
defaultValue: addressOptions[1],
})
const [defaultOutputAmount] = useSelect('defaultOutputAmount', {
options: inputOptions,
defaultValue: inputOptions[0],
})
return (
<Swap
convenienceFee={convenienceFee}
convenienceFeeRecipient={convenienceFeeRecipient}
defaultInputAddress={optionsToAddressMap[defaultInput]}
defaultInputAmount={defaultInputAmount}
defaultOutputAddress={optionsToAddressMap[defaultOutput]}
defaultOutputAmount={defaultOutputAmount}
tokenList={tokens}
/>
)
}
export default <Fixture />

View File

@@ -1,56 +1,173 @@
import { Trans } from '@lingui/macro'
import { Token, TradeType } from '@uniswap/sdk-core'
import { CHAIN_INFO } from 'constants/chainInfo'
import { useERC20PermitFromTrade } from 'hooks/useERC20Permit'
import { useUpdateAtom } from 'jotai/utils'
import { useAtomValue } from 'jotai/utils'
import { inputAtom, outputAtom, swapAtom } from 'lib/state/swap'
import { useCallback, useMemo, useState } from 'react'
import { useSwapInfo } from 'lib/hooks/swap'
import useSwapApproval, {
ApprovalState,
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 useActiveWeb3React from 'lib/hooks/useActiveWeb3React'
import useTransactionDeadline from 'lib/hooks/useTransactionDeadline'
import { Link, Spinner } from 'lib/icons'
import { displayTxHashAtom, Field, independentFieldAtom } from 'lib/state/swap'
import { TransactionType } from 'lib/state/transactions'
import styled, { useTheme } from 'lib/theme'
import { useCallback, useEffect, useMemo, useState } from 'react'
import invariant from 'tiny-invariant'
import ActionButton from '../ActionButton'
import Dialog from '../Dialog'
import { StatusDialog } from './Status'
import Row from '../Row'
import { SummaryDialog } from './Summary'
const mockBalance = 123.45
enum Mode {
NONE,
SUMMARY,
STATUS,
interface SwapButtonProps {
disabled?: boolean
}
export default function SwapButton() {
const swap = useAtomValue(swapAtom)
const input = useAtomValue(inputAtom)
const output = useAtomValue(outputAtom)
const balance = mockBalance
const [mode, setMode] = useState(Mode.NONE)
const EtherscanA = styled.a`
color: currentColor;
text-decoration: none;
`
function useIsPendingApproval(token?: Token, spender?: string): boolean {
return Boolean(usePendingApproval(token, spender))
}
export default function SwapButton({ disabled }: SwapButtonProps) {
const { account, chainId } = useActiveWeb3React()
const { tokenColorExtraction } = useTheme()
const {
trade,
allowedSlippage,
currencies: { [Field.INPUT]: inputCurrency },
currencyBalances: { [Field.INPUT]: inputCurrencyBalance },
currencyAmounts: { [Field.INPUT]: inputCurrencyAmount, [Field.OUTPUT]: outputCurrencyAmount },
feeOptions,
} = useSwapInfo()
const independentField = useAtomValue(independentFieldAtom)
const [activeTrade, setActiveTrade] = useState<typeof trade.trade | undefined>()
useEffect(() => {
setActiveTrade((activeTrade) => activeTrade && trade.trade)
}, [trade])
// 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
const [approval, getApproval] = useSwapApproval(optimizedTrade, allowedSlippage, useIsPendingApproval)
const approvalHash = usePendingApproval(
inputCurrency?.isToken ? inputCurrency : undefined,
useSwapRouterAddress(optimizedTrade)
)
const addTransaction = useAddTransaction()
const addApprovalTransaction = useCallback(() => {
getApproval().then((transaction) => {
if (transaction) {
addTransaction({ type: TransactionType.APPROVAL, ...transaction })
}
})
}, [addTransaction, getApproval])
const actionProps = useMemo(() => {
if (swap && input.token && input.value && output.token && output.value && input.value <= balance) {
if (input.approved) {
return {}
} else {
if (disabled) return { disabled: true }
if (chainId && inputCurrencyAmount) {
if (!inputCurrencyBalance || inputCurrencyBalance.lessThan(inputCurrencyAmount)) {
return { disabled: true }
} else if (approval === ApprovalState.PENDING) {
return {
updated: { message: <Trans>Approve {input.token.symbol} first</Trans>, action: <Trans>Approve</Trans> },
disabled: true,
update: {
message: (
<EtherscanA href={approvalHash && `${CHAIN_INFO[chainId].explorer}tx/${approvalHash}`} target="_blank">
<Row gap={0.25}>
<Trans>
Approval pending <Link />
</Trans>
</Row>
</EtherscanA>
),
action: <Trans>Approve</Trans>,
icon: Spinner,
},
}
} else if (approval === ApprovalState.NOT_APPROVED) {
return {
update: {
message: <Trans>Approve {inputCurrencyAmount.currency.symbol} first</Trans>,
action: <Trans>Approve</Trans>,
},
}
}
return {}
}
return { disabled: true }
}, [balance, input.approved, input.token, input.value, output.token, output.value, swap])
}, [approval, approvalHash, chainId, disabled, inputCurrencyAmount, inputCurrencyBalance])
const deadline = useTransactionDeadline()
const { signatureData } = useERC20PermitFromTrade(optimizedTrade, allowedSlippage, deadline)
// the callback to execute the swap
const { callback: swapCallback } = useSwapCallback({
trade: optimizedTrade,
allowedSlippage,
recipientAddressOrName: account ?? null,
signatureData,
deadline,
feeOptions,
})
//@TODO(ianlapham): add a loading state, process errors
const setDisplayTxHash = useUpdateAtom(displayTxHashAtom)
const onConfirm = useCallback(() => {
// TODO: Send the tx to the connected wallet.
setMode(Mode.STATUS)
}, [])
swapCallback?.()
.then((response) => {
setDisplayTxHash(response.hash)
invariant(inputCurrencyAmount && outputCurrencyAmount)
addTransaction({
response,
type: TransactionType.SWAP,
tradeType: independentField === Field.INPUT ? TradeType.EXACT_INPUT : TradeType.EXACT_OUTPUT,
inputCurrencyAmount,
outputCurrencyAmount,
})
})
.catch((error) => {
//@TODO(ianlapham): add error handling
console.log(error)
})
.finally(() => {
setActiveTrade(undefined)
})
}, [addTransaction, independentField, inputCurrencyAmount, outputCurrencyAmount, setDisplayTxHash, swapCallback])
return (
<>
<ActionButton color="interactive" onClick={() => setMode(Mode.SUMMARY)} onUpdate={() => void 0} {...actionProps}>
<ActionButton
color={tokenColorExtraction ? 'interactive' : 'accent'}
onClick={() => setActiveTrade(trade.trade)}
onUpdate={addApprovalTransaction}
{...actionProps}
>
<Trans>Review swap</Trans>
</ActionButton>
{mode >= Mode.SUMMARY && (
<Dialog color="dialog" onClose={() => setMode(Mode.NONE)}>
<SummaryDialog onConfirm={onConfirm} />
</Dialog>
)}
{mode >= Mode.STATUS && (
<Dialog color="dialog">
<StatusDialog onClose={() => setMode(Mode.NONE)} />
{activeTrade && (
<Dialog color="dialog" onClose={() => setActiveTrade(undefined)}>
<SummaryDialog trade={activeTrade} allowedSlippage={allowedSlippage} onConfirm={onConfirm} />
</Dialog>
)}
</>

View File

@@ -0,0 +1,76 @@
import { BigNumber } from '@ethersproject/bignumber'
import { DefaultAddress, SwapProps } from 'lib/components/Swap'
import { IntegrationError } from 'lib/errors'
import { PropsWithChildren, useEffect } from 'react'
import { isAddress } from '../../../utils'
function isAddressOrAddressMap(addressOrMap: DefaultAddress): boolean {
if (typeof addressOrMap === 'object') {
return Object.values(addressOrMap).every((address) => isAddress(address))
}
if (typeof addressOrMap === 'string') {
return typeof isAddress(addressOrMap) === 'string'
}
return false
}
type ValidatorProps = PropsWithChildren<SwapProps>
export default function SwapPropValidator(props: ValidatorProps) {
const { convenienceFee, convenienceFeeRecipient } = props
useEffect(() => {
if (convenienceFee) {
if (convenienceFee > 100 || convenienceFee < 0) {
throw new IntegrationError(`convenienceFee must be between 0 and 100. (You set it to ${convenienceFee})`)
}
if (!convenienceFeeRecipient) {
throw new IntegrationError('convenienceFeeRecipient is required when convenienceFee is set.')
}
if (typeof convenienceFeeRecipient === 'string') {
if (!isAddress(convenienceFeeRecipient)) {
throw new IntegrationError(
`convenienceFeeRecipient must be a valid address. (You set it to ${convenienceFeeRecipient}.)`
)
}
} else if (typeof convenienceFeeRecipient === 'object') {
Object.values(convenienceFeeRecipient).forEach((recipient) => {
if (!isAddress(recipient)) {
const values = Object.values(convenienceFeeRecipient).join(', ')
throw new IntegrationError(
`All values in convenienceFeeRecipient object must be valid addresses. (You used ${values}.)`
)
}
})
}
}
}, [convenienceFee, convenienceFeeRecipient])
const { defaultInputAddress, defaultInputAmount, defaultOutputAddress, defaultOutputAmount } = props
useEffect(() => {
if (defaultOutputAmount && defaultInputAmount) {
throw new IntegrationError('defaultInputAmount and defaultOutputAmount may not both be defined.')
}
if (defaultInputAmount && BigNumber.from(defaultInputAmount).lt(0)) {
throw new IntegrationError(`defaultInputAmount must be a positive number. (You set it to ${defaultInputAmount})`)
}
if (defaultOutputAmount && BigNumber.from(defaultOutputAmount).lt(0)) {
throw new IntegrationError(
`defaultOutputAmount must be a positive number. (You set it to ${defaultOutputAmount})`
)
}
if (defaultInputAddress && !isAddressOrAddressMap(defaultInputAddress) && defaultInputAddress !== 'NATIVE') {
throw new IntegrationError(
`defaultInputAddress(es) must be a valid address or "NATIVE". (You set it to ${defaultInputAddress}`
)
}
if (defaultOutputAddress && !isAddressOrAddressMap(defaultOutputAddress) && defaultOutputAddress !== 'NATIVE') {
throw new IntegrationError(
`defaultOutputAddress(es) must be a valid address or "NATIVE". (You set it to ${defaultOutputAddress}`
)
}
}, [defaultInputAddress, defaultInputAmount, defaultOutputAddress, defaultOutputAmount])
return <>{props.children}</>
}

View File

@@ -1,7 +1,7 @@
import { Trans } from '@lingui/macro'
import { Input } from 'lib/state/swap'
import { Currency } from '@uniswap/sdk-core'
import { loadingOpacityCss } from 'lib/css/loading'
import styled, { keyframes, ThemedText } from 'lib/theme'
import { Token } from 'lib/types'
import { FocusEvent, ReactNode, useCallback, useRef, useState } from 'react'
import Button from '../Button'
@@ -14,7 +14,7 @@ const TokenInputRow = styled(Row)`
grid-template-columns: 1fr;
`
const ValueInput = styled(DecimalInput)`
const ValueInput = styled(DecimalInput)<{ $loading: boolean }>`
color: ${({ theme }) => theme.primary};
:hover:not(:focus-within) {
@@ -24,6 +24,8 @@ const ValueInput = styled(DecimalInput)`
:hover:not(:focus-within)::placeholder {
color: ${({ theme }) => theme.onHover(theme.secondary)};
}
${loadingOpacityCss}
`
const delayedFadeIn = keyframes`
@@ -45,20 +47,24 @@ const MaxButton = styled(Button)`
`
interface TokenInputProps {
input: Input
currency?: Currency
amount: string
disabled?: boolean
onMax?: () => void
onChangeInput: (input: number | undefined) => void
onChangeToken: (token: Token) => void
onChangeInput: (input: string) => void
onChangeCurrency: (currency: Currency) => void
loading?: boolean
children: ReactNode
}
export default function TokenInput({
input: { value, token },
currency,
amount,
disabled,
onMax,
onChangeInput,
onChangeToken,
onChangeCurrency,
loading,
children,
}: TokenInputProps) {
const max = useRef<HTMLButtonElement>(null)
@@ -69,15 +75,17 @@ export default function TokenInput({
setShowMax(false)
}
}, [])
return (
<Column gap={0.25}>
<TokenInputRow gap={0.5} onBlur={onBlur}>
<ThemedText.H2>
<ValueInput
value={value}
value={amount}
onFocus={onFocus}
onChange={onChangeInput}
disabled={disabled || !token}
disabled={disabled || !currency}
$loading={Boolean(loading)}
></ValueInput>
</ThemedText.H2>
{showMax && (
@@ -87,7 +95,7 @@ export default function TokenInput({
</ThemedText.ButtonMedium>
</MaxButton>
)}
<TokenSelect value={token} collapsed={showMax} disabled={disabled} onSelect={onChangeToken} />
<TokenSelect value={currency} collapsed={showMax} disabled={disabled} onSelect={onChangeCurrency} />
</TokenInputRow>
{children}
</Column>

View File

@@ -1,124 +0,0 @@
import { Trans } from '@lingui/macro'
import { useAtomValue } from 'jotai/utils'
import { AlertTriangle, Info, largeIconCss, Spinner } from 'lib/icons'
import { Field, Input, inputAtom, outputAtom, stateAtom, swapAtom } from 'lib/state/swap'
import styled, { ThemedText, ThemeProvider } from 'lib/theme'
import { useMemo, useState } from 'react'
import { TextButton } from '../Button'
import Row from '../Row'
import Rule from '../Rule'
import Tooltip from '../Tooltip'
const mockBalance = 123.45
function RoutingTooltip() {
return (
<Tooltip icon={Info} placement="bottom">
<ThemeProvider>
<ThemedText.Subhead2>TODO: Routing Tooltip</ThemedText.Subhead2>
</ThemeProvider>
</Tooltip>
)
}
type FilledInput = Input & Required<Pick<Input, 'token' | 'value'>>
function asFilledInput(input: Input): FilledInput | undefined {
return input.token && input.value ? (input as FilledInput) : undefined
}
interface LoadedStateProps {
input: FilledInput
output: FilledInput
}
function LoadedState({ input, output }: LoadedStateProps) {
const [flip, setFlip] = useState(true)
const ratio = useMemo(() => {
const [a, b] = flip ? [output, input] : [input, output]
const ratio = `1 ${a.token.symbol} = ${b.value / a.value} ${b.token.symbol}`
const usdc = a.usdc && ` ($${(a.usdc / a.value).toLocaleString('en')})`
return (
<Row gap={0.25} style={{ userSelect: 'text' }}>
{ratio}
{usdc && <ThemedText.Caption color="secondary">{usdc}</ThemedText.Caption>}
</Row>
)
}, [flip, input, output])
return (
<TextButton color="primary" onClick={() => setFlip(!flip)}>
{ratio}
</TextButton>
)
}
const ToolbarRow = styled(Row)`
padding: 0.5em 0;
${largeIconCss}
`
export default function Toolbar({ disabled }: { disabled?: boolean }) {
const { activeInput } = useAtomValue(stateAtom)
const swap = useAtomValue(swapAtom)
const input = useAtomValue(inputAtom)
const output = useAtomValue(outputAtom)
const balance = mockBalance
const caption = useMemo(() => {
const filledInput = asFilledInput(input)
const filledOutput = asFilledInput(output)
if (disabled) {
return (
<>
<AlertTriangle color="secondary" />
<Trans>Connect wallet to swap</Trans>
</>
)
}
if (activeInput === Field.INPUT ? filledInput && output.token : filledOutput && input.token) {
if (!swap) {
return (
<>
<Spinner color="secondary" />
<Trans>Fetching best price</Trans>
</>
)
}
if (filledInput && filledInput.value > balance) {
return (
<>
<AlertTriangle color="secondary" />
<Trans>Insufficient {filledInput.token.symbol}</Trans>
</>
)
}
if (filledInput && filledOutput) {
return (
<>
<RoutingTooltip />
<LoadedState input={filledInput} output={filledOutput} />
</>
)
}
}
return (
<>
<Info color="secondary" />
<Trans>Enter an amount</Trans>
</>
)
}, [activeInput, balance, disabled, input, output, swap])
return (
<>
<Rule />
<ThemedText.Caption>
<ToolbarRow justify="flex-start" gap={0.5} iconSize={4 / 3}>
{caption}
</ToolbarRow>
</ThemedText.Caption>
</>
)
}

View File

@@ -0,0 +1,81 @@
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 { InterfaceTrade } from 'state/routing/types'
import { TextButton } from '../../Button'
import Row from '../../Row'
import RoutingTooltip from './RoutingTooltip'
interface CaptionProps {
icon?: Icon
caption: ReactNode
}
function Caption({ icon: Icon = AlertTriangle, caption }: CaptionProps) {
return (
<>
<Icon color="secondary" />
{caption}
</>
)
}
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>} />
}
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)
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 (
<>
<RoutingTooltip />
<TextButton color="primary" onClick={() => setFlip(!flip)}>
{ratio}
</TextButton>
</>
)
}

View File

@@ -0,0 +1,14 @@
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

@@ -0,0 +1,72 @@
import { ALL_SUPPORTED_CHAIN_IDS } from 'constants/chains'
import { useIsAmountPopulated, useSwapInfo } from 'lib/hooks/swap'
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 { TradeState } from 'state/routing/types'
import Row from '../../Row'
import Rule from '../../Rule'
import * as Caption from './Caption'
const ToolbarRow = styled(Row)`
padding: 0.5em 0;
${largeIconCss}
`
export default function Toolbar({ disabled }: { disabled?: boolean }) {
const { chainId } = useActiveWeb3React()
const {
trade: { trade, state },
currencies: { [Field.INPUT]: inputCurrency, [Field.OUTPUT]: outputCurrency },
currencyBalances: { [Field.INPUT]: balance },
} = useSwapInfo()
const [routeFound, routeIsLoading] = useMemo(
() => [Boolean(trade?.swaps), TradeState.LOADING === state || TradeState.SYNCING === state],
[state, trade?.swaps]
)
const isAmountPopulated = useIsAmountPopulated()
const caption = useMemo(() => {
if (disabled) {
return <Caption.ConnectWallet />
}
if (chainId && !ALL_SUPPORTED_CHAIN_IDS.includes(chainId)) {
return <Caption.UnsupportedNetwork />
}
if (balance && trade?.inputAmount.greaterThan(balance)) {
return <Caption.InsufficientBalance currency={trade.inputAmount.currency} />
}
if (inputCurrency && outputCurrency && isAmountPopulated) {
if (!trade || routeIsLoading) {
return <Caption.LoadingTrade />
}
if (!routeFound) {
return <Caption.InsufficientLiquidity />
}
if (trade.inputAmount && trade.outputAmount) {
return <Caption.Trade trade={trade} />
}
}
return <Caption.Empty />
}, [balance, chainId, disabled, inputCurrency, isAmountPopulated, outputCurrency, routeFound, routeIsLoading, trade])
return (
<>
<Rule />
<ThemedText.Caption>
<ToolbarRow justify="flex-start" gap={0.5} iconSize={4 / 3}>
{caption}
</ToolbarRow>
</ThemedText.Caption>
</>
)
}

View File

@@ -1,9 +1,18 @@
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 { usePendingTransactions } from 'lib/hooks/transactions'
import useActiveWeb3React from 'lib/hooks/useActiveWeb3React'
import useTokenList, { DEFAULT_TOKEN_LIST } from 'lib/hooks/useTokenList'
import useTokenList from 'lib/hooks/useTokenList'
import { displayTxHashAtom } from 'lib/state/swap'
import { SwapTransactionInfo, Transaction, TransactionType } from 'lib/state/transactions'
import { useMemo, useState } from 'react'
import Dialog from '../Dialog'
import Header from '../Header'
import { BoundaryProvider } from '../Popover'
import Wallet from '../Wallet'
@@ -11,47 +20,56 @@ import Input from './Input'
import Output from './Output'
import ReverseButton from './ReverseButton'
import Settings from './Settings'
import { StatusDialog } from './Status'
import SwapButton from './SwapButton'
import SwapPropValidator from './SwapPropValidator'
import Toolbar from './Toolbar'
interface DefaultTokenAmount {
address?: string | { [chainId: number]: string }
amount?: number
}
export type DefaultAddress = string | { [chainId: number]: string | 'NATIVE' } | 'NATIVE'
interface SwapDefaults {
tokenList: string | TokenInfo[]
input: DefaultTokenAmount
output: DefaultTokenAmount
}
const DEFAULT_INPUT: DefaultTokenAmount = { address: 'ETH' }
const DEFAULT_OUTPUT: DefaultTokenAmount = {}
function useSwapDefaults(defaults: Partial<SwapDefaults> = {}): SwapDefaults {
const tokenList = defaults.tokenList || DEFAULT_TOKEN_LIST
const input: DefaultTokenAmount = defaults.input || DEFAULT_INPUT
const output: DefaultTokenAmount = defaults.output || DEFAULT_OUTPUT
input.amount = input.amount || 0
output.amount = output.amount || 0
// eslint-disable-next-line react-hooks/exhaustive-deps
return useMemo(() => ({ tokenList, input, output }), [])
function getSwapTx(txs: { [hash: string]: Transaction }, hash?: string): Transaction<SwapTransactionInfo> | undefined {
if (hash) {
const tx = txs[hash]
if (tx?.info?.type === TransactionType.SWAP) {
return tx as Transaction<SwapTransactionInfo>
}
}
return
}
export interface SwapProps {
defaults?: Partial<SwapDefaults>
tokenList?: string | TokenInfo[]
defaultInputAddress?: DefaultAddress
defaultInputAmount?: string
defaultOutputAddress?: DefaultAddress
defaultOutputAmount?: string
convenienceFee?: number
convenienceFeeRecipient?: string | { [chainId: number]: string }
onConnectWallet?: () => void
}
export default function Swap({ defaults }: SwapProps) {
const { tokenList } = useSwapDefaults(defaults)
useTokenList(tokenList)
export default function Swap(props: SwapProps) {
const list = useTokenList(props.tokenList)
useSyncSwapDefaults(props)
useSyncConvenienceFee(props)
const { active, account, chainId } = useActiveWeb3React()
const [boundary, setBoundary] = useState<HTMLDivElement | null>(null)
const { active, account } = useActiveWeb3React()
const [displayTxHash, setDisplayTxHash] = useAtom(displayTxHashAtom)
const pendingTxs = usePendingTransactions()
const displayTx = getSwapTx(pendingTxs, displayTxHash)
const onSupportedChain = useMemo(
() => chainId && ALL_SUPPORTED_CHAIN_IDS.includes(chainId) && list.some((token) => token.chainId === chainId),
[chainId, list]
)
return (
<>
<Header logo title={<Trans>Swap</Trans>}>
{active && <Wallet disabled={!account} />}
<SwapPropValidator {...props}>
{onSupportedChain && <SwapInfoUpdater />}
<Header title={<Trans>Swap</Trans>}>
{active && <Wallet disabled={!account} onClick={props.onConnectWallet} />}
<Settings disabled={!active} />
</Header>
<div ref={setBoundary}>
@@ -60,10 +78,15 @@ export default function Swap({ defaults }: SwapProps) {
<ReverseButton disabled={!active} />
<Output disabled={!active}>
<Toolbar disabled={!active} />
<SwapButton />
<SwapButton disabled={!account} />
</Output>
</BoundaryProvider>
</div>
</>
{displayTx && (
<Dialog color="dialog">
<StatusDialog tx={displayTx} onClose={() => setDisplayTxHash()} />
</Dialog>
)}
</SwapPropValidator>
)
}

View File

@@ -1,29 +1,31 @@
import useNativeEvent from 'lib/hooks/useNativeEvent'
import { Currency } from '@uniswap/sdk-core'
import useCurrencyLogoURIs from 'lib/hooks/useCurrencyLogoURIs'
import { Slash } from 'lib/icons'
import styled from 'lib/theme'
import uriToHttp from 'lib/utils/uriToHttp'
import { useState } from 'react'
import { useCallback, useEffect, useState } from 'react'
const badSrcs = new Set<string>()
interface TokenImgProps {
className?: string
token: {
name?: string
symbol: string
logoURI?: string
}
token: Currency
}
const TRANSPARENT_SRC = 'data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=='
function TokenImg({ className, token }: TokenImgProps) {
const [img, setImg] = useState<HTMLImageElement | null>(null)
const src = token.logoURI ? uriToHttp(token.logoURI)[0] : TRANSPARENT_SRC
useNativeEvent(img, 'error', () => {
if (img) {
// Use a local transparent gif to avoid the browser-dependent broken img icon.
// The icon may still flash, but using a native event further reduces the duration.
img.src = TRANSPARENT_SRC
}
})
return <img className={className} src={src} alt={token.name || token.symbol} ref={setImg} />
const srcs = useCurrencyLogoURIs(token)
const [src, setSrc] = useState<string | undefined>()
useEffect(() => {
setSrc(srcs.find((src) => !badSrcs.has(src)))
}, [srcs])
const onError = useCallback(() => {
if (src) badSrcs.add(src)
setSrc(srcs.find((src) => !badSrcs.has(src)))
}, [src, srcs])
if (src) {
return <img className={className} src={src} alt={token.name || token.symbol} onError={onError} />
}
return <Slash className={className} color="secondary" />
}
export default styled(TokenImg)<{ size?: number }>`

View File

@@ -1,5 +1,5 @@
import { Currency } from '@uniswap/sdk-core'
import styled, { ThemedText } from 'lib/theme'
import { Token } from 'lib/types'
import Button from '../Button'
import Row from '../Row'
@@ -11,8 +11,8 @@ const TokenButton = styled(Button)`
`
interface TokenBaseProps {
value: Token
onClick: (value: Token) => void
value: Currency
onClick: (value: Currency) => void
}
export default function TokenBase({ value, onClick }: TokenBaseProps) {

View File

@@ -1,7 +1,8 @@
import { Trans } from '@lingui/macro'
import { Currency } from '@uniswap/sdk-core'
import { ChevronDown } from 'lib/icons'
import styled, { ThemedText } from 'lib/theme'
import { Token } from 'lib/types'
import { useMemo } from 'react'
import Button from '../Button'
import Row from '../Row'
@@ -27,16 +28,18 @@ const TokenButtonRow = styled(Row)<{ collapsed: boolean }>`
`
interface TokenButtonProps {
value?: Token
value?: Currency
collapsed: boolean
disabled?: boolean
onClick: () => void
}
export default function TokenButton({ value, collapsed, disabled, onClick }: TokenButtonProps) {
const buttonBackgroundColor = useMemo(() => (value ? 'interactive' : 'accent'), [value])
const contentColor = useMemo(() => (value || disabled ? 'onInteractive' : 'onAccent'), [value, disabled])
return (
<StyledTokenButton onClick={onClick} empty={!value} color={value ? 'interactive' : 'accent'} disabled={disabled}>
<ThemedText.ButtonLarge color="onInteractive">
<StyledTokenButton onClick={onClick} empty={!value} color={buttonBackgroundColor} disabled={disabled}>
<ThemedText.ButtonLarge color={contentColor}>
<TokenButtonRow gap={0.4} collapsed={Boolean(value) && collapsed}>
{value ? (
<>
@@ -46,7 +49,7 @@ export default function TokenButton({ value, collapsed, disabled, onClick }: Tok
) : (
<Trans>Select a token</Trans>
)}
<ChevronDown color="onInteractive" strokeWidth={3} />
<ChevronDown color={contentColor} strokeWidth={3} />
</TokenButtonRow>
</ThemedText.ButtonLarge>
</StyledTokenButton>

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