Compare commits

..

85 Commits

Author SHA1 Message Date
Zach Pomerantz
b152b11515 fix: token select input handling (#3303) 2022-02-15 19:35:14 -08:00
Zach Pomerantz
0f51991109 feat: improved warning ux (#3310) 2022-02-15 19:34:02 -08:00
Zach Pomerantz
da8884d87d fix: action prop warning (#3304) 2022-02-15 19:33:29 -08:00
Zach Pomerantz
79bdc0c5ee fix: summary details heights (#3302) 2022-02-15 19:33:09 -08:00
Zach Pomerantz
82c30681ea fix: ignore stale SOR fetches (#3313)
* fix: propagate ROUTE_NOT_FOUND and fallback appropriately

* fix: display insufficient liquidities

* fix: ignore stale SOR results

* fix: retain trade state while loading

* fix: mv debouncing to SOR logic for sync state
2022-02-14 18:23:55 -08:00
Ian Lapham
41ef961679 feat: optimize client side SOR for widgets (#3294)
* start SOR updates

* update pool providers to static

* update router config

* remove log

* udpate defaults for chainId

* small changes
2022-02-14 13:35:05 -07:00
Zach Pomerantz
7de63ab462 feat: focus and hover hooks (#3287)
* feat: add focus/hover hooks

* refactor: use focus/hover hooks
2022-02-14 06:32:11 -08:00
Crowdin Bot
59c5989721 chore(i18n): synchronize translations from crowdin [skip ci] 2022-02-12 15:07:37 +00:00
Zach Pomerantz
b042d2b3b4 fix: i18n resolution (#3299) 2022-02-11 10:34:03 -08:00
Zach Pomerantz
897e7f4581 fix: approval action button (#3297) 2022-02-11 08:47:45 -08:00
Zach Pomerantz
a7fb7dc906 chore: v0.0.11-beta 2022-02-11 08:13:06 -08:00
Crowdin Bot
5fe89b9d6c chore(i18n): synchronize translations from crowdin [skip ci] 2022-02-11 04:07:20 +00:00
Zach Pomerantz
acbcd3763c style: update type sizes (#3292) 2022-02-10 20:37:58 -07:00
Zach Pomerantz
01c467b48c fix: max button flashing on tabbing (#3291) 2022-02-10 20:37:13 -07:00
Zach Pomerantz
636abe3b7b fix: respond to updated amounts immediately (#3289) 2022-02-10 20:35:34 -07:00
Zach Pomerantz
8404c6076c feat: confirm price impact (#3288)
* refactor: action button naming

* feat: high price impact acknowledgement
2022-02-10 20:33:51 -07:00
Zach Pomerantz
b4aac94c2c fix: settings ux (#3282)
* fix: max slippage warning logic

* fix: option border specificity

* fix: dialog resizing through animation

* fix: initial warning states

* fix: hide Modal class
2022-02-10 14:22:54 -08:00
Zach Pomerantz
f47fcc9c17 feat: focus input on token select (#3286) 2022-02-10 14:12:24 -08:00
Crowdin Bot
b5d27e2063 chore(i18n): synchronize translations from crowdin [skip ci] 2022-02-10 22:06:48 +00:00
Jordan Frankfurt
26275ca580 fix(widgets): remove trading header (#3263)
* fix(widgets): remove trading header

* correct height for removed title, more scalable/consistent transform values

* 347->346 height
2022-02-10 15:59:26 -06:00
Zach Pomerantz
92b7ca8f55 feat: hide balance on blur (#3285)
* feat: hide balance when blurred

* refactor: express focused through props

* refactor: share Input/Output code
2022-02-10 13:39:08 -08:00
Jordan Frankfurt
c5ea01ce19 fix(widgets): complete etherscan link and stop timer on tx inclusion (#3267)
* fix(widgets): complete etherscan link and stop timer on tx inclusion

* use preexisting helper for etherscan link

* use z's EtherscanLink component

* pr review
2022-02-10 15:35:19 -06:00
Ian Lapham
88712b5065 fix: adjust max button for native currencies (#3279)
* adjust max button for native currencies

* update var naming
2022-02-10 12:54:01 -08:00
Zach Pomerantz
1af34ae016 feat: EtherscanA component (#3284)
* feat: EtherscanA component

* refactor: EtherscanLink from ExternalLink
2022-02-10 09:14:54 -08:00
Zach Pomerantz
9cb19dd0ea fix: filter integrator fee (#3281) 2022-02-10 08:51:39 -08:00
Zach Pomerantz
02a77254c7 fix: retain sig figs in localized nums (#3280) 2022-02-10 08:51:29 -08:00
Zach Pomerantz
69ed7015ab chore: clean up old generated css (#3277) 2022-02-10 08:51:18 -08:00
Zach Pomerantz
ff16d3f18f fix: theme colors (#3274) 2022-02-10 08:51:11 -08:00
Zach Pomerantz
5175cb6d1f fix: leave intermediate artifacts in watch (#3272) 2022-02-10 08:50:56 -08:00
Zach Pomerantz
b33686855d fix: link footer to uniswap.org (#3283) 2022-02-10 08:49:24 -08:00
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
158 changed files with 3636 additions and 3267 deletions

3
.env
View File

@@ -1 +1,2 @@
REACT_APP_INFURA_KEY="4bf032f2d38a4ed6bb975b80d6340847"
REACT_APP_INFURA_KEY="4bf032f2d38a4ed6bb975b80d6340847"
REACT_APP_LOCALES="locales"

5
.gitignore vendored
View File

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

View File

@@ -5,5 +5,6 @@
"webpack": {
"configPath": "react-scripts/config/webpack.config",
"overridePath": "cosmos.override.js"
}
},
"port": 5001
}

View File

@@ -17,6 +17,7 @@ module.exports = (webpackConfig) => ({
'process.env': {
...plugin.definitions['process.env'],
REACT_APP_IS_WIDGET: true,
REACT_APP_LOCALES: '"../locales"',
},
})
}

View File

@@ -1,16 +1,20 @@
{
"name": "@uniswap/interface",
"name": "@uniswap/widgets",
"version": "0.0.11-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",
@@ -60,18 +68,20 @@
"@uniswap/governance": "^1.0.2",
"@uniswap/liquidity-staker": "^1.0.2",
"@uniswap/merkle-distributor": "1.0.1",
"@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",
@@ -92,8 +102,10 @@
"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.6",
"react-dom": "^17.0.1",
"react-ga": "^2.5.7",
"react-is": "^17.0.2",
"react-markdown": "^4.3.1",
@@ -103,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",
@@ -132,7 +147,8 @@
"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",
@@ -154,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",
@@ -162,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",
@@ -173,13 +190,15 @@
"@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",
@@ -192,8 +211,6 @@
"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",
@@ -204,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' } }),
process.env.ROLLUP_WATCH ? undefined : 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

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

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

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

@@ -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,5 +1,6 @@
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'
@@ -8,7 +9,7 @@ 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 { 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

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

@@ -1,46 +1,12 @@
import ethereumLogoUrl from 'assets/images/ethereum-logo.png'
import arbitrumLogoUrl from 'assets/svg/arbitrum_logo.svg'
import optimismLogoUrl from 'assets/svg/optimistic_ethereum.svg'
import polygonMaticLogo from 'assets/svg/polygon-matic-logo.svg'
import ms from 'ms.macro'
import ethereumLogoUrl from '../assets/images/ethereum-logo.png'
import arbitrumLogoUrl from '../assets/svg/arbitrum_logo.svg'
import optimismLogoUrl from '../assets/svg/optimistic_ethereum.svg'
import polygonMaticLogo from '../assets/svg/polygon-matic-logo.svg'
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 },
},
}

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,7 +1,7 @@
/* 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'

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

@@ -1,15 +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 TickLensABI } from '@uniswap/v3-periphery/artifacts/contracts/lens/TickLens.sol/TickLens.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'
@@ -18,16 +14,11 @@ 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,
@@ -35,7 +26,7 @@ import {
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, TickLens, UniswapInterfaceMulticall } from 'types/v3'
@@ -43,6 +34,14 @@ 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,
@@ -123,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,

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

@@ -2,7 +2,7 @@ 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 { SwapRouter as V3SwapRouter, Trade as V3Trade } from '@uniswap/v3-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'
@@ -36,7 +36,8 @@ export function useSwapCallArguments(
allowedSlippage: Percent,
recipientAddressOrName: string | null | undefined,
signatureData: SignatureData | null | undefined,
deadline: BigNumber | undefined
deadline: BigNumber | undefined,
feeOptions: FeeOptions | undefined
): SwapCall[] {
const { account, chainId, library } = useActiveWeb3React()
@@ -98,6 +99,7 @@ export function useSwapCallArguments(
} else {
// swap options shared by v3 and v2+v3 swap routers
const sharedSwapOptions = {
fee: feeOptions,
recipient,
slippageTolerance: allowedSlippage,
...(signatureData
@@ -167,15 +169,16 @@ export function useSwapCallArguments(
]
}
}, [
trade,
recipient,
library,
account,
chainId,
deadline,
routerContract,
allowedSlippage,
argentWalletContract,
chainId,
deadline,
feeOptions,
library,
recipient,
routerContract,
signatureData,
trade,
])
}

View File

@@ -33,7 +33,7 @@ export function useSwapCallback(
state,
callback: libCallback,
error,
} = useLibSwapCallBack(trade, allowedSlippage, recipient, signatureData, deadline)
} = useLibSwapCallBack({ trade, allowedSlippage, recipientAddressOrName: recipient, signatureData, deadline })
const callback = useMemo(() => {
if (!libCallback || !trade) {

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,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,225 +0,0 @@
@import "~@fontsource/ibm-plex-mono/400.css";
@font-face {
font-family: "Inter";
font-style: normal;
font-display: block;
font-weight: 400;
src: url("~@fontsource/inter/files/inter-cyrillic-ext-400-normal.woff2") format("woff2"), url("~@fontsource/inter/files/inter-all-400-normal.woff") format("woff");
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}
@font-face {
font-family: "Inter";
font-style: normal;
font-display: block;
font-weight: 400;
src: url("~@fontsource/inter/files/inter-cyrillic-400-normal.woff2") format("woff2"), url("~@fontsource/inter/files/inter-all-400-normal.woff") format("woff");
unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
@font-face {
font-family: "Inter";
font-style: normal;
font-display: block;
font-weight: 400;
src: url("~@fontsource/inter/files/inter-greek-ext-400-normal.woff2") format("woff2"), url("~@fontsource/inter/files/inter-all-400-normal.woff") format("woff");
unicode-range: U+1F00-1FFF;
}
@font-face {
font-family: "Inter";
font-style: normal;
font-display: block;
font-weight: 400;
src: url("~@fontsource/inter/files/inter-greek-400-normal.woff2") format("woff2"), url("~@fontsource/inter/files/inter-all-400-normal.woff") format("woff");
unicode-range: U+0370-03FF;
}
@font-face {
font-family: "Inter";
font-style: normal;
font-display: block;
font-weight: 400;
src: url("~@fontsource/inter/files/inter-vietnamese-400-normal.woff2") format("woff2"), url("~@fontsource/inter/files/inter-all-400-normal.woff") format("woff");
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB;
}
@font-face {
font-family: "Inter";
font-style: normal;
font-display: block;
font-weight: 400;
src: url("~@fontsource/inter/files/inter-latin-ext-400-normal.woff2") format("woff2"), url("~@fontsource/inter/files/inter-all-400-normal.woff") format("woff");
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
font-family: "Inter";
font-style: normal;
font-display: block;
font-weight: 400;
src: url("~@fontsource/inter/files/inter-latin-400-normal.woff2") format("woff2"), url("~@fontsource/inter/files/inter-all-400-normal.woff") format("woff");
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
font-family: "Inter";
font-style: normal;
font-display: block;
font-weight: 500;
src: url("~@fontsource/inter/files/inter-cyrillic-ext-500-normal.woff2") format("woff2"), url("~@fontsource/inter/files/inter-all-500-normal.woff") format("woff");
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}
@font-face {
font-family: "Inter";
font-style: normal;
font-display: block;
font-weight: 500;
src: url("~@fontsource/inter/files/inter-cyrillic-500-normal.woff2") format("woff2"), url("~@fontsource/inter/files/inter-all-500-normal.woff") format("woff");
unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
@font-face {
font-family: "Inter";
font-style: normal;
font-display: block;
font-weight: 500;
src: url("~@fontsource/inter/files/inter-greek-ext-500-normal.woff2") format("woff2"), url("~@fontsource/inter/files/inter-all-500-normal.woff") format("woff");
unicode-range: U+1F00-1FFF;
}
@font-face {
font-family: "Inter";
font-style: normal;
font-display: block;
font-weight: 500;
src: url("~@fontsource/inter/files/inter-greek-500-normal.woff2") format("woff2"), url("~@fontsource/inter/files/inter-all-500-normal.woff") format("woff");
unicode-range: U+0370-03FF;
}
@font-face {
font-family: "Inter";
font-style: normal;
font-display: block;
font-weight: 500;
src: url("~@fontsource/inter/files/inter-vietnamese-500-normal.woff2") format("woff2"), url("~@fontsource/inter/files/inter-all-500-normal.woff") format("woff");
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB;
}
@font-face {
font-family: "Inter";
font-style: normal;
font-display: block;
font-weight: 500;
src: url("~@fontsource/inter/files/inter-latin-ext-500-normal.woff2") format("woff2"), url("~@fontsource/inter/files/inter-all-500-normal.woff") format("woff");
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
font-family: "Inter";
font-style: normal;
font-display: block;
font-weight: 500;
src: url("~@fontsource/inter/files/inter-latin-500-normal.woff2") format("woff2"), url("~@fontsource/inter/files/inter-all-500-normal.woff") format("woff");
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
font-family: "Inter";
font-style: normal;
font-display: block;
font-weight: 600;
src: url("~@fontsource/inter/files/inter-cyrillic-ext-600-normal.woff2") format("woff2"), url("~@fontsource/inter/files/inter-all-600-normal.woff") format("woff");
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}
@font-face {
font-family: "Inter";
font-style: normal;
font-display: block;
font-weight: 600;
src: url("~@fontsource/inter/files/inter-cyrillic-600-normal.woff2") format("woff2"), url("~@fontsource/inter/files/inter-all-600-normal.woff") format("woff");
unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
@font-face {
font-family: "Inter";
font-style: normal;
font-display: block;
font-weight: 600;
src: url("~@fontsource/inter/files/inter-greek-ext-600-normal.woff2") format("woff2"), url("~@fontsource/inter/files/inter-all-600-normal.woff") format("woff");
unicode-range: U+1F00-1FFF;
}
@font-face {
font-family: "Inter";
font-style: normal;
font-display: block;
font-weight: 600;
src: url("~@fontsource/inter/files/inter-greek-600-normal.woff2") format("woff2"), url("~@fontsource/inter/files/inter-all-600-normal.woff") format("woff");
unicode-range: U+0370-03FF;
}
@font-face {
font-family: "Inter";
font-style: normal;
font-display: block;
font-weight: 600;
src: url("~@fontsource/inter/files/inter-vietnamese-600-normal.woff2") format("woff2"), url("~@fontsource/inter/files/inter-all-600-normal.woff") format("woff");
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB;
}
@font-face {
font-family: "Inter";
font-style: normal;
font-display: block;
font-weight: 600;
src: url("~@fontsource/inter/files/inter-latin-ext-600-normal.woff2") format("woff2"), url("~@fontsource/inter/files/inter-all-600-normal.woff") format("woff");
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
font-family: "Inter";
font-style: normal;
font-display: block;
font-weight: 600;
src: url("~@fontsource/inter/files/inter-latin-600-normal.woff2") format("woff2"), url("~@fontsource/inter/files/inter-all-600-normal.woff") format("woff");
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
font-family: "InterVariable";
font-style: normal;
font-display: block;
font-weight: 100 900;
src: url("~@fontsource/inter/files/inter-cyrillic-ext-variable-wghtOnly-normal.woff2") format("woff2");
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}
@font-face {
font-family: "InterVariable";
font-style: normal;
font-display: block;
font-weight: 100 900;
src: url("~@fontsource/inter/files/inter-cyrillic-variable-wghtOnly-normal.woff2") format("woff2");
unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
@font-face {
font-family: "InterVariable";
font-style: normal;
font-display: block;
font-weight: 100 900;
src: url("~@fontsource/inter/files/inter-greek-ext-variable-wghtOnly-normal.woff2") format("woff2");
unicode-range: U+1F00-1FFF;
}
@font-face {
font-family: "InterVariable";
font-style: normal;
font-display: block;
font-weight: 100 900;
src: url("~@fontsource/inter/files/inter-greek-variable-wghtOnly-normal.woff2") format("woff2");
unicode-range: U+0370-03FF;
}
@font-face {
font-family: "InterVariable";
font-style: normal;
font-display: block;
font-weight: 100 900;
src: url("~@fontsource/inter/files/inter-vietnamese-variable-wghtOnly-normal.woff2") format("woff2");
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB;
}
@font-face {
font-family: "InterVariable";
font-style: normal;
font-display: block;
font-weight: 100 900;
src: url("~@fontsource/inter/files/inter-latin-ext-variable-wghtOnly-normal.woff2") format("woff2");
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
font-family: "InterVariable";
font-style: normal;
font-display: block;
font-weight: 100 900;
src: url("~@fontsource/inter/files/inter-latin-variable-wghtOnly-normal.woff2") format("woff2");
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

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, 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'
@@ -15,7 +15,7 @@ const StyledButton = styled(Button)`
}
`
const UpdateRow = styled(Row)``
const ActionRow = styled(Row)``
const grow = keyframes`
from {
@@ -28,12 +28,12 @@ const grow = keyframes`
}
`
const updateCss = css`
const actionCss = css`
border: 1px solid ${({ theme }) => theme.outline};
padding: calc(0.25em - 1px);
padding-left: calc(0.75em - 1px);
${UpdateRow} {
${ActionRow} {
animation: ${grow} 0.25s ease-in;
white-space: nowrap;
}
@@ -45,44 +45,44 @@ const updateCss = css`
}
`
export const Overlay = styled(Row)<{ update?: boolean }>`
export const Overlay = styled(Row)<{ hasAction: boolean }>`
border-radius: ${({ theme }) => theme.borderRadius}em;
flex-direction: row-reverse;
min-height: 3.5em;
transition: padding 0.25s ease-out;
${({ update }) => update && updateCss}
${({ hasAction }) => hasAction && actionCss}
`
export interface Action {
message: ReactNode
icon?: Icon
onClick: () => void
children: ReactNode
}
export interface ActionButtonProps {
color?: Color
disabled?: boolean
update?: { message: ReactNode; action: ReactNode; icon?: Icon }
action?: Action
onClick: () => void
onUpdate?: () => void
children: ReactNode
}
export default function ActionButton({
color = 'accent',
disabled,
update,
onClick,
onUpdate,
children,
}: ActionButtonProps) {
export default function ActionButton({ color = 'accent', disabled, action, onClick, children }: ActionButtonProps) {
const textColor = useMemo(() => (color === 'accent' && !disabled ? 'onAccent' : 'currentColor'), [color, disabled])
return (
<Overlay update={Boolean(update)} flex align="stretch">
<StyledButton color={color} disabled={disabled} onClick={update ? onUpdate : onClick}>
<ThemedText.TransitionButton buttonSize={update ? 'medium' : 'large'} color="currentColor">
{update ? update.action : children}
<Overlay hasAction={Boolean(action)} flex align="stretch">
<StyledButton color={color} disabled={disabled} onClick={action ? action.onClick : onClick}>
<ThemedText.TransitionButton buttonSize={action ? 'medium' : 'large'} color={textColor}>
{action ? action.children : children}
</ThemedText.TransitionButton>
</StyledButton>
{update && (
<UpdateRow gap={0.5}>
<LargeIcon color="currentColor" icon={update.icon || AlertTriangle} />
<ThemedText.Subhead2>{update?.message}</ThemedText.Subhead2>
</UpdateRow>
{action && (
<ActionRow gap={0.5}>
<LargeIcon color="currentColor" icon={action.icon || AlertTriangle} />
<ThemedText.Subhead2>{action?.message}</ThemedText.Subhead2>
</ActionRow>
)}
</Overlay>
)

View File

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

View File

@@ -1,6 +1,6 @@
import { Icon } from 'lib/icons'
import styled, { Color } from 'lib/theme'
import { ComponentProps } from 'react'
import { ComponentProps, forwardRef } from 'react'
export const BaseButton = styled.button`
background-color: transparent;
@@ -55,10 +55,12 @@ interface IconButtonProps {
iconProps?: ComponentProps<Icon>
}
export function IconButton({ icon: Icon, iconProps, ...props }: IconButtonProps & ComponentProps<typeof BaseButton>) {
return (
<SecondaryButton {...props}>
<Icon {...iconProps} />
</SecondaryButton>
)
}
export const IconButton = forwardRef<HTMLButtonElement, IconButtonProps & ComponentProps<typeof BaseButton>>(
function IconButton({ icon: Icon, iconProps, ...props }: IconButtonProps & ComponentProps<typeof BaseButton>, ref) {
return (
<SecondaryButton {...props} ref={ref}>
<Icon {...iconProps} />
</SecondaryButton>
)
}
)

View File

@@ -73,13 +73,12 @@ export const Modal = styled.div<{ color: Color }>`
border-radius: ${({ theme }) => theme.borderRadius * 0.75}em;
display: flex;
flex-direction: column;
height: calc(100% - 0.5em);
height: 100%;
left: 0;
margin: 0.25em;
overflow: hidden;
position: absolute;
top: 0;
width: calc(100% - 0.5em);
width: 100%;
z-index: ${Layer.DIALOG};
`
@@ -106,7 +105,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

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

View File

@@ -87,10 +87,10 @@ interface ErrorDialogProps {
header?: ReactNode
error: Error
action: ReactNode
onAction: () => void
onClick: () => void
}
export default function ErrorDialog({ header, error, action, onAction }: ErrorDialogProps) {
export default function ErrorDialog({ header, error, action, onClick }: ErrorDialogProps) {
const [open, setOpen] = useState(false)
const [details, setDetails] = useState<HTMLDivElement | null>(null)
const scrollbar = useScrollbar(details)
@@ -123,7 +123,7 @@ export default function ErrorDialog({ header, error, action, onAction }: ErrorDi
</ThemedText.Code>
</Column>
</ErrorColumn>
<ActionButton onClick={onAction}>{action}</ActionButton>
<ActionButton onClick={onClick}>{action}</ActionButton>
</ExpandoColumn>
</Column>
)

View File

@@ -1,3 +1,4 @@
import { SUPPORTED_LOCALES } from 'constants/locales'
import { WidgetProps } from 'lib/components/Widget'
import { IntegrationError } from 'lib/errors'
import { PropsWithChildren, useEffect } from 'react'
@@ -18,5 +19,12 @@ export default function WidgetsPropsValidator(props: PropsWithChildren<WidgetPro
}
}, [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,32 @@
import { SupportedChainId } from 'constants/chains'
import useActiveWeb3React from 'lib/hooks/useActiveWeb3React'
import styled, { Color } from 'lib/theme'
import { ReactNode, useMemo } from 'react'
import { ExplorerDataType, getExplorerLink } from 'utils/getExplorerLink'
import ExternalLink from './ExternalLink'
const Link = styled(ExternalLink)<{ color: Color }>`
color: ${({ theme, color }) => theme[color]};
text-decoration: none;
`
interface EtherscanLinkProps {
type: ExplorerDataType
data?: string
color?: Color
children: ReactNode
}
export default function EtherscanLink({ data, type, color = 'currentColor', children }: EtherscanLinkProps) {
const { chainId } = useActiveWeb3React()
const url = useMemo(
() => data && getExplorerLink(chainId || SupportedChainId.MAINNET, data, type),
[chainId, data, type]
)
return (
<Link href={url} color={color} target="_blank">
{children}
</Link>
)
}

View File

@@ -8,7 +8,7 @@ export default function ExternalLink({
href,
rel = 'noopener noreferrer',
...rest
}: Omit<HTMLProps<HTMLAnchorElement>, 'as' | 'ref' | 'onClick'> & { href: string }) {
}: Omit<HTMLProps<HTMLAnchorElement>, 'as' | 'ref' | 'onClick'> & { href?: string }) {
return (
<a target={target} rel={rel} href={href} {...rest}>
{rest.children}

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,17 +1,30 @@
import { Trans } from '@lingui/macro'
import { useLingui } from '@lingui/react'
import { useUSDCValue } from 'hooks/useUSDCPrice'
import { useAtomValue } from 'jotai/utils'
import { loadingOpacityCss } from 'lib/css/loading'
import { useSwapAmount, useSwapCurrency, useSwapInfo } from 'lib/hooks/swap'
import { usePrefetchCurrencyColor } from 'lib/hooks/useCurrencyColor'
import { Field } from 'lib/state/swap'
import { Field, independentFieldAtom } from 'lib/state/swap'
import styled, { ThemedText } from 'lib/theme'
import { useCallback } from 'react'
import { useMemo } from 'react'
import { TradeState } from 'state/routing/types'
import { formatCurrencyAmount } from 'utils/formatCurrencyAmount'
import { maxAmountSpend } from 'utils/maxAmountSpend'
import Column from '../Column'
import Row from '../Row'
import TokenImg from '../TokenImg'
import TokenInput from './TokenInput'
export const LoadingRow = styled(Row)<{ $loading: boolean }>`
${loadingOpacityCss};
`
export const Balance = styled(ThemedText.Body2)<{ focused: boolean }>`
opacity: ${({ focused }) => (focused ? 1 : 0)};
transition: opacity 0.25s ${({ focused }) => (focused ? 'ease-in' : 'ease-out')};
`
const InputColumn = styled(Column)<{ approved?: boolean }>`
margin: 0.75em;
position: relative;
@@ -22,12 +35,15 @@ const InputColumn = styled(Column)<{ approved?: boolean }>`
}
`
interface InputProps {
disabled?: boolean
export interface InputProps {
disabled: boolean
focused: boolean
}
export default function Input({ disabled }: InputProps) {
export default function Input({ disabled, focused }: InputProps) {
const { i18n } = useLingui()
const {
trade: { state: tradeState },
currencyBalances: { [Field.INPUT]: balance },
currencyAmounts: { [Field.INPUT]: inputCurrencyAmount },
} = useSwapInfo()
@@ -39,22 +55,25 @@ export default function Input({ disabled }: InputProps) {
// extract eagerly in case of reversal
usePrefetchCurrencyColor(swapInputCurrency)
const isRouteLoading = tradeState === TradeState.SYNCING || tradeState === TradeState.LOADING
const isDependentField = useAtomValue(independentFieldAtom) !== Field.INPUT
const isLoading = isRouteLoading && isDependentField
//TODO(ianlapham): mimic logic from app swap page
const mockApproved = true
const onMax = useCallback(() => {
if (balance) {
updateSwapInputAmount(balance.toExact())
// account for gas needed if using max on native token
const maxAmount = useMemo(() => maxAmountSpend(balance), [balance])
const onMax = useMemo(() => {
if (maxAmount?.greaterThan(0)) {
return () => updateSwapInputAmount(maxAmount.toExact())
}
}, [balance, updateSwapInputAmount])
return
}, [maxAmount, updateSwapInputAmount])
return (
<InputColumn gap={0.5} approved={mockApproved}>
<Row>
<ThemedText.Subhead2 color="secondary">
<Trans>Trading</Trans>
</ThemedText.Subhead2>
</Row>
<TokenInput
currency={swapInputCurrency}
amount={(swapInputAmount !== undefined ? swapInputAmount : inputCurrencyAmount?.toSignificant(6)) ?? ''}
@@ -62,14 +81,15 @@ export default function Input({ disabled }: InputProps) {
onMax={onMax}
onChangeInput={updateSwapInputAmount}
onChangeCurrency={updateSwapInputCurrency}
loading={isLoading}
>
<ThemedText.Body2 color="secondary">
<Row>
{inputUSDC ? `$${inputUSDC.toFixed(2)}` : '-'}
<LoadingRow $loading={isLoading}>{inputUSDC ? `$${inputUSDC.toFixed(2)}` : '-'}</LoadingRow>
{balance && (
<ThemedText.Body2 color={inputCurrencyAmount?.greaterThan(balance) ? 'error' : undefined}>
Balance: <span style={{ userSelect: 'text' }}>{formatCurrencyAmount(balance, 4)}</span>
</ThemedText.Body2>
<Balance color={inputCurrencyAmount?.greaterThan(balance) ? 'error' : undefined} focused={focused}>
Balance: <span style={{ userSelect: 'text' }}>{formatCurrencyAmount(balance, 4, i18n.locale)}</span>
</Balance>
)}
</Row>
</ThemedText.Body2>

View File

@@ -1,18 +1,22 @@
import { Trans } from '@lingui/macro'
import { useLingui } from '@lingui/react'
import { useUSDCValue } from 'hooks/useUSDCPrice'
import { atom } from 'jotai'
import { useAtomValue } from 'jotai/utils'
import BrandedFooter from 'lib/components/BrandedFooter'
import { useSwapAmount, useSwapCurrency, useSwapInfo } from 'lib/hooks/swap'
import useCurrencyColor from 'lib/hooks/useCurrencyColor'
import { Field } from 'lib/state/swap'
import { Field, independentFieldAtom } from 'lib/state/swap'
import styled, { DynamicThemeProvider, ThemedText } from 'lib/theme'
import { ReactNode, useCallback, useMemo } from 'react'
import { PropsWithChildren, useMemo } from 'react'
import { TradeState } from 'state/routing/types'
import { computeFiatValuePriceImpact } from 'utils/computeFiatValuePriceImpact'
import { formatCurrencyAmount } from 'utils/formatCurrencyAmount'
import { getPriceImpactWarning } from 'utils/prices'
import Column from '../Column'
import Row from '../Row'
import { Balance, InputProps, LoadingRow } from './Input'
import TokenInput from './TokenInput'
export const colorAtom = atom<string | undefined>(undefined)
@@ -21,24 +25,23 @@ 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)};
}
`
interface OutputProps {
disabled?: boolean
children: ReactNode
}
export default function Output({ disabled, focused, children }: PropsWithChildren<InputProps>) {
const { i18n } = useLingui()
export default function Output({ disabled, children }: OutputProps) {
const {
trade: { state: tradeState },
currencyBalances: { [Field.OUTPUT]: balance },
currencyAmounts: { [Field.INPUT]: inputCurrencyAmount, [Field.OUTPUT]: outputCurrencyAmount },
} = useSwapInfo()
@@ -46,6 +49,10 @@ export default function Output({ disabled, children }: OutputProps) {
const [swapOutputAmount, updateSwapOutputAmount] = useSwapAmount(Field.OUTPUT)
const [swapOutputCurrency, updateSwapOutputCurrency] = useSwapCurrency(Field.OUTPUT)
const isRouteLoading = tradeState === TradeState.SYNCING || tradeState === TradeState.LOADING
const isDependentField = useAtomValue(independentFieldAtom) !== Field.OUTPUT
const isLoading = isRouteLoading && isDependentField
const overrideColor = useAtomValue(colorAtom)
const dynamicColor = useCurrencyColor(swapOutputCurrency)
const color = overrideColor || dynamicColor
@@ -57,46 +64,45 @@ export default function Output({ disabled, children }: OutputProps) {
const outputUSDC = useUSDCValue(outputCurrencyAmount)
const priceImpact = useMemo(() => {
const computedChange = computeFiatValuePriceImpact(inputUSDC, outputUSDC)
return computedChange ? parseFloat(computedChange.multiply(-1)?.toSignificant(3)) : undefined
const fiatValuePriceImpact = computeFiatValuePriceImpact(inputUSDC, outputUSDC)
if (!fiatValuePriceImpact) return null
const color = getPriceImpactWarning(fiatValuePriceImpact)
const sign = fiatValuePriceImpact.lessThan(0) ? '+' : ''
const displayedPriceImpact = parseFloat(fiatValuePriceImpact.multiply(-1)?.toSignificant(3))
return (
<ThemedText.Body2 color={color}>
({sign}
{displayedPriceImpact}%)
</ThemedText.Body2>
)
}, [inputUSDC, outputUSDC])
const usdc = useMemo(() => {
if (outputUSDC) {
return `$${outputUSDC.toFixed(2)} (${priceImpact && priceImpact > 0 ? '+' : ''}${priceImpact}%)`
}
return ''
}, [priceImpact, outputUSDC])
const onMax = useCallback(() => {
if (balance) {
updateSwapOutputAmount(balance.toExact())
}
}, [balance, updateSwapOutputAmount])
return (
<DynamicThemeProvider color={color}>
<OutputColumn hasColor={hasColor} gap={0.5}>
<Row>
<ThemedText.Subhead2 color="secondary">
<ThemedText.Subhead1 color="secondary">
<Trans>For</Trans>
</ThemedText.Subhead2>
</ThemedText.Subhead1>
</Row>
<TokenInput
currency={swapOutputCurrency}
amount={(swapOutputAmount !== undefined ? swapOutputAmount : outputCurrencyAmount?.toSignificant(6)) ?? ''}
disabled={disabled}
onMax={onMax}
onChangeInput={updateSwapOutputAmount}
onChangeCurrency={updateSwapOutputCurrency}
loading={isLoading}
>
<ThemedText.Body2 color="secondary">
<Row>
{usdc}
<LoadingRow gap={0.5} $loading={isLoading}>
{outputUSDC?.toFixed(2)} {priceImpact}
</LoadingRow>
{balance && (
<span>
Balance: <span style={{ userSelect: 'text' }}>{formatCurrencyAmount(balance, 4)}</span>
</span>
<Balance focused={focused}>
Balance: <span style={{ userSelect: 'text' }}>{formatCurrencyAmount(balance, 4, i18n.locale)}</span>
</Balance>
)}
</Row>
</ThemedText.Body2>

View File

@@ -9,8 +9,7 @@ import Row from '../Row'
const ReverseRow = styled(Row)`
left: 50%;
position: absolute;
top: 7.45em;
transform: translateX(-50%);
transform: translate(-50%, -50%);
z-index: ${Layer.OVERLAY};
`

View File

@@ -1,10 +1,12 @@
import { t, Trans } from '@lingui/macro'
import { Percent } from '@uniswap/sdk-core'
import { Trans } from '@lingui/macro'
import { useAtom } from 'jotai'
import { Check, LargeIcon } from 'lib/icons'
import { maxSlippageAtom } from 'lib/state/settings'
import Popover from 'lib/components/Popover'
import { useTooltip } from 'lib/components/Tooltip'
import { getSlippageWarning, toPercent } from 'lib/hooks/useAllowedSlippage'
import { AlertTriangle, Check, Icon, LargeIcon, XOctagon } from 'lib/icons'
import { autoSlippageAtom, maxSlippageAtom } from 'lib/state/settings'
import styled, { ThemedText } from 'lib/theme'
import { PropsWithChildren, useCallback, useRef, useState } from 'react'
import { forwardRef, memo, ReactNode, useCallback, useMemo, useRef, useState } from 'react'
import { BaseButton, TextButton } from '../../Button'
import Column from '../../Column'
@@ -15,6 +17,10 @@ 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 placeholder = '0.10'
const Button = styled(TextButton)<{ selected: boolean }>`
${({ selected }) => optionCss(selected)}
@@ -23,7 +29,6 @@ const Button = styled(TextButton)<{ selected: boolean }>`
const Custom = styled(BaseButton)<{ selected: boolean }>`
${({ selected }) => optionCss(selected)}
${inputCss}
border-color: ${({ selected, theme }) => (selected ? theme.active : 'transparent')} !important;
padding: calc(0.75em - 3px) 0.625em;
`
@@ -31,55 +36,107 @@ interface OptionProps {
wrapper: typeof Button | typeof Custom
selected: boolean
onSelect: () => void
icon?: ReactNode
tabIndex?: number
children: ReactNode
}
function Option({ wrapper: Wrapper, children, selected, onSelect }: PropsWithChildren<OptionProps>) {
const Option = forwardRef<HTMLButtonElement, OptionProps>(function Option(
{ wrapper: Wrapper, children, selected, onSelect, icon, tabIndex }: OptionProps,
ref
) {
return (
<Wrapper selected={selected} onClick={onSelect}>
<Wrapper selected={selected} onClick={onSelect} ref={ref} tabIndex={tabIndex}>
<Row gap={0.5}>
{children}
<span style={{ width: '1.2em' }}>{selected && <LargeIcon icon={Check} />}</span>
{icon ? icon : <LargeIcon icon={selected ? Check : undefined} size={1.25} />}
</Row>
</Wrapper>
)
}
})
const Warning = memo(function Warning({ state, showTooltip }: { state?: 'warning' | 'error'; showTooltip: boolean }) {
let icon: Icon | undefined
let content: ReactNode
let show = showTooltip
switch (state) {
case 'error':
icon = XOctagon
content = invalidSlippage
show = true
break
case 'warning':
icon = AlertTriangle
content = highSlippage
break
}
return (
<Popover
key={state}
content={<ThemedText.Caption>{content}</ThemedText.Caption>}
show={show}
placement="top"
offset={16}
contained
>
<LargeIcon icon={icon} color={state} size={1.25} />
</Popover>
)
})
export default function MaxSlippageSelect() {
const [autoSlippage, setAutoSlippage] = useAtom(autoSlippageAtom)
const [maxSlippage, setMaxSlippage] = useAtom(maxSlippageAtom)
const maxSlippageInput = useMemo(() => maxSlippage?.toString() || '', [maxSlippage])
const [warning, setWarning] = useState<'warning' | 'error' | undefined>(getSlippageWarning(toPercent(maxSlippage)))
const option = useRef<HTMLButtonElement>(null)
const showTooltip = useTooltip(option.current)
const [custom, setCustom] = useState('')
const input = useRef<HTMLInputElement>(null)
const focus = useCallback(() => input.current?.focus(), [input])
const onInputChange = useCallback(
(custom: string) => {
setCustom(custom)
const numerator = Math.floor(+custom * 100)
if (numerator) {
setMaxSlippage(new Percent(numerator, 10_000))
} else {
setMaxSlippage('auto')
}
const processValue = useCallback(
(value: number | undefined) => {
const percent = toPercent(value)
const warning = getSlippageWarning(percent)
setWarning(warning)
setMaxSlippage(value)
setAutoSlippage(!percent || warning === 'error')
},
[setMaxSlippage]
[setAutoSlippage, setMaxSlippage]
)
const onInputSelect = useCallback(() => {
focus()
onInputChange(custom)
}, [custom, focus, onInputChange])
processValue(maxSlippage)
}, [focus, maxSlippage, processValue])
return (
<Column gap={0.75}>
<Label name={<Trans>Max slippage</Trans>} tooltip={tooltip} />
<Row gap={0.5} grow="last">
<Option wrapper={Button} selected={maxSlippage === 'auto'} onSelect={() => setMaxSlippage('auto')}>
<Option wrapper={Button} selected={autoSlippage} onSelect={() => setAutoSlippage(true)}>
<ThemedText.ButtonMedium>
<Trans>Auto</Trans>
</ThemedText.ButtonMedium>
</Option>
<Option wrapper={Custom} onSelect={onInputSelect} selected={maxSlippage !== 'auto'}>
<Row>
<DecimalInput value={custom} onChange={onInputChange} placeholder={t`Custom`} ref={input} />%
<Option
wrapper={Custom}
selected={!autoSlippage}
onSelect={onInputSelect}
icon={warning && <Warning state={warning} showTooltip={showTooltip} />}
ref={option}
tabIndex={-1}
>
<Row color={warning === 'error' ? 'error' : undefined}>
<DecimalInput
size={Math.max(maxSlippageInput.length, 3)}
value={maxSlippageInput}
onChange={(input) => processValue(+input)}
placeholder={placeholder}
ref={input}
/>
%
</Row>
</Option>
</Row>

View File

@@ -1,6 +1,5 @@
import { Trans } from '@lingui/macro'
import { useAtom } from 'jotai'
import { TRANSACTION_TTL_DEFAULT, transactionTtlAtom } from 'lib/state/settings'
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?.toString() ?? ''}
onChange={(value) => setTransactionTtl(value ? parseFloat(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

@@ -14,9 +14,17 @@ export const optionCss = (selected: boolean) => css`
grid-gap: 0.25em;
padding: calc(0.75em - 1px) 0.625em;
:enabled {
border: 1px solid ${({ theme }) => (selected ? theme.active : theme.outline)};
}
: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

@@ -1,10 +1,13 @@
import { Trans } from '@lingui/macro'
import ErrorDialog, { StatusHeader } from 'lib/components/Error/ErrorDialog'
import EtherscanLink from 'lib/components/EtherscanLink'
import useInterval from 'lib/hooks/useInterval'
import { CheckCircle, Clock, Spinner } from 'lib/icons'
import { SwapTransactionInfo, Transaction } from 'lib/state/transactions'
import styled, { ThemedText } from 'lib/theme'
import ms from 'ms.macro'
import { useCallback, useMemo, useState } from 'react'
import { ExplorerDataType } from 'utils/getExplorerLink'
import ActionButton from '../../ActionButton'
import Column from '../../Column'
@@ -25,16 +28,9 @@ const TransactionRow = styled(Row)`
function ElapsedTime({ tx }: { tx: Transaction<SwapTransactionInfo> }) {
const [elapsedMs, setElapsedMs] = useState(0)
useInterval(
() => {
if (tx.info.response.timestamp) {
setElapsedMs(tx.info.response.timestamp - tx.addedTime)
} else {
setElapsedMs(Date.now() - tx.addedTime)
}
},
elapsedMs === tx.info.response.timestamp ? null : 1000
)
useInterval(() => setElapsedMs(Date.now() - tx.addedTime), tx.receipt ? null : ms`1s`)
const toElapsedTime = useCallback((ms: number) => {
let sec = Math.floor(ms / 1000)
const min = Math.floor(sec / 60)
@@ -57,11 +53,6 @@ function ElapsedTime({ tx }: { tx: Transaction<SwapTransactionInfo> }) {
)
}
const EtherscanA = styled.a`
color: ${({ theme }) => theme.accent};
text-decoration: none;
`
interface TransactionStatusProps {
tx: Transaction<SwapTransactionInfo>
onClose: () => void
@@ -74,6 +65,7 @@ function TransactionStatus({ tx, onClose }: TransactionStatusProps) {
const heading = useMemo(() => {
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.receipt?.status ? 'success' : undefined}>
@@ -82,9 +74,9 @@ function TransactionStatus({ tx, onClose }: TransactionStatusProps) {
</StatusHeader>
<TransactionRow flex>
<ThemedText.ButtonSmall>
<EtherscanA href="//etherscan.io" target="_blank">
<EtherscanLink type={ExplorerDataType.TRANSACTION} data={tx.info.response.hash}>
<Trans>View on Etherscan</Trans>
</EtherscanA>
</EtherscanLink>
</ThemedText.ButtonSmall>
<ElapsedTime tx={tx} />
</TransactionRow>
@@ -101,7 +93,7 @@ export default function TransactionStatusDialog({ tx, onClose }: TransactionStat
header={errorMessage}
error={new Error('TODO(zzmp)')}
action={<Trans>Dismiss</Trans>}
onAction={onClose}
onClick={onClose}
/>
) : (
<TransactionStatus tx={tx} onClose={onClose} />

View File

@@ -1,26 +1,35 @@
import { t } from '@lingui/macro'
import { useLingui } from '@lingui/react'
import { Trade } from '@uniswap/router-sdk'
import { Currency, Percent, TradeType } from '@uniswap/sdk-core'
import { useAtom } from 'jotai'
import { integratorFeeAtom } from 'lib/state/settings'
import { ThemedText } from 'lib/theme'
import { useAtomValue } from 'jotai/utils'
import { getSlippageWarning } from 'lib/hooks/useAllowedSlippage'
import { feeOptionsAtom } from 'lib/state/swap'
import styled, { Color, ThemedText } from 'lib/theme'
import { useMemo } from 'react'
import { currencyId } from 'utils/currencyId'
import { computeRealizedLPFeePercent } from 'utils/prices'
import { formatCurrencyAmount } from 'utils/formatCurrencyAmount'
import { computeRealizedLPFeeAmount, computeRealizedPriceImpact, getPriceImpactWarning } 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>
)
@@ -35,38 +44,61 @@ export default function Details({ trade, allowedSlippage }: DetailsProps) {
const { inputAmount, outputAmount } = trade
const inputCurrency = inputAmount.currency
const outputCurrency = outputAmount.currency
const integrator = window.location.hostname
const [integratorFee] = useAtom(integratorFeeAtom)
const feeOptions = useAtomValue(feeOptionsAtom)
const priceImpact = useMemo(() => computeRealizedPriceImpact(trade), [trade])
const lpFeeAmount = useMemo(() => computeRealizedLPFeeAmount(trade), [trade])
const { i18n } = useLingui()
const priceImpact = useMemo(() => {
const realizedLpFeePercent = computeRealizedLPFeePercent(trade)
return trade.priceImpact.subtract(realizedLpFeePercent)
}, [trade])
const details = useMemo((): [string, string][] => {
const details = useMemo(() => {
const rows: Array<[string, string] | [string, string, Color | undefined]> = []
// @TODO(ianlapham): Check that provider fee is even a valid list item
return [
// [t`Liquidity provider fee`, `${swap.lpFee} ${inputSymbol}`],
[t`${integrator} fee`, integratorFee && `${integratorFee} ${currencyId(inputCurrency)}`],
[t`Price impact`, `${priceImpact.toFixed(2)}%`],
trade.tradeType === TradeType.EXACT_INPUT
? [t`Maximum sent`, `${trade.maximumAmountIn(allowedSlippage).toSignificant(6)} ${inputCurrency.symbol}`]
: [],
trade.tradeType === TradeType.EXACT_OUTPUT
? [t`Minimum received`, `${trade.minimumAmountOut(allowedSlippage).toSignificant(6)} ${outputCurrency.symbol}`]
: [],
[t`Slippage tolerance`, `${allowedSlippage.toFixed(2)}%`],
].filter(isDetail)
function isDetail(detail: unknown[]): detail is [string, string] {
return Boolean(detail[1])
if (feeOptions) {
const fee = outputAmount.multiply(feeOptions.fee)
if (fee.greaterThan(0)) {
const parsedFee = formatCurrencyAmount(fee, 6, i18n.locale)
rows.push([t`${integrator} fee`, `${parsedFee} ${outputCurrency.symbol || currencyId(outputCurrency)}`])
}
}
}, [allowedSlippage, inputCurrency, integrator, integratorFee, outputCurrency.symbol, priceImpact, trade])
rows.push([t`Price impact`, `${priceImpact.toFixed(2)}%`, getPriceImpactWarning(priceImpact)])
if (lpFeeAmount) {
const parsedLpFee = formatCurrencyAmount(lpFeeAmount, 6, i18n.locale)
rows.push([t`Liquidity provider fee`, `${parsedLpFee} ${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}`])
}
rows.push([t`Slippage tolerance`, `${allowedSlippage.toFixed(2)}%`, getSlippageWarning(allowedSlippage)])
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} />
))}
</>
)

View File

@@ -1,3 +1,4 @@
import { useLingui } from '@lingui/react'
import { Currency, CurrencyAmount } from '@uniswap/sdk-core'
import { useUSDCValue } from 'hooks/useUSDCPrice'
import { ArrowRight } from 'lib/icons'
@@ -5,6 +6,7 @@ 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'
@@ -21,9 +23,10 @@ interface TokenValueProps {
}
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
@@ -36,13 +39,13 @@ function TokenValue({ input, usdc, change }: TokenValueProps) {
<Row gap={0.375} justify="flex-start">
<TokenImg token={input.currency} />
<ThemedText.Body2>
{input.toSignificant(6)} {input.currency.symbol}
{formatCurrencyAmount(input, 6, i18n.locale)} {input.currency.symbol}
</ThemedText.Body2>
</Row>
{usdc && usdcAmount && (
<Row justify="flex-start">
<ThemedText.Caption color="secondary">
${usdcAmount.toFixed(2)}
${formatCurrencyAmount(usdcAmount, 2, i18n.locale)}
{change && <Percent gain={change > 0}> {percent}</Percent>}
</ThemedText.Caption>
</Row>

View File

@@ -1,16 +1,21 @@
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 { useAtomValue } from 'jotai/utils'
import { IconButton } from 'lib/components/Button'
import { getSlippageWarning } from 'lib/hooks/useAllowedSlippage'
import useScrollbar from 'lib/hooks/useScrollbar'
import { Expando, Info } from 'lib/icons'
import { AlertTriangle, BarChart, Expando, Info } from 'lib/icons'
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, getPriceImpactWarning } from 'utils/prices'
import { tradeMeaningfullyDiffers } from 'utils/tradeMeaningFullyDiffer'
import ActionButton from '../../ActionButton'
import ActionButton, { Action } from '../../ActionButton'
import Column from '../../Column'
import { Header } from '../../Dialog'
import Row from '../../Row'
@@ -37,13 +42,14 @@ const Body = styled(Column)<{ open: boolean }>`
transition: flex-grow 0.25s;
${DetailsColumn} {
flex-basis: ${({ open }) => (open ? 7 : 0)}em;
flex-basis: ${({ open }) => (open ? 7.5 : 0)}em;
overflow-y: hidden;
position: relative;
transition: flex-basis 0.25s;
${Column} {
height: 100%;
height: 7.5em;
grid-template-rows: repeat(auto-fill, 1em);
padding: ${({ open }) => (open ? '0.5em 0' : 0)};
transition: padding 0.25s;
@@ -61,6 +67,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 }) =>
@@ -71,8 +78,6 @@ 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
@@ -80,23 +85,46 @@ interface SummaryDialogProps {
}
export function SummaryDialog({ trade, allowedSlippage, onConfirm }: SummaryDialogProps) {
const { inputAmount, outputAmount } = trade
const { inputAmount, outputAmount, executionPrice } = trade
const inputCurrency = inputAmount.currency
const outputCurrency = outputAmount.currency
const price = trade.executionPrice
const priceImpact = useMemo(() => computeRealizedPriceImpact(trade), [trade])
const independentField = useAtomValue(independentFieldAtom)
const { i18n } = useLingui()
const [open, setOpen] = useState(false)
const [details, setDetails] = useState<HTMLDivElement | null>(null)
const scrollbar = useScrollbar(details)
const warning = useMemo(() => {
return getPriceImpactWarning(priceImpact) || getSlippageWarning(allowedSlippage)
}, [allowedSlippage, priceImpact])
const [ackPriceImpact, setAckPriceImpact] = useState(false)
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)
const action = useMemo((): Action | undefined => {
if (doesTradeDiffer) {
return {
message: <Trans>Price updated</Trans>,
icon: BarChart,
onClick: () => setConfirmedTrade(trade),
children: <Trans>Accept</Trans>,
}
} else if (getPriceImpactWarning(priceImpact) === 'error' && !ackPriceImpact) {
return {
message: <Trans>High price impact</Trans>,
onClick: () => setAckPriceImpact(true),
children: <Trans>Acknowledge</Trans>,
}
}
return
}, [ackPriceImpact, doesTradeDiffer, priceImpact, trade])
if (!(inputAmount && outputAmount && inputCurrency && outputCurrency)) {
return null
@@ -109,13 +137,14 @@ export function SummaryDialog({ trade, allowedSlippage, onConfirm }: SummaryDial
<SummaryColumn gap={0.75} flex justify="center">
<Summary input={inputAmount} output={outputAmount} usdc={true} />
<ThemedText.Caption>
1 {inputCurrency.symbol} = {price?.toSignificant(6)} {outputCurrency.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>
@@ -133,22 +162,19 @@ export function SummaryDialog({ trade, allowedSlippage, onConfirm }: SummaryDial
<Trans>Output is estimated.</Trans>
{independentField === Field.INPUT && (
<Trans>
You will send at most {trade.maximumAmountIn(allowedSlippage).toSignificant(6)} {inputCurrency.symbol}{' '}
You will receive at least{' '}
{formatCurrencyAmount(trade.minimumAmountOut(allowedSlippage), 6, i18n.locale)} {outputCurrency.symbol}{' '}
or the transaction will revert.
</Trans>
)}
{independentField === Field.OUTPUT && (
<Trans>
You will receive at least {trade.minimumAmountOut(allowedSlippage).toSignificant(6)}{' '}
{outputCurrency.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={() => setConfirmedTrade(trade)}
update={doesTradeDiffer ? priceUpdate : undefined}
>
<ActionButton onClick={onConfirm} action={action}>
<Trans>Confirm swap</Trans>
</ActionButton>
</ExpandoColumn>

View File

@@ -25,6 +25,18 @@ function Fixture() {
}
}, [color, setColor])
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',
@@ -52,11 +64,13 @@ function Fixture() {
return (
<Swap
tokenList={tokens}
convenienceFee={convenienceFee}
convenienceFeeRecipient={convenienceFeeRecipient}
defaultInputAddress={optionsToAddressMap[defaultInput]}
defaultInputAmount={defaultInputAmount}
defaultOutputAddress={optionsToAddressMap[defaultOutput]}
defaultOutputAmount={defaultOutputAmount}
tokenList={tokens}
/>
)
}

View File

@@ -1,6 +1,5 @@
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'
@@ -18,12 +17,14 @@ 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 from 'lib/theme'
import { useTheme } from 'lib/theme'
import { useCallback, useEffect, useMemo, useState } from 'react'
import invariant from 'tiny-invariant'
import { ExplorerDataType } from 'utils/getExplorerLink'
import ActionButton from '../ActionButton'
import ActionButton, { ActionButtonProps } from '../ActionButton'
import Dialog from '../Dialog'
import EtherscanLink from '../EtherscanLink'
import Row from '../Row'
import { SummaryDialog } from './Summary'
@@ -31,11 +32,6 @@ interface SwapButtonProps {
disabled?: boolean
}
const EtherscanA = styled.a`
color: currentColor;
text-decoration: none;
`
function useIsPendingApproval(token?: Token, spender?: string): boolean {
return Boolean(usePendingApproval(token, spender))
}
@@ -43,12 +39,15 @@ function useIsPendingApproval(token?: Token, spender?: string): boolean {
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)
@@ -77,32 +76,36 @@ export default function SwapButton({ disabled }: SwapButtonProps) {
})
}, [addTransaction, getApproval])
const actionProps = useMemo(() => {
const actionProps = useMemo((): Partial<ActionButtonProps> | undefined => {
if (disabled) return { disabled: true }
if (chainId && inputCurrencyAmount && inputCurrencyBalance?.greaterThan(inputCurrencyAmount)) {
if (approval === ApprovalState.PENDING) {
if (chainId && inputCurrencyAmount) {
if (!inputCurrencyBalance || inputCurrencyBalance.lessThan(inputCurrencyAmount)) {
return { disabled: true }
} else if (approval === ApprovalState.PENDING) {
return {
disabled: true,
update: {
action: {
message: (
<EtherscanA href={approvalHash && `${CHAIN_INFO[chainId].explorer}tx/${approvalHash}`} target="_blank">
<EtherscanLink type={ExplorerDataType.TRANSACTION} data={approvalHash}>
<Row gap={0.25}>
<Trans>
Approval pending <Link />
</Trans>
</Row>
</EtherscanA>
</EtherscanLink>
),
action: <Trans>Approve</Trans>,
icon: Spinner,
onClick: addApprovalTransaction,
children: <Trans>Approve</Trans>,
},
}
} else if (approval === ApprovalState.NOT_APPROVED) {
return {
update: {
action: {
message: <Trans>Approve {inputCurrencyAmount.currency.symbol} first</Trans>,
action: <Trans>Approve</Trans>,
onClick: addApprovalTransaction,
children: <Trans>Approve</Trans>,
},
}
}
@@ -110,20 +113,20 @@ export default function SwapButton({ disabled }: SwapButtonProps) {
}
return { disabled: true }
}, [approval, approvalHash, chainId, disabled, inputCurrencyAmount, inputCurrencyBalance])
}, [addApprovalTransaction, approval, approvalHash, chainId, disabled, inputCurrencyAmount, inputCurrencyBalance])
// @TODO(ianlapham): connect deadline from state instead of passing undefined.
const { signatureData } = useERC20PermitFromTrade(optimizedTrade, allowedSlippage, undefined)
const deadline = useTransactionDeadline()
const { signatureData } = useERC20PermitFromTrade(optimizedTrade, allowedSlippage, deadline)
// the callback to execute the swap
const { callback: swapCallback } = useSwapCallback(
optimizedTrade,
const { callback: swapCallback } = useSwapCallback({
trade: optimizedTrade,
allowedSlippage,
account ?? null,
recipientAddressOrName: account ?? null,
signatureData,
deadline
)
deadline,
feeOptions,
})
//@TODO(ianlapham): add a loading state, process errors
const setDisplayTxHash = useUpdateAtom(displayTxHashAtom)
@@ -145,14 +148,16 @@ export default function SwapButton({ disabled }: SwapButtonProps) {
//@TODO(ianlapham): add error handling
console.log(error)
})
.finally(() => {
setActiveTrade(undefined)
})
}, [addTransaction, independentField, inputCurrencyAmount, outputCurrencyAmount, setDisplayTxHash, swapCallback])
return (
<>
<ActionButton
color="interactive"
color={tokenColorExtraction ? 'interactive' : 'accent'}
onClick={() => setActiveTrade(trade.trade)}
onUpdate={addApprovalTransaction}
{...actionProps}
>
<Trans>Review swap</Trans>

View File

@@ -1,5 +1,6 @@
import { Trans } from '@lingui/macro'
import { Currency } from '@uniswap/sdk-core'
import { loadingOpacityCss } from 'lib/css/loading'
import styled, { keyframes, ThemedText } from 'lib/theme'
import { FocusEvent, ReactNode, useCallback, useRef, useState } from 'react'
@@ -13,8 +14,9 @@ const TokenInputRow = styled(Row)`
grid-template-columns: 1fr;
`
const ValueInput = styled(DecimalInput)`
const ValueInput = styled(DecimalInput)<{ $loading: boolean }>`
color: ${({ theme }) => theme.primary};
height: 1em;
:hover:not(:focus-within) {
color: ${({ theme }) => theme.onHover(theme.primary)};
@@ -23,6 +25,8 @@ const ValueInput = styled(DecimalInput)`
:hover:not(:focus-within)::placeholder {
color: ${({ theme }) => theme.onHover(theme.secondary)};
}
${loadingOpacityCss}
`
const delayedFadeIn = keyframes`
@@ -50,6 +54,7 @@ interface TokenInputProps {
onMax?: () => void
onChangeInput: (input: string) => void
onChangeCurrency: (currency: Currency) => void
loading?: boolean
children: ReactNode
}
@@ -60,16 +65,27 @@ export default function TokenInput({
onMax,
onChangeInput,
onChangeCurrency,
loading,
children,
}: TokenInputProps) {
const max = useRef<HTMLButtonElement>(null)
const [showMax, setShowMax] = useState(false)
const onFocus = useCallback(() => setShowMax(Boolean(onMax)), [onMax])
const onBlur = useCallback((e: FocusEvent) => {
if (e.relatedTarget !== max.current) {
if (e.relatedTarget !== max.current && e.relatedTarget !== input.current) {
setShowMax(false)
}
}, [])
const input = useRef<HTMLInputElement>(null)
const onSelect = useCallback(
(currency: Currency) => {
onChangeCurrency(currency)
setTimeout(() => input.current?.focus(), 0)
},
[onChangeCurrency]
)
return (
<Column gap={0.25}>
<TokenInputRow gap={0.5} onBlur={onBlur}>
@@ -79,6 +95,8 @@ export default function TokenInput({
onFocus={onFocus}
onChange={onChangeInput}
disabled={disabled || !currency}
$loading={Boolean(loading)}
ref={input}
></ValueInput>
</ThemedText.H2>
{showMax && (
@@ -88,7 +106,7 @@ export default function TokenInput({
</ThemedText.ButtonMedium>
</MaxButton>
)}
<TokenSelect value={currency} collapsed={showMax} disabled={disabled} onSelect={onChangeCurrency} />
<TokenSelect value={currency} collapsed={showMax} disabled={disabled} onSelect={onSelect} />
</TokenInputRow>
{children}
</Column>

View File

@@ -1,171 +0,0 @@
import { Trans } from '@lingui/macro'
import { Currency, CurrencyAmount, TradeType } from '@uniswap/sdk-core'
import { ALL_SUPPORTED_CHAIN_IDS } from 'constants/chains'
import useUSDCPrice from 'hooks/useUSDCPrice'
import { useSwapInfo } from 'lib/hooks/swap'
import useActiveWeb3React from 'lib/hooks/useActiveWeb3React'
import { AlertTriangle, Info, largeIconCss, Spinner } from 'lib/icons'
import { Field } from 'lib/state/swap'
import styled, { ThemedText } from 'lib/theme'
import { useMemo, useState } from 'react'
import { InterfaceTrade, TradeState } from 'state/routing/types'
import { TextButton } from '../Button'
import Row from '../Row'
import Rule from '../Rule'
const ToolbarRow = styled(Row)`
padding: 0.5em 0;
${largeIconCss}
`
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>
)
*/
}
interface LoadedStateProps {
inputAmount: CurrencyAmount<Currency>
outputAmount: CurrencyAmount<Currency>
trade: InterfaceTrade<Currency, Currency, TradeType> | undefined
}
function LoadedState({ inputAmount, outputAmount, trade }: LoadedStateProps) {
const [flip, setFlip] = useState(true)
const executionPrice = trade?.executionPrice
const fiatValueInput = useUSDCPrice(inputAmount.currency)
const fiatValueOutput = useUSDCPrice(outputAmount.currency)
const ratio = useMemo(() => {
const [a, b] = flip ? [outputAmount, inputAmount] : [inputAmount, outputAmount]
const ratio = `1 ${a.currency.symbol} = ${executionPrice?.toSignificant(6)} ${b.currency.symbol}`
const usdc = !flip
? fiatValueInput
? ` ($${fiatValueInput.toSignificant(2)})`
: null
: fiatValueOutput
? ` ($${fiatValueOutput.toSignificant(2)})`
: 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 (
<TextButton color="primary" onClick={() => setFlip(!flip)}>
{ratio}
</TextButton>
)
}
export default function Toolbar({ disabled }: { disabled?: boolean }) {
const { chainId } = useActiveWeb3React()
const {
trade,
currencies: { [Field.INPUT]: inputCurrency, [Field.OUTPUT]: outputCurency },
currencyBalances: { [Field.INPUT]: balance },
currencyAmounts: { [Field.INPUT]: inputAmount, [Field.OUTPUT]: outputAmount },
} = useSwapInfo()
const [routeFound, routeIsLoading, routeIsSyncing] = useMemo(
() => [Boolean(trade?.trade?.swaps), TradeState.LOADING === trade?.state, TradeState.SYNCING === trade?.state],
[trade]
)
const caption = useMemo(() => {
if (disabled) {
return (
<>
<AlertTriangle color="secondary" />
<Trans>Connect wallet to swap</Trans>
</>
)
}
if (chainId && !ALL_SUPPORTED_CHAIN_IDS.includes(chainId)) {
return (
<>
<AlertTriangle color="secondary" />
<Trans>Unsupported network&#8211;switch to another to trade.</Trans>
</>
)
}
if (inputCurrency && outputCurency) {
if (!trade?.trade || routeIsLoading || routeIsSyncing) {
return (
<>
<Spinner color="secondary" />
<Trans>Fetching best price</Trans>
</>
)
}
if (inputAmount && balance && inputAmount.greaterThan(balance)) {
return (
<>
<AlertTriangle color="secondary" />
<Trans>Insufficient {inputCurrency?.symbol}</Trans>
</>
)
}
if (inputCurrency && outputCurency && !routeFound && !routeIsLoading && !routeIsSyncing) {
return (
<>
<AlertTriangle color="secondary" />
<Trans>Insufficient liquidity for this trade.</Trans>
</>
)
}
if (inputCurrency && inputAmount && outputCurency && outputAmount) {
return (
<>
<RoutingTooltip />
<LoadedState inputAmount={inputAmount} outputAmount={outputAmount} trade={trade?.trade} />
</>
)
}
}
return (
<>
<Info color="secondary" />
<Trans>Enter an amount</Trans>
</>
)
}, [
balance,
chainId,
disabled,
inputAmount,
inputCurrency,
outputAmount,
outputCurency,
routeFound,
routeIsLoading,
routeIsSyncing,
trade?.trade,
])
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,66 @@
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 isRouteLoading = state === TradeState.SYNCING || state === TradeState.LOADING
const isAmountPopulated = useIsAmountPopulated()
const caption = useMemo(() => {
if (disabled) {
return <Caption.ConnectWallet />
}
if (chainId && !ALL_SUPPORTED_CHAIN_IDS.includes(chainId)) {
return <Caption.UnsupportedNetwork />
}
if (inputCurrency && outputCurrency && isAmountPopulated) {
if (isRouteLoading) {
return <Caption.LoadingTrade />
}
if (!trade?.swaps) {
return <Caption.InsufficientLiquidity />
}
if (balance && trade?.inputAmount.greaterThan(balance)) {
return <Caption.InsufficientBalance currency={trade.inputAmount.currency} />
}
if (trade.inputAmount && trade.outputAmount) {
return <Caption.Trade trade={trade} />
}
}
return <Caption.Empty />
}, [balance, chainId, disabled, inputCurrency, isAmountPopulated, isRouteLoading, outputCurrency, trade])
return (
<>
<Rule />
<ThemedText.Caption>
<ToolbarRow justify="flex-start" gap={0.5} iconSize={4 / 3}>
{caption}
</ToolbarRow>
</ThemedText.Caption>
</>
)
}

View File

@@ -1,14 +1,17 @@
import { Trans } from '@lingui/macro'
import { TokenInfo } from '@uniswap/token-lists'
import { ALL_SUPPORTED_CHAIN_IDS } from 'constants/chains'
import { useAtom } from 'jotai'
import useSwapDefaults from 'lib/hooks/swap/useSwapDefaults'
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 useHasFocus from 'lib/hooks/useHasFocus'
import useTokenList from 'lib/hooks/useTokenList'
import { displayTxHashAtom } from 'lib/state/swap'
import { SwapTransactionInfo, Transaction, TransactionType } from 'lib/state/transactions'
import { useState } from 'react'
import { useMemo, useState } from 'react'
import Dialog from '../Dialog'
import Header from '../Header'
@@ -43,31 +46,40 @@ export interface SwapProps {
defaultOutputAmount?: string
convenienceFee?: number
convenienceFeeRecipient?: string | { [chainId: number]: string }
onConnectWallet?: () => void
}
export default function Swap(props: SwapProps) {
useTokenList(props.tokenList)
useSwapDefaults(props)
const list = useTokenList(props.tokenList)
useSyncSwapDefaults(props)
useSyncConvenienceFee(props)
const { active, account } = useActiveWeb3React()
const [boundary, setBoundary] = useState<HTMLDivElement | null>(null)
const { active, account, chainId } = useActiveWeb3React()
const [wrapper, setWrapper] = useState<HTMLDivElement | null>(null)
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]
)
const focused = useHasFocus(wrapper)
return (
<SwapPropValidator {...props}>
<SwapInfoUpdater />
{onSupportedChain && <SwapInfoUpdater />}
<Header title={<Trans>Swap</Trans>}>
{active && <Wallet disabled={!account} />}
{active && <Wallet disabled={!account} onClick={props.onConnectWallet} />}
<Settings disabled={!active} />
</Header>
<div ref={setBoundary}>
<BoundaryProvider value={boundary}>
<Input disabled={!active} />
<div ref={setWrapper}>
<BoundaryProvider value={wrapper}>
<Input disabled={!active} focused={focused} />
<ReverseButton disabled={!active} />
<Output disabled={!active}>
<Output disabled={!active} focused={focused}>
<Toolbar disabled={!active} />
<SwapButton disabled={!account} />
</Output>

View File

@@ -1,10 +1,11 @@
import useTokenList, { DEFAULT_TOKEN_LIST } from 'lib/hooks/useTokenList'
import DEFAULT_TOKEN_LIST from '@uniswap/default-token-list'
import useTokenList from 'lib/hooks/useTokenList'
import { Modal } from './Dialog'
import { TokenSelectDialog } from './TokenSelect'
export default function Fixture() {
useTokenList(DEFAULT_TOKEN_LIST)
useTokenList(DEFAULT_TOKEN_LIST.tokens)
return (
<Modal color="module">

View File

@@ -2,6 +2,7 @@ import { Trans } from '@lingui/macro'
import { Currency } from '@uniswap/sdk-core'
import { ChevronDown } from 'lib/icons'
import styled, { ThemedText } from 'lib/theme'
import { useMemo } from 'react'
import Button from '../Button'
import Row from '../Row'
@@ -34,9 +35,11 @@ interface TokenButtonProps {
}
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>

View File

@@ -1,3 +1,4 @@
import { useLingui } from '@lingui/react'
import { Currency } from '@uniswap/sdk-core'
import useActiveWeb3React from 'lib/hooks/useActiveWeb3React'
import useCurrencyBalance from 'lib/hooks/useCurrencyBalance'
@@ -19,8 +20,8 @@ import {
} from 'react'
import AutoSizer from 'react-virtualized-auto-sizer'
import { areEqual, FixedSizeList, FixedSizeListProps } from 'react-window'
import invariant from 'tiny-invariant'
import { currencyId } from 'utils/currencyId'
import { formatCurrencyAmount } from 'utils/formatCurrencyAmount'
import { BaseButton } from '../Button'
import Column from '../Column'
@@ -69,6 +70,7 @@ interface BubbledEvent extends SyntheticEvent {
}
function TokenOption({ index, value, style }: TokenOptionProps) {
const { i18n } = useLingui()
const ref = useRef<HTMLButtonElement>(null)
// Annotate the event to be handled later instead of passing in handlers to avoid rerenders.
// This prevents token logos from reloading and flashing on the screen.
@@ -101,7 +103,7 @@ function TokenOption({ index, value, style }: TokenOptionProps) {
<ThemedText.Caption color="secondary">{value.name}</ThemedText.Caption>
</Column>
</Row>
{balance?.greaterThan(0) && balance?.toFixed(2)}
{balance?.greaterThan(0) && formatCurrencyAmount(balance, 2, i18n.locale)}
</Row>
</ThemedText.Body1>
</TokenButton>
@@ -137,68 +139,73 @@ const TokenOptions = forwardRef<TokenOptionsHandle, TokenOptionsProps>(function
ref
) {
const [focused, setFocused] = useState(false)
const [hover, setHover] = useState(-1)
useEffect(() => setHover(-1), [tokens])
const [hover, setHover] = useState<{ index: number; currency?: Currency }>({ index: -1 })
useEffect(() => {
setHover((hover) => {
const index = hover.currency ? tokens.indexOf(hover.currency) : -1
return { index, currency: tokens[index] }
})
}, [tokens])
const list = useRef<FixedSizeList>(null)
const [element, setElement] = useState<HTMLElement | null>(null)
const scrollTo = useCallback(
(index: number | undefined) => {
if (index === undefined) return
list.current?.scrollToItem(index)
if (focused) {
element?.querySelector<HTMLElement>(`[data-index='${index}']`)?.focus()
}
setHover({ index, currency: tokens[index] })
},
[element, focused, tokens]
)
const onKeyDown = useCallback(
(e: KeyboardEvent) => {
if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {
if (e.key === 'ArrowDown' && hover < tokens.length - 1) {
scrollTo(hover + 1)
} else if (e.key === 'ArrowUp' && hover > 0) {
scrollTo(hover - 1)
} else if (e.key === 'ArrowUp' && hover === -1) {
if (e.key === 'ArrowDown' && hover.index < tokens.length - 1) {
scrollTo(hover.index + 1)
} else if (e.key === 'ArrowUp' && hover.index > 0) {
scrollTo(hover.index - 1)
} else if (e.key === 'ArrowUp' && hover.index === -1) {
scrollTo(tokens.length - 1)
}
e.preventDefault()
}
if (e.key === 'Enter' && hover) {
onSelect(tokens[hover])
}
function scrollTo(index: number) {
list.current?.scrollToItem(index)
setHover(index)
if (e.key === 'Enter' && hover.index !== -1) {
onSelect(tokens[hover.index])
}
},
[hover, onSelect, tokens]
[hover.index, onSelect, scrollTo, tokens]
)
const blur = useCallback(() => setHover(-1), [])
const blur = useCallback(() => setHover({ index: -1 }), [])
useImperativeHandle(ref, () => ({ onKeyDown, blur }), [blur, onKeyDown])
const onClick = useCallback(({ token }: BubbledEvent) => token && onSelect(token), [onSelect])
const onFocus = useCallback(({ index }: BubbledEvent) => {
if (index !== undefined) {
setHover(index)
const onFocus = useCallback(
({ index }: BubbledEvent) => {
setFocused(true)
}
}, [])
const onBlur = useCallback(() => setFocused(false), [])
const onMouseMove = useCallback(
({ index, ref }: BubbledEvent) => {
if (index !== undefined) {
setHover(index)
if (focused) {
ref?.focus()
}
}
scrollTo(index)
},
[focused]
[scrollTo]
)
const onBlur = useCallback(() => setFocused(false), [])
const onMouseMove = useCallback(({ index }: BubbledEvent) => scrollTo(index), [scrollTo])
const [element, setElement] = useState<HTMLElement | null>(null)
const scrollbar = useScrollbar(element, { padded: true })
const onHover = useRef<HTMLDivElement>(null)
// use native onscroll handler to capture Safari's bouncy overscroll effect
useNativeEvent(element, 'scroll', (e) => {
invariant(element)
if (onHover.current) {
// must be set synchronously to avoid jank (avoiding useState)
onHover.current.style.marginTop = `${-element.scrollTop}px`
}
})
useNativeEvent(
element,
'scroll',
useCallback(() => {
if (element && onHover.current) {
// must be set synchronously to avoid jank (avoiding useState)
onHover.current.style.marginTop = `${-element.scrollTop}px`
}
}, [element])
)
return (
<Column
@@ -212,11 +219,11 @@ const TokenOptions = forwardRef<TokenOptionsHandle, TokenOptionsProps>(function
style={{ overflow: 'hidden' }}
>
{/* OnHover is a workaround to Safari's incorrect (overflow: overlay) implementation */}
<OnHover hover={hover} ref={onHover} />
<OnHover hover={hover.index} ref={onHover} />
<AutoSizer disableWidth>
{({ height }) => (
<TokenList
hover={hover}
hover={hover.index}
height={height}
width="100%"
itemCount={tokens.length}

View File

@@ -1,40 +1,45 @@
import { Placement } from '@popperjs/core'
import useHasFocus from 'lib/hooks/useHasFocus'
import useHasHover from 'lib/hooks/useHasHover'
import { HelpCircle, Icon } from 'lib/icons'
import styled from 'lib/theme'
import { ReactNode, useState } from 'react'
import { ComponentProps, ReactNode, useRef } from 'react'
import { IconButton } from './Button'
import Popover from './Popover'
export function useTooltip(tooltip: Node | null | undefined): boolean {
const hover = useHasHover(tooltip)
const focus = useHasFocus(tooltip)
return hover || focus
}
const IconTooltip = styled(IconButton)`
:hover {
cursor: help;
}
cursor: help;
`
interface TooltipInterface {
interface TooltipProps {
icon?: Icon
iconProps?: ComponentProps<Icon>
children: ReactNode
placement: Placement
placement?: Placement
offset?: number
contained?: true
}
export default function Tooltip({
icon: Icon = HelpCircle,
iconProps,
children,
placement = 'auto',
offset,
contained,
}: TooltipInterface) {
const [show, setShow] = useState(false)
}: TooltipProps) {
const tooltip = useRef<HTMLDivElement>(null)
const showTooltip = useTooltip(tooltip.current)
return (
<Popover content={children} show={show} placement={placement} contained={contained}>
<IconTooltip
onMouseEnter={() => setShow(true)}
onMouseLeave={() => setShow(false)}
onFocus={() => setShow(true)}
onBlur={() => setShow(false)}
icon={Icon}
/>
<Popover content={children} show={showTooltip} placement={placement} offset={offset} contained={contained}>
<IconTooltip icon={Icon} iconProps={iconProps} ref={tooltip} />
</Popover>
)
}

View File

@@ -1,15 +1,25 @@
import { CreditCard } from 'lib/icons'
import { ThemedText } from 'lib/theme'
import { Trans } from '@lingui/macro'
import { Wallet as WalletIcon } from 'lib/icons'
import styled, { ThemedText } from 'lib/theme'
import Row from './Row'
export default function Wallet({ disabled }: { disabled?: boolean }) {
interface WalletProps {
disabled?: boolean
onClick?: () => void
}
const ClickableRow = styled(Row)<{ onClick?: unknown }>`
cursor: ${({ onClick }) => onClick && 'pointer'};
`
export default function Wallet({ disabled, onClick }: WalletProps) {
return disabled ? (
<ThemedText.Caption color="secondary">
<Row gap={0.25}>
<CreditCard />
Connect wallet to swap
</Row>
<ClickableRow gap={0.5} onClick={onClick}>
<WalletIcon />
<Trans>Connect your wallet</Trans>
</ClickableRow>
</ThemedText.Caption>
) : null
}

View File

@@ -1,11 +1,11 @@
import { initializeConnector, Web3ReactHooks } from '@web3-react/core'
import { EIP1193 } from '@web3-react/eip1193'
import { Actions, Connector, Provider as EthProvider } from '@web3-react/types'
import { Url } from '@web3-react/url'
import { SetStateAction } from 'jotai'
import { RESET, useUpdateAtom } from 'jotai/utils'
import { injectedAtom, urlAtom } from 'lib/state/web3'
import { ReactNode, useEffect } from 'react'
import { initializeConnector, Web3ReactHooks } from 'widgets-web3-react/core'
import { EIP1193 } from 'widgets-web3-react/eip1193'
import { Actions, Connector, Provider as EthProvider } from 'widgets-web3-react/types'
import { Url } from 'widgets-web3-react/url'
interface Web3ProviderProps {
jsonRpcEndpoint?: string

View File

@@ -1,3 +1,4 @@
import { Provider as EthProvider } from '@web3-react/types'
import { DEFAULT_LOCALE, SupportedLocale } from 'constants/locales'
import { Provider as AtomProvider } from 'jotai'
import { TransactionsUpdater } from 'lib/hooks/transactions'
@@ -6,26 +7,14 @@ import { UNMOUNTING } from 'lib/hooks/useUnmount'
import { Provider as I18nProvider } from 'lib/i18n'
import { MulticallUpdater, store as multicallStore } from 'lib/state/multicall'
import styled, { keyframes, Theme, ThemeProvider } from 'lib/theme'
import { PropsWithChildren, StrictMode, useRef } from 'react'
import { PropsWithChildren, StrictMode, useState } from 'react'
import { Provider as ReduxProvider } from 'react-redux'
import { Provider as EthProvider } from 'widgets-web3-react/types'
import { Provider as DialogProvider } from './Dialog'
import { Modal, Provider as DialogProvider } from './Dialog'
import ErrorBoundary, { ErrorHandler } from './Error/ErrorBoundary'
import WidgetPropValidator from './Error/WidgetsPropsValidator'
import Web3Provider from './Web3Provider'
const slideDown = keyframes`
to {
top: calc(100% - 0.25em);
}
`
const slideUp = keyframes`
from {
top: calc(100% - 0.25em);
}
`
const WidgetWrapper = styled.div<{ width?: number | string }>`
-moz-osx-font-smoothing: grayscale;
-webkit-font-smoothing: antialiased;
@@ -39,17 +28,12 @@ const WidgetWrapper = styled.div<{ width?: number | string }>`
font-size: 16px;
font-smooth: always;
font-variant: none;
height: 376px;
height: 348px;
min-width: 300px;
overflow-y: hidden;
padding: 0.25em;
position: relative;
width: ${({ width }) => width && (isNaN(Number(width)) ? width : `${width}px`)};
@supports (overflow: clip) {
overflow-y: clip;
}
* {
box-sizing: border-box;
font-family: ${({ theme }) => theme.fontFamily};
@@ -59,12 +43,37 @@ const WidgetWrapper = styled.div<{ width?: number | string }>`
font-family: ${({ theme }) => theme.fontFamilyVariable};
}
}
`
.dialog {
const slideDown = keyframes`
to {
transform: translateY(calc(100% - 0.25em));
}
`
const slideUp = keyframes`
from {
transform: translateY(calc(100% - 0.25em));
}
`
const DialogWrapper = styled.div`
height: calc(100% - 0.5em);
left: 0;
margin: 0.25em;
overflow: hidden;
position: absolute;
top: 0;
width: calc(100% - 0.5em);
@supports (overflow: clip) {
overflow: clip;
}
${Modal} {
animation: ${slideUp} 0.25s ease-in-out;
}
.dialog.${UNMOUNTING} {
${Modal}.${UNMOUNTING} {
animation: ${slideDown} 0.25s ease-in-out;
}
`
@@ -98,18 +107,19 @@ export default function Widget(props: PropsWithChildren<WidgetProps>) {
provider,
jsonRpcEndpoint,
width = 360,
dialog,
dialog: userDialog,
className,
onError,
} = props
const wrapper = useRef<HTMLDivElement>(null)
const [dialog, setDialog] = useState<HTMLDivElement | null>(null)
return (
<StrictMode>
<I18nProvider locale={locale}>
<ThemeProvider theme={theme}>
<WidgetWrapper width={width} className={className} ref={wrapper}>
<DialogProvider value={dialog || wrapper.current}>
<WidgetWrapper width={width} className={className}>
<DialogWrapper ref={setDialog} />
<DialogProvider value={userDialog || dialog}>
<ErrorBoundary onError={onError}>
<WidgetPropValidator {...props}>
<ReduxProvider store={multicallStore}>

View File

@@ -1,12 +1,12 @@
import { INFURA_NETWORK_URLS } from 'constants/chainInfo'
import { initializeConnector } from '@web3-react/core'
import { MetaMask } from '@web3-react/metamask'
import { SupportedChainId } from 'constants/chains'
import { INFURA_NETWORK_URLS } from 'constants/infura'
import { DEFAULT_LOCALE, SUPPORTED_LOCALES } from 'constants/locales'
import Widget from 'lib/components/Widget'
import { darkTheme, defaultTheme, lightTheme } from 'lib/theme'
import { ReactNode, useEffect, useMemo } from 'react'
import { useSelect, useValue } from 'react-cosmos/fixture'
import { initializeConnector } from 'widgets-web3-react/core'
import { MetaMask } from 'widgets-web3-react/metamask'
export const [metaMask] = initializeConnector<MetaMask>((actions) => new MetaMask(actions))

8
src/lib/css/loading.ts Normal file
View File

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

View File

@@ -0,0 +1,98 @@
import { BigintIsh, CurrencyAmount, Token, TradeType } from '@uniswap/sdk-core'
import { AlphaRouter, AlphaRouterConfig, AlphaRouterParams, ChainId } from '@uniswap/smart-order-router'
import JSBI from 'jsbi'
import { GetQuoteResult } from 'state/routing/types'
import { transformSwapRouteToGetQuoteResult } from 'utils/transformSwapRouteToGetQuoteResult'
export const AUTO_ROUTER_SUPPORTED_CHAINS: ChainId[] = Object.values(ChainId) as number[]
async function getQuote(
{
type,
chainId,
tokenIn,
tokenOut,
amount: amountRaw,
}: {
type: 'exactIn' | 'exactOut'
chainId: ChainId
tokenIn: { address: string; chainId: number; decimals: number; symbol?: string }
tokenOut: { address: string; chainId: number; decimals: number; symbol?: string }
amount: BigintIsh
},
routerParams: AlphaRouterParams,
routerConfig: Partial<AlphaRouterConfig>
): Promise<{ data: GetQuoteResult; error?: unknown }> {
const router = new AlphaRouter(routerParams)
const currencyIn = new Token(tokenIn.chainId, tokenIn.address, tokenIn.decimals, tokenIn.symbol)
const currencyOut = new Token(tokenOut.chainId, tokenOut.address, tokenOut.decimals, tokenOut.symbol)
const baseCurrency = type === 'exactIn' ? currencyIn : currencyOut
const quoteCurrency = type === 'exactIn' ? currencyOut : currencyIn
const amount = CurrencyAmount.fromRawAmount(baseCurrency, JSBI.BigInt(amountRaw))
const swapRoute = await router.route(
amount,
quoteCurrency,
type === 'exactIn' ? TradeType.EXACT_INPUT : TradeType.EXACT_OUTPUT,
/*swapConfig=*/ undefined,
routerConfig
)
if (!swapRoute) throw new Error('Failed to generate client side quote')
return { data: transformSwapRouteToGetQuoteResult(type, amount, swapRoute) }
}
interface QuoteArguments {
tokenInAddress: string
tokenInChainId: ChainId
tokenInDecimals: number
tokenInSymbol?: string
tokenOutAddress: string
tokenOutChainId: ChainId
tokenOutDecimals: number
tokenOutSymbol?: string
amount: string
type: 'exactIn' | 'exactOut'
}
export async function getClientSideQuote(
{
tokenInAddress,
tokenInChainId,
tokenInDecimals,
tokenInSymbol,
tokenOutAddress,
tokenOutChainId,
tokenOutDecimals,
tokenOutSymbol,
amount,
type,
}: QuoteArguments,
routerParams: AlphaRouterParams,
routerConfig: Partial<AlphaRouterConfig>
) {
return getQuote(
{
type,
chainId: tokenInChainId,
tokenIn: {
address: tokenInAddress,
chainId: tokenInChainId,
decimals: tokenInDecimals,
symbol: tokenInSymbol,
},
tokenOut: {
address: tokenOutAddress,
chainId: tokenOutChainId,
decimals: tokenOutDecimals,
symbol: tokenOutSymbol,
},
amount,
},
routerParams,
routerConfig
)
}

View File

@@ -0,0 +1,155 @@
import { Protocol } from '@uniswap/router-sdk'
import { Currency, CurrencyAmount, TradeType } from '@uniswap/sdk-core'
import { ChainId } from '@uniswap/smart-order-router'
import useDebounce from 'hooks/useDebounce'
import { useStablecoinAmountFromFiatValue } from 'hooks/useUSDCPrice'
import { useEffect, useMemo, useState } from 'react'
import { GetQuoteResult, InterfaceTrade, TradeState } from 'state/routing/types'
import { computeRoutes, transformRoutesToTrade } from 'state/routing/utils'
import useActiveWeb3React from '../useActiveWeb3React'
import { getClientSideQuote } from './clientSideSmartOrderRouter'
import { useRoutingAPIArguments } from './useRoutingAPIArguments'
/**
* Reduces client-side latency by increasing the minimum percentage of the input token to use for each route in a split route while SOR is used client-side.
* Defaults are defined in https://github.com/Uniswap/smart-order-router/blob/309e6f6603984d3b5aef0733b0cfaf129c29f602/src/routers/alpha-router/config.ts#L83.
*/
const DistributionPercents: { [key: number]: number } = {
[ChainId.MAINNET]: 10,
[ChainId.OPTIMISM]: 10,
[ChainId.OPTIMISTIC_KOVAN]: 10,
[ChainId.ARBITRUM_ONE]: 25,
[ChainId.ARBITRUM_RINKEBY]: 25,
}
const DEFAULT_DISTRIBUTION_PERCENT = 10
function getConfig(chainId: ChainId | undefined) {
return {
// Limit to only V2 and V3.
protocols: [Protocol.V2, Protocol.V3],
distributionPercent: (chainId && DistributionPercents[chainId]) ?? DEFAULT_DISTRIBUTION_PERCENT,
}
}
export default function useClientSideSmartOrderRouterTrade<TTradeType extends TradeType>(
tradeType: TTradeType,
amountSpecified?: CurrencyAmount<Currency>,
otherCurrency?: Currency
): {
state: TradeState
trade: InterfaceTrade<Currency, Currency, TTradeType> | undefined
} {
// Debounce is used to prevent excessive requests to SOR, as it is data intensive.
// This helps provide a "syncing" state the UI can reference for loading animations.
const inputs = useMemo(() => [tradeType, amountSpecified, otherCurrency], [tradeType, amountSpecified, otherCurrency])
const debouncedInputs = useDebounce(inputs, 200)
const isDebouncing = inputs !== debouncedInputs
const chainId = amountSpecified?.currency.chainId
const { library } = useActiveWeb3React()
const [currencyIn, currencyOut]: [Currency | undefined, Currency | undefined] = useMemo(
() =>
tradeType === TradeType.EXACT_INPUT
? [amountSpecified?.currency, otherCurrency]
: [otherCurrency, amountSpecified?.currency],
[amountSpecified, otherCurrency, tradeType]
)
const queryArgs = useRoutingAPIArguments({
tokenIn: currencyIn,
tokenOut: currencyOut,
amount: amountSpecified,
tradeType,
useClientSideRouter: true,
})
const params = useMemo(() => chainId && library && { chainId, provider: library }, [chainId, library])
const [loading, setLoading] = useState(false)
const [{ data: quoteResult, error }, setResult] = useState<{
data?: GetQuoteResult
error?: unknown
}>({ error: undefined })
const config = useMemo(() => getConfig(chainId), [chainId])
// When arguments update, make a new call to SOR for updated quote
useEffect(() => {
setLoading(true)
if (isDebouncing) return
let stale = false
fetchQuote()
return () => {
stale = true
setLoading(false)
}
async function fetchQuote() {
if (queryArgs && params) {
let result
try {
result = await getClientSideQuote(queryArgs, params, config)
} catch {
result = { error: true }
}
if (!stale) {
setResult(result)
setLoading(false)
}
}
}
}, [queryArgs, params, config, isDebouncing])
const route = useMemo(
() => computeRoutes(currencyIn, currencyOut, tradeType, quoteResult),
[currencyIn, currencyOut, quoteResult, tradeType]
)
const gasUseEstimateUSD = useStablecoinAmountFromFiatValue(quoteResult?.gasUseEstimateUSD) ?? null
const trade = useMemo(() => {
if (route) {
try {
return route && transformRoutesToTrade(route, tradeType, gasUseEstimateUSD)
} catch (e: unknown) {
console.debug('transformRoutesToTrade failed: ', e)
}
}
return
}, [gasUseEstimateUSD, route, tradeType])
return useMemo(() => {
if (!currencyIn || !currencyOut) {
return { state: TradeState.INVALID, trade: undefined }
}
// Returns the last trade state while syncing/loading to avoid jank from clearing the last trade while loading.
if (isDebouncing) {
return { state: TradeState.SYNCING, trade }
} else if (loading) {
return { state: TradeState.LOADING, trade }
}
let otherAmount = undefined
if (quoteResult) {
switch (tradeType) {
case TradeType.EXACT_INPUT:
otherAmount = CurrencyAmount.fromRawAmount(currencyOut, quoteResult.quote)
break
case TradeType.EXACT_OUTPUT:
otherAmount = CurrencyAmount.fromRawAmount(currencyIn, quoteResult.quote)
break
}
}
if (error || !otherAmount || !route || route.length === 0 || !queryArgs) {
return { state: TradeState.NO_ROUTE_FOUND, trade: undefined }
}
if (trade) {
return { state: TradeState.VALID, trade }
}
return { state: TradeState.INVALID, trade: undefined }
}, [currencyIn, currencyOut, isDebouncing, loading, quoteResult, error, route, queryArgs, trade, tradeType])
}

View File

@@ -0,0 +1,41 @@
import { Currency, CurrencyAmount, TradeType } from '@uniswap/sdk-core'
import { useMemo } from 'react'
/**
* Returns query arguments for the Routing API query or undefined if the
* query should be skipped. Input arguments do not need to be memoized, as they will
* be destructured.
*/
export function useRoutingAPIArguments({
tokenIn,
tokenOut,
amount,
tradeType,
useClientSideRouter,
}: {
tokenIn: Currency | undefined
tokenOut: Currency | undefined
amount: CurrencyAmount<Currency> | undefined
tradeType: TradeType
useClientSideRouter: boolean
}) {
return useMemo(
() =>
!tokenIn || !tokenOut || !amount || tokenIn.equals(tokenOut)
? undefined
: {
amount: amount.quotient.toString(),
tokenInAddress: tokenIn.wrapped.address,
tokenInChainId: tokenIn.wrapped.chainId,
tokenInDecimals: tokenIn.wrapped.decimals,
tokenInSymbol: tokenIn.wrapped.symbol,
tokenOutAddress: tokenOut.wrapped.address,
tokenOutChainId: tokenOut.wrapped.chainId,
tokenOutDecimals: tokenOut.wrapped.decimals,
tokenOutSymbol: tokenOut.wrapped.symbol,
useClientSideRouter,
type: (tradeType === TradeType.EXACT_INPUT ? 'exactIn' : 'exactOut') as 'exactIn' | 'exactOut',
},
[amount, tokenIn, tokenOut, tradeType, useClientSideRouter]
)
}

View File

@@ -72,3 +72,8 @@ export function useSwapAmount(field: Field): [string | undefined, (amount: strin
)
return [value, updateAmount]
}
// check if any amount has been entered by user
export function useIsAmountPopulated() {
return Boolean(useAtomValue(amountAtom))
}

View File

@@ -0,0 +1,32 @@
import { Currency, CurrencyAmount, TradeType } from '@uniswap/sdk-core'
import { useClientSideV3Trade } from 'hooks/useClientSideV3Trade'
import { InterfaceTrade, TradeState } from 'state/routing/types'
import useClientSideSmartOrderRouterTrade from '../routing/useClientSideSmartOrderRouterTrade'
/**
* Returns the best v2+v3 trade for a desired swap.
* @param tradeType whether the swap is an exact in/out
* @param amountSpecified the exact amount to swap in/out
* @param otherCurrency the desired output/payment currency
*/
export function useBestTrade(
tradeType: TradeType,
amountSpecified?: CurrencyAmount<Currency>,
otherCurrency?: Currency
): {
state: TradeState
trade: InterfaceTrade<Currency, Currency, TradeType> | undefined
} {
const clientSORTrade = useClientSideSmartOrderRouterTrade(tradeType, amountSpecified, otherCurrency)
// Use a simple client side logic as backup if SOR is not available.
const useFallback = clientSORTrade.state === TradeState.NO_ROUTE_FOUND || clientSORTrade.state === TradeState.INVALID
const fallbackTrade = useClientSideV3Trade(
tradeType,
useFallback ? amountSpecified : undefined,
useFallback ? otherCurrency : undefined
)
return useFallback ? fallbackTrade : clientSORTrade
}

View File

@@ -3,6 +3,7 @@ import { BigNumber } from '@ethersproject/bignumber'
import { TransactionResponse } from '@ethersproject/providers'
import { Trans } from '@lingui/macro'
import { Percent } from '@uniswap/sdk-core'
import { FeeOptions } from '@uniswap/v3-sdk'
import useActiveWeb3React from 'hooks/useActiveWeb3React'
import useENS from 'hooks/useENS'
import { SignatureData } from 'hooks/useERC20Permit'
@@ -17,18 +18,40 @@ export enum SwapCallbackState {
VALID,
}
interface UseSwapCallbackReturns {
state: SwapCallbackState
callback: null | (() => Promise<TransactionResponse>)
error: ReactNode | null
}
interface UseSwapCallbackArgs {
trade: AnyTrade | undefined // trade to execute, required
allowedSlippage: Percent // in bips
recipientAddressOrName: string | null | undefined // the ENS name or address of the recipient of the trade, or null if swap should be returned to sender
signatureData: SignatureData | null | undefined
deadline: BigNumber | undefined
feeOptions?: FeeOptions
}
// 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(
trade: AnyTrade | undefined, // trade to execute, required
allowedSlippage: Percent, // in bips
recipientAddressOrName: string | null | undefined, // the ENS name or address of the recipient of the trade, or null if swap should be returned to sender
signatureData: SignatureData | null | undefined,
deadline: BigNumber | undefined
): { state: SwapCallbackState; callback: null | (() => Promise<TransactionResponse>); error: ReactNode | null } {
export function useSwapCallback({
trade,
allowedSlippage,
recipientAddressOrName,
signatureData,
deadline,
feeOptions,
}: UseSwapCallbackArgs): UseSwapCallbackReturns {
const { account, chainId, library } = useActiveWeb3React()
const swapCalls = useSwapCallArguments(trade, allowedSlippage, recipientAddressOrName, signatureData, deadline)
const swapCalls = useSwapCallArguments(
trade,
allowedSlippage,
recipientAddressOrName,
signatureData,
deadline,
feeOptions
)
const { callback } = useSendSwapTransaction(account, chainId, library, trade, swapCalls)
const { address: recipientAddress } = useENS(recipientAddressOrName)

View File

@@ -1,18 +1,18 @@
import { Trans } from '@lingui/macro'
import { Currency, CurrencyAmount, Percent, TradeType } from '@uniswap/sdk-core'
import useAutoSlippageTolerance from 'hooks/useAutoSlippageTolerance'
import { useClientSideV3Trade } from 'hooks/useClientSideV3Trade'
import { FeeOptions } from '@uniswap/v3-sdk'
import { atom } from 'jotai'
import { useAtomValue, useUpdateAtom } from 'jotai/utils'
import { useCurrencyBalances } from 'lib/hooks/useCurrencyBalance'
import { maxSlippageAtom } from 'lib/state/settings'
import { Field, swapAtom } from 'lib/state/swap'
import { feeOptionsAtom, Field, swapAtom } from 'lib/state/swap'
import tryParseCurrencyAmount from 'lib/utils/tryParseCurrencyAmount'
import { ReactNode, useEffect, useMemo } from 'react'
import { InterfaceTrade, TradeState } from 'state/routing/types'
import { isAddress } from '../../../utils'
import useActiveWeb3React from '../useActiveWeb3React'
import useAllowedSlippage from '../useAllowedSlippage'
import { useBestTrade } from './useBestTrade'
interface SwapInfo {
currencies: { [field in Field]?: Currency }
@@ -23,6 +23,7 @@ interface SwapInfo {
state: TradeState
}
allowedSlippage: Percent
feeOptions: FeeOptions | undefined
}
const BAD_RECIPIENT_ADDRESSES: { [address: string]: true } = {
@@ -42,6 +43,8 @@ function useComputeSwapInfo(): SwapInfo {
[Field.OUTPUT]: outputCurrency,
} = useAtomValue(swapAtom)
const feeOptions = useAtomValue(feeOptionsAtom)
const to = account
const relevantTokenBalances = useCurrencyBalances(
@@ -54,11 +57,11 @@ function useComputeSwapInfo(): SwapInfo {
() => tryParseCurrencyAmount(amount, (isExactIn ? inputCurrency : outputCurrency) ?? undefined),
[inputCurrency, isExactIn, outputCurrency, amount]
)
const parsedAmountIn = isExactIn ? parsedAmount : undefined
const parsedAmountOut = isExactIn ? undefined : parsedAmount
/**
* @TODO (ianlapham): eventually need a strategy for routing API here
*/
const trade = useClientSideV3Trade(
//@TODO(ianlapham): this would eventually be replaced with routing api logic.
const trade = useBestTrade(
isExactIn ? TradeType.EXACT_INPUT : TradeType.EXACT_OUTPUT,
parsedAmount,
(isExactIn ? outputCurrency : inputCurrency) ?? undefined
@@ -82,22 +85,13 @@ function useComputeSwapInfo(): SwapInfo {
const currencyAmounts = useMemo(
() => ({
[Field.INPUT]: trade.trade?.inputAmount,
[Field.OUTPUT]: trade.trade?.outputAmount,
[Field.INPUT]: parsedAmountIn || trade.trade?.inputAmount,
[Field.OUTPUT]: parsedAmountOut || trade.trade?.outputAmount,
}),
[trade.trade?.inputAmount, trade.trade?.outputAmount]
[parsedAmountIn, parsedAmountOut, trade.trade?.inputAmount, trade.trade?.outputAmount]
)
/*
* If user has enabled 'auto' slippage, use the default best slippage calculated
* based on the trade. If user has entered custom slippage, use that instead.
*/
const autoSlippageTolerance = useAutoSlippageTolerance(trade.trade)
const maxSlippage = useAtomValue(maxSlippageAtom)
const allowedSlippage = useMemo(
() => (maxSlippage === 'auto' ? autoSlippageTolerance : maxSlippage),
[autoSlippageTolerance, maxSlippage]
)
const allowedSlippage = useAllowedSlippage(trade.trade)
const inputError = useMemo(() => {
let inputError: ReactNode | undefined
@@ -141,8 +135,9 @@ function useComputeSwapInfo(): SwapInfo {
inputError,
trade,
allowedSlippage,
feeOptions,
}),
[currencies, currencyBalances, currencyAmounts, inputError, trade, allowedSlippage]
[currencies, currencyBalances, currencyAmounts, inputError, trade, allowedSlippage, feeOptions]
)
}
@@ -152,6 +147,7 @@ const swapInfoAtom = atom<SwapInfo>({
currencyAmounts: {},
trade: { state: TradeState.INVALID },
allowedSlippage: new Percent(0),
feeOptions: undefined,
})
export function SwapInfoUpdater() {

View File

@@ -0,0 +1,35 @@
import { Percent } from '@uniswap/sdk-core'
import useActiveWeb3React from 'hooks/useActiveWeb3React'
import { useUpdateAtom } from 'jotai/utils'
import { feeOptionsAtom } from 'lib/state/swap'
import { useEffect } from 'react'
interface FeeOptionsArgs {
convenienceFee?: number
convenienceFeeRecipient?: string | string | { [chainId: number]: string }
}
export default function useSyncConvenienceFee({ convenienceFee, convenienceFeeRecipient }: FeeOptionsArgs) {
const { chainId } = useActiveWeb3React()
const updateFeeOptions = useUpdateAtom(feeOptionsAtom)
useEffect(() => {
if (convenienceFee && convenienceFeeRecipient) {
if (typeof convenienceFeeRecipient === 'string') {
updateFeeOptions({
fee: new Percent(convenienceFee, 10_000),
recipient: convenienceFeeRecipient,
})
return
}
if (chainId && convenienceFeeRecipient[chainId]) {
updateFeeOptions({
fee: new Percent(convenienceFee, 10_000),
recipient: convenienceFeeRecipient[chainId],
})
return
}
}
updateFeeOptions(undefined)
}, [chainId, convenienceFee, convenienceFeeRecipient, updateFeeOptions])
}

View File

@@ -31,7 +31,7 @@ interface UseSwapDefaultsArgs {
defaultOutputAmount?: string
}
export default function useSwapDefaults({
export default function useSyncSwapDefaults({
defaultInputAddress,
defaultInputAmount,
defaultOutputAddress,

View File

@@ -1,6 +1,6 @@
import { Web3ReactHooks } from '@web3-react/core'
import { useAtomValue } from 'jotai/utils'
import { injectedAtom, urlAtom, Web3ReactState } from 'lib/state/web3'
import { Web3ReactHooks } from 'widgets-web3-react/core'
export function useActiveWeb3ReactState(): Web3ReactState {
const injected = useAtomValue(injectedAtom)

View File

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

View File

@@ -0,0 +1,16 @@
import { useCallback, useEffect, useState } from 'react'
export default function useHasFocus(node: Node | null | undefined): boolean {
const [hasFocus, setHasFocus] = useState(node?.contains(document.activeElement) ?? false)
const onFocus = useCallback(() => setHasFocus(true), [])
const onBlur = useCallback((e) => setHasFocus(node?.contains(e.relatedTarget) ?? false), [node])
useEffect(() => {
node?.addEventListener('focusin', onFocus)
node?.addEventListener('focusout', onBlur)
return () => {
node?.removeEventListener('focusin', onFocus)
node?.removeEventListener('focusout', onBlur)
}
}, [node, onFocus, onBlur])
return hasFocus
}

View File

@@ -0,0 +1,16 @@
import { useCallback, useEffect, useState } from 'react'
export default function useHasHover(node: Node | null | undefined): boolean {
const [hasHover, setHasHover] = useState(false)
const onMouseEnter = useCallback(() => setHasHover(true), [])
const onMouseLeave = useCallback((e) => setHasHover(false), [])
useEffect(() => {
node?.addEventListener('mouseenter', onMouseEnter)
node?.addEventListener('mouseleave', onMouseLeave)
return () => {
node?.removeEventListener('mouseenter', onMouseEnter)
node?.removeEventListener('mouseleave', onMouseLeave)
}
}, [node, onMouseEnter, onMouseLeave])
return hasHover
}

View File

@@ -1,11 +1,13 @@
import { useEffect } from 'react'
export default function useNativeEvent(
export default function useNativeEvent<K extends keyof HTMLElementEventMap>(
element: HTMLElement | null,
...eventListener: Parameters<HTMLElement['addEventListener']>
type: K,
listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any,
options?: boolean | AddEventListenerOptions | undefined
) {
useEffect(() => {
element?.addEventListener(...eventListener)
return () => element?.removeEventListener(...eventListener)
}, [element, eventListener])
element?.addEventListener(type, listener, options)
return () => element?.removeEventListener(type, listener, options)
}, [element, type, listener, options])
}

View File

@@ -1,5 +1,5 @@
import { css } from 'lib/theme'
import { useEffect, useMemo, useState } from 'react'
import { useCallback, useEffect, useMemo, useState } from 'react'
import useNativeEvent from './useNativeEvent'
@@ -52,7 +52,11 @@ export default function useScrollbar(element: HTMLElement | null, { padded = fal
useEffect(() => {
setOverflow(hasOverflow(element))
}, [element])
useNativeEvent(element, 'transitionend', () => setOverflow(hasOverflow(element)))
useNativeEvent(
element,
'transitionend',
useCallback(() => setOverflow(hasOverflow(element)), [element])
)
return useMemo(() => (overflow ? scrollbarCss(padded) : overflowCss), [overflow, padded])
function hasOverflow(element: HTMLElement | null) {

View File

@@ -1,7 +1,6 @@
import { Currency, CurrencyAmount, Token } from '@uniswap/sdk-core'
import { TokenInfo } from '@uniswap/token-lists'
import { useMemo } from 'react'
import { useTokenBalances } from 'state/wallet/hooks'
/** Sorts currency amounts (descending). */
function balanceComparator(a?: CurrencyAmount<Currency>, b?: CurrencyAmount<Currency>) {
@@ -15,8 +14,10 @@ function balanceComparator(a?: CurrencyAmount<Currency>, b?: CurrencyAmount<Curr
return 0
}
type TokenBalances = { [tokenAddress: string]: CurrencyAmount<Token> | undefined }
/** Sorts tokens by currency amount (descending), then symbol (ascending). */
export function tokenComparator(balances: ReturnType<typeof useTokenBalances>, a: Token, b: Token) {
export function tokenComparator(balances: TokenBalances, a: Token, b: Token) {
// Sorts by balances
const balanceComparison = balanceComparator(balances[a.address], balances[b.address])
if (balanceComparison !== 0) return balanceComparison

View File

@@ -1,22 +1,33 @@
import { BigNumber } from '@ethersproject/bignumber'
import { L2_CHAIN_IDS } from 'constants/chains'
import { L2_DEADLINE_FROM_NOW } from 'constants/misc'
import { DEFAULT_DEADLINE_FROM_NOW, L2_DEADLINE_FROM_NOW } from 'constants/misc'
import useCurrentBlockTimestamp from 'hooks/useCurrentBlockTimestamp'
import { useAtomValue } from 'jotai/utils'
import { useAtom } from 'jotai'
import { transactionTtlAtom } from 'lib/state/settings'
import { useMemo } from 'react'
import useActiveWeb3React from './useActiveWeb3React'
/** Returns the default transaction TTL for the chain, in minutes. */
export function useDefaultTransactionTtl(): number {
const { chainId } = useActiveWeb3React()
if (chainId && L2_CHAIN_IDS.includes(chainId)) return L2_DEADLINE_FROM_NOW / 60
return DEFAULT_DEADLINE_FROM_NOW / 60
}
/** Returns the user-inputted transaction TTL, in minutes. */
export function useTransactionTtl(): [number | undefined, (ttl?: number) => void] {
return useAtom(transactionTtlAtom)
}
// combines the block timestamp with the user setting to give the deadline that should be used for any submitted transaction
export default function useTransactionDeadline(): BigNumber | undefined {
const { chainId } = useActiveWeb3React()
const userDeadline = useAtomValue(transactionTtlAtom)
const [ttl] = useTransactionTtl()
const defaultTtl = useDefaultTransactionTtl()
const blockTimestamp = useCurrentBlockTimestamp()
return useMemo(() => {
if (blockTimestamp && chainId && L2_CHAIN_IDS.includes(chainId)) return blockTimestamp.add(L2_DEADLINE_FROM_NOW)
//@TODO(ianlapham): update this to be stored as seconds
if (blockTimestamp && userDeadline) return blockTimestamp.add(userDeadline * 60) // adjust for seconds
return undefined
}, [blockTimestamp, chainId, userDeadline])
if (!blockTimestamp) return undefined
return blockTimestamp.add((ttl || defaultTtl) /* in seconds */ * 60)
}, [blockTimestamp, defaultTtl, ttl])
}

View File

@@ -80,7 +80,8 @@ export async function dynamicActivate(locale: SupportedLocale) {
i18n.loadLocaleData(locale, { plurals: () => plurals[locale] })
// There are no default messages in production; instead, bundle the default to save a network request:
// see https://github.com/lingui/js-lingui/issues/388#issuecomment-497779030
const catalog = locale === DEFAULT_LOCALE ? DEFAULT_CATALOG : await import(`locales/${locale}`)
const catalog =
locale === DEFAULT_LOCALE ? DEFAULT_CATALOG : await import(`${process.env.REACT_APP_LOCALES}/${locale}`)
i18n.load(locale, catalog.messages)
i18n.activate(locale)
}

View File

@@ -1,16 +1,21 @@
/* eslint-disable no-restricted-imports */
import { ReactComponent as CheckIcon } from 'lib/assets/svg/check.svg'
import { ReactComponent as ExpandoIcon } from 'lib/assets/svg/expando.svg'
import { ReactComponent as LogoIcon } from 'lib/assets/svg/logo.svg'
import { ReactComponent as SpinnerIcon } from 'lib/assets/svg/spinner.svg'
import { ReactComponent as WalletIcon } from 'lib/assets/svg/wallet.svg'
import styled, { Color, css, keyframes } from 'lib/theme'
import { FunctionComponent, SVGProps } from 'react'
/* eslint-disable no-restricted-imports */
import { Icon as FeatherIcon } from 'react-feather'
import {
AlertTriangle as AlertTriangleIcon,
ArrowDown as ArrowDownIcon,
ArrowRight as ArrowRightIcon,
ArrowUp as ArrowUpIcon,
BarChart2 as BarChart2Icon,
CheckCircle as CheckCircleIcon,
ChevronDown as ChevronDownIcon,
Clock as ClockIcon,
CreditCard as CreditCardIcon,
ExternalLink as LinkIcon,
HelpCircle as HelpCircleIcon,
Info as InfoIcon,
@@ -18,12 +23,9 @@ import {
Slash as SlashIcon,
Trash2 as Trash2Icon,
X as XIcon,
XOctagon as XOctagonIcon,
} from 'react-feather'
import { ReactComponent as CheckIcon } from '../assets/svg/check.svg'
import { ReactComponent as ExpandoIcon } from '../assets/svg/expando.svg'
import { ReactComponent as LogoIcon } from '../assets/svg/logo.svg'
import { ReactComponent as SpinnerIcon } from '../assets/svg/spinner.svg'
/* eslint-enable no-restricted-imports */
type SVGIcon = FunctionComponent<SVGProps<SVGSVGElement>>
@@ -36,8 +38,6 @@ function icon(Icon: FeatherIcon | SVGIcon) {
`
}
export type Icon = ReturnType<typeof icon>
export const largeIconCss = css<{ iconSize: number }>`
display: flex;
@@ -50,11 +50,14 @@ export const largeIconCss = css<{ iconSize: number }>`
const LargeWrapper = styled.div<{ iconSize: number }>`
height: 1em;
width: ${({ iconSize }) => iconSize}em;
${largeIconCss}
`
export type Icon = ReturnType<typeof icon> | typeof LargeIcon
interface LargeIconProps {
icon: Icon
icon?: Icon
color?: Color
size?: number
className?: string
@@ -63,7 +66,7 @@ interface LargeIconProps {
export function LargeIcon({ icon: Icon, color, size = 1.2, className }: LargeIconProps) {
return (
<LargeWrapper color={color} iconSize={size} className={className}>
<Icon color={color} />
{Icon && <Icon color={color} />}
</LargeWrapper>
)
}
@@ -73,16 +76,18 @@ export const ArrowDown = icon(ArrowDownIcon)
export const ArrowRight = icon(ArrowRightIcon)
export const ArrowUp = icon(ArrowUpIcon)
export const CheckCircle = icon(CheckCircleIcon)
export const BarChart = icon(BarChart2Icon)
export const ChevronDown = icon(ChevronDownIcon)
export const Clock = icon(ClockIcon)
export const CreditCard = icon(CreditCardIcon)
export const HelpCircle = icon(HelpCircleIcon)
export const Info = icon(InfoIcon)
export const Link = icon(LinkIcon)
export const Settings = icon(SettingsIcon)
export const Slash = icon(SlashIcon)
export const Trash2 = icon(Trash2Icon)
export const Wallet = icon(WalletIcon)
export const X = icon(XIcon)
export const XOctagon = icon(XOctagonIcon)
export const Check = styled(icon(CheckIcon))`
circle {

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