Compare commits

...

87 Commits

Author SHA1 Message Date
Tina
7e1ab5fcd2 feat: Remove local routing setting (#7462) [hotfix for prod] (#7469)
* fix: change default tx deadline to 10m (#7451)

* fix: change deadline to 10m

* test: add unit tests

* fix: improve unit tests

* feat: Remove local routing setting (#7462)

* remove client side router preference

* update e2e test

* fix comment

---------

Co-authored-by: eddie <66155195+just-toby@users.noreply.github.com>
2023-10-13 17:09:14 -04:00
Tina
3f510aabcc fix: change default tx deadline to 10m (#7451) [hotfix for prod] (#7468)
fix: change default tx deadline to 10m (#7451)

* fix: change deadline to 10m

* test: add unit tests

* fix: improve unit tests

Co-authored-by: eddie <66155195+just-toby@users.noreply.github.com>
2023-10-13 15:46:02 -04:00
eddie
b29968d014 fix: hotfix update setting user.router_preference for analytics (#7459)
fix: delay setting user.router_preference until statsig and redux initialize
2023-10-12 14:18:04 -07:00
Thomas Thachil
a07556b87d chore(): update deeplink package names (#7449) 2023-10-11 13:13:37 -04:00
UL Service Account
5b551af25a ci: add global CODEOWNERS 2023-10-06 19:47:30 +00:00
UL Service Account
69f6ca2635 ci(t9n): download translations from crowdin 2023-10-06 19:47:30 +00:00
Jack Short
2c7381ff47 fix: removing scrollbar on swap with banner (#7434) 2023-10-06 15:33:04 -04:00
Jack Short
6e4746a7fe feat: uk disclaimer banner (#7428)
* feat: uk disclaimer banner

* bad merge with sitemap

* button

* cypress test

* intercept ordering

* comments

* sitemap was committed idk why

* font weights

* moving uk disclaimer

* removing trash
2023-10-06 14:00:07 -04:00
Kristie Huang
48379c66ce feat: [info] remove balance summaries from TDP (#7430) 2023-10-06 13:07:29 -04:00
eddie
1b7f0d11fd fix: override user pref in analytics (#7420) 2023-10-06 09:21:18 -07:00
Kristie Huang
db1d264ad3 fix: unhide native gas token from miniportfolio (#7374)
* fix: unhide native gas token from miniportfolio

* wip tests & gql types

* fix tests, default hide small balances

* pr review

* fix e2e hidden count
2023-10-06 12:11:44 -04:00
Connor McEwen
fd24cb890a fix: meta tag injector uses property, not name (#7431) 2023-10-06 11:46:39 -04:00
cartcrom
932c4482d2 feat: updated rate/routing tooltips (#7412)
* feat: updated routing tooltips

* refactor: gas price formatting

* fit: boolean rendering
2023-10-05 17:25:53 -04:00
Connor McEwen
2d8dac5c15 fix: merge issue (#7427)
* fix: merge issue

* update snapshots
2023-10-05 16:30:57 -04:00
Kristie Huang
0e3d188a9a feat: add feature flags settings overrides box (#7406)
* add feature flags settings overrides box

* cat

* useGate hook monstrosity

* pr changes

* exclude devflagsbox from code cov

* pr review

* mobile bottom bar

* nit

* initialize atom

* fix atom initialization

* remove comments

* fix devbox initialization

* nit remove diff
2023-10-05 15:48:02 -04:00
cartcrom
1be62f0bec feat: updated slippage ui (#7409)
* feat: updated slippage ui

* fix: update settings to also have period in max slippage string

* test: update e2e test search string
2023-10-05 15:34:23 -04:00
Connor McEwen
e6519a7dd1 feat: support redirects for a list of header paths (#7411)
* add country code to meta tag

* use blocked paths header

* proper types

* add test

* Update functions/components/metaTagInjector.ts

Co-authored-by: Zach Pomerantz <zzmp@uniswap.org>

* Update functions/components/metaTagInjector.ts

Co-authored-by: Zach Pomerantz <zzmp@uniswap.org>

* Update src/pages/App.tsx

Co-authored-by: Zach Pomerantz <zzmp@uniswap.org>

* pr suggestions

* skip failing e2e

* revert test change

* take file from main

---------

Co-authored-by: Zach Pomerantz <zzmp@uniswap.org>
2023-10-05 15:25:45 -04:00
eddie
3ced65b8a4 feat: add sitemap for app.uniswap.org (#7408)
* feat: add sitemap for app.uniswap.org

* feat: script to update lastmod

* fix: deps and snapshots

* fix: use xml2js

* fix: improve test and sitemap
2023-10-05 12:19:58 -07:00
eddie
bab8506919 fix: dont crash on invalid tokenId (#7410) 2023-10-05 12:12:34 -07:00
eddie
4a79280edc feat: allow manual test runs (#7415) 2023-10-05 12:12:21 -07:00
eddie
53f0ca9b7e fix: disable UniswapX opt-out in e2e tests (#7423) 2023-10-05 11:54:24 -07:00
eddie
0381200fec fix: ignore large slices in immutable check (#7425) 2023-10-05 11:40:52 -07:00
Matthew Spector
040ebb5475 fix: Remove Minus Sign for FOT Display (#7419) 2023-10-05 11:33:28 -07:00
Charles Bachmeier
0752314d87 fix: click on test row directly (#7424) 2023-10-05 11:08:39 -07:00
Charles Bachmeier
9db5fd104a fix: use in house token for low volume test (#7414)
* fix: use discontinued project for low volume test

* use token I created

* update comment

* no info available

* remove socials

* update comment

* checksummed address
2023-10-04 18:18:57 -07:00
cartcrom
b9db195017 feat: gas costs ui updates (#7405)
* feat: gas costs ui updates

* lint

* test: update snapshots

* test: update other snapshots
2023-10-04 16:08:00 -04:00
eddie
b6bdbcf587 test: mock http requests in jest (#7394)
* test: try no coverage for unit tests

* test: onlychanged

* test: try parallel

* fix: no http in unit tests

* fix: deduplicate

* fix: nock only

* fix: maxworkers 2

* fix: remove nock

* fix: restore nock

* fix: comment

* fix: try again with jest-offline

* fix: remove jest-offline

* test: try 2 again

* fix: 100%
2023-10-03 14:37:27 -07:00
eddie
cc325b2fbe fix: no stale trade when otherCurrency is missing (#7403) 2023-10-03 14:33:48 -07:00
Jack Short
2694379c97 chore: currency percentages (#7358)
* formatPercent

* hook deps

* price chart

* price chart formatting

* bug bash findings

* Update src/utils/formatNumbers.ts

Co-authored-by: Zach Pomerantz <zzmp@uniswap.org>

* fixing merge errors

* unit tests

* special cases

---------

Co-authored-by: Zach Pomerantz <zzmp@uniswap.org>
2023-10-03 13:29:36 -07:00
cartcrom
82aaf0784a refactor: swap line items and tooltips decomposition (#7390)
* refactor: swap line items and tooltips decomposition

* test: test line items directly

* refactor: added tooltip prop

* refactor: preview trade logic

* fix: percentage color

* lint

* fix: exchange rate alignment

* fix: initial pr comments

* test: fix snapshots

* refactor: var naming

* fix: uneeded dep array var

* refactor: small nit
2023-10-03 15:22:07 -04:00
Jordan Frankfurt
55a509cad8 chore: update @web3-react/* (#7398)
* chore: update @web3-react/walletconnect-v2

* update remaining packages and their patches
2023-10-03 13:46:02 -05:00
Tina
463dd6fdfb feat: Move UniswapX signature expiry back to deadline (#7402)
startTime -> deadline
2023-10-03 14:29:03 -04:00
Kristie Huang
3ad4fb6846 feat: change address screening service to Uniswap /screen API (#7339)
* change fetch url to uniswap api

* clean code + write tests

* nit updates

* nit-rename test

---------

Co-authored-by: Kristie Huang <kristie.huang@uniswap.org>
2023-10-02 13:04:30 -04:00
gnewfield
1c76277c46 feat: polish blocked, nonexistent NFT collection page (#7373)
* add pages for nonexistent and blocked NFT collections

* add padding

* fix themed text import

* update design and revise based on pr comments
2023-10-02 13:02:28 -04:00
Charles Bachmeier
f90f81b3d9 feat: add error message to wallet connect fail event (#7387) 2023-10-02 09:56:35 -07:00
Charles Bachmeier
81accd1864 feat: [info] Add Liquidity and Swap buttons on PDP (#7382)
* feat: setup initial pool details page and route

* add pool data query and call on enw page

* make query dynamic to url chainId

* Get and display Header info

* add token symbols

* split header into its own file

* add helper function to not default to eth chain

* add helper function tests

* add header component tests

* add mocked test for PDP

* use valid values

* allow unsupported BE chains supported by thegraph

* typecheck

* remove useless row

* no longer needed child

* use first and last child

* move mock consts to their own file

* skele linear task

* return null

* descriptiive pool not found bool

* modify correct logo container

* update snapshots

* instantiate all chain apollo clients

* added snapshot test

* merge main and update snapshots

* Update src/pages/PoolDetails/PoolDetailsHeader.tsx

Co-authored-by: Nate Wienert <natewienert@gmail.com>

* type feeTier

* setup init stats component

* correctly query pool data for t24, t48, and tWeek timestamps

* add comments

* sanitize pool data and update tests

* correct test data

* add todo

* lint

* show correct data

* remove logs

* use formatter

* showing colored bars

* styled graph

* get muted color

* refactor: move getColor to src

* refactor useColor to use getColor function

* remove consts

* refactor files

* 1st class var support courtesy of carter

* remove logging and adds comments

* mobile styling

* move Stats to its own file

* add test cases

* add test file

* update padding

* remove old test file

* respond to feedback

* right column wrapper

* add non-functional pdp buttons

* update tests

* add button functionality

* working tokenId for position

* split buttons in their own file

* add tests

* reduce screenshots

---------

Co-authored-by: Nate Wienert <natewienert@gmail.com>
2023-10-02 09:56:17 -07:00
eddie
524ce49fcb feat: dynamic defaultCurrencyCode for moonpay (#7383) 2023-10-02 09:48:55 -07:00
Kristie Huang
cbec108172 feat: add chains dynamicconfig for feature flags (#7389)
* feat: add feature flags dynamic config for chains

* fix

* add better chainid error checking

* quiet linter

* refactor & should be quick_route_chains only
2023-09-29 14:53:01 -04:00
eddie
3a4dc91e49 fix: wrap/unwrap activity parsing (#7384)
* fix: add unit test

* fix: re-use swap parse logic
2023-09-29 10:26:09 -07:00
Jack Short
af80079957 feat: remove buy button and landing terminology for uk (#7386)
* feat: remove buy button and landing terminology for uk

* removing tarballs

* mocked setup

* setting compliance to gb

* turning back on defaults

* cache for user

* moving to hook and grid sizing

* fixing tests

* comments

* landinage page cards

* cypress test

* removing extra store
2023-09-29 12:32:05 -04:00
Tina
c7a8e9e5a7 feat: Quick routes (#7348)
* wip, added PreviewTrade and now amending request arg type

* updates

* update logic to progress to swap review screen

* add token tax info to preview trades

* add loading component

* add feature flag and fix analytics and perf stuff

* update debounce amount

* add latencyMs measure

* change types

* add inline comments

* actually pass in feature flags

* dep array

* fix snapshot and unit tests

* fix unit tests

* update font color for loading text

* remove all chains feature flag

* remove from feature flag modal

* dont flicker review modal when allowance is loading

* remove comment

* add snapshot tests

* triple equals

* add comment

* change cast
2023-09-29 12:20:10 -04:00
eddie
e6362212c6 feat: uniswapx deadline (#7376)
* feat: uniswapX time-to-sign

* fix: animation timing

* fix: bug

* fix: improve props and remove memo
2023-09-28 10:52:35 -07:00
Charles Bachmeier
d63bdf1887 feat: [info] Add Stats Section to PDP (#7353)
* feat: setup initial pool details page and route

* add pool data query and call on enw page

* make query dynamic to url chainId

* Get and display Header info

* add token symbols

* split header into its own file

* add helper function to not default to eth chain

* add helper function tests

* add header component tests

* add mocked test for PDP

* use valid values

* allow unsupported BE chains supported by thegraph

* typecheck

* remove useless row

* no longer needed child

* use first and last child

* move mock consts to their own file

* skele linear task

* return null

* descriptiive pool not found bool

* modify correct logo container

* update snapshots

* instantiate all chain apollo clients

* added snapshot test

* merge main and update snapshots

* Update src/pages/PoolDetails/PoolDetailsHeader.tsx

Co-authored-by: Nate Wienert <natewienert@gmail.com>

* type feeTier

* setup init stats component

* correctly query pool data for t24, t48, and tWeek timestamps

* add comments

* sanitize pool data and update tests

* correct test data

* add todo

* lint

* show correct data

* remove logs

* use formatter

* showing colored bars

* styled graph

* get muted color

* refactor: move getColor to src

* refactor useColor to use getColor function

* remove consts

* refactor files

* 1st class var support courtesy of carter

* remove logging and adds comments

* mobile styling

* move Stats to its own file

* add test cases

* add test file

* update padding

* remove old test file

* respond to feedback

* right column wrapper

* update tests

---------

Co-authored-by: Nate Wienert <natewienert@gmail.com>
2023-09-28 09:27:29 -07:00
eddie
3bb55c6b5d fix: hide price when no amount input (#7379)
* fix: hide price when no amount input

* fix: snapshots
2023-09-27 10:47:16 -07:00
Kristie Huang
71212f7e32 fix: use sentence case for text (#7375) 2023-09-27 12:46:09 -04:00
eddie
731ff4a485 feat: user property git commit hash (#7378) 2023-09-26 13:37:08 -07:00
eddie
519ba8963a test: unit tests for parseRemote (#7359) 2023-09-26 13:36:56 -07:00
Jack Short
ec784ccb36 chore: updating input currency panel to handle comma separator (#7336) 2023-09-26 12:33:15 -05:00
Brendan Wong
20d8404717 fix: update polygon branding (#7190)
* update polygon branding

* Update matic-token-icon.svg
2023-09-25 12:11:12 -04:00
Jordan Frankfurt
809841df0a feat: new app provider w/ fallback behavior (#7205)
* feat: new app provider w/ fallback behavior

* progress

* update useContractX signer params

* Revert "update useContractX signer params"

This reverts commit 386d1580df.

* extend jsonrpcprovider

* add mainnet quicknode example, use old staticJsonRpc extension

* add tests

* unit testing

* fixes to tests/tsc

* Update src/state/routing/gas.ts

Co-authored-by: Zach Pomerantz <zzmp@uniswap.org>

* pr review

* e2e tests should only talk to the chain via connected wallet

* Revert "e2e tests should only talk to the chain via connected wallet"

This reverts commit 0ce76eb7e4.

* add charlie's null nit

* fix e2e

* add feature flag

* Update cypress/support/setupTests.ts

Co-authored-by: Zach Pomerantz <zzmp@uniswap.org>

* pr review

* pr feedback

* fix tests

* add generic send test

* fix merge error

* add a failure rate calculation and inline comments on scoring algo w/ an example

* fix sort test

* cleaner provider creation

* simplify sort

---------

Co-authored-by: Zach Pomerantz <zzmp@uniswap.org>
2023-09-22 14:05:27 -05:00
Tina
2dc5a6efb4 fix: Remove e2e test (#7369)
* remove now untrue test

* add test about missing stats
2023-09-22 11:35:54 -07:00
eddie
7cd72a706d feat: show usd price with no input amount (#7328)
* feat: show usd price with no input amount

* fix: snapshots

* fix: make tokens undefined only - no null

* fix: readability

* fix: syntax improvements
2023-09-22 10:31:23 -07:00
Kristie Huang
4f8956f79a fix: should show slippage/deadline on LP flow settings (#7367)
* fix: should show slippage/deadline on LP flow settings

* write unit tests & update

---------

Co-authored-by: Kristie Huang <kristie.huang@uniswap.org>
2023-09-22 13:12:32 -04:00
Charles Bachmeier
beef7f2d86 feat: Condense color extraction logic and improve fallback (#7347)
* refactor: move getColor to src

* refactor useColor to use getColor function

* remove consts

* refactor files

* clean up color convert fn

* move getColor test and import test images

* hardcode array buffers for images

* add invalid png
2023-09-22 10:09:05 -07:00
Zach Pomerantz
b667662b49 fix: lazy load nft profile (#7361) 2023-09-22 10:08:22 -07:00
Zach Pomerantz
ed87df6269 feat: account suspense (#7337)
* feat: eagerly connect outside of react lifecycle

* test: reflect selected wallet in localStorage

* test: spy only on portfolio balances

* feat: connectionReady

* feat: connecting state

* feat: leave space for address

* fix tests

* better meta

* fix

* fix wallet change

* add interactivity earlier

* add validation

* update localstorage key in cypress setup

* even less thrash

* load per account

* simplify, hopefully

* explanatory

* inf render

* whoopsie

* ordering
2023-09-22 09:57:35 -07:00
Charles Bachmeier
622c72d4a8 feat: [info] Migrate Historical Pool Data Queries (#7310)
* correctly query pool data for t24, t48, and tWeek timestamps

* add comments

* sanitize pool data and update tests

* correct test data

* add todo

* lint

* remove logs

* 1st class var support courtesy of carter

* remove logging and adds comments
2023-09-22 09:47:25 -07:00
eddie
df6c44d2c4 fix: use Date as a performance tracker fallback (#7349) 2023-09-22 09:40:25 -07:00
eddie
59e7a2867a fix: ui fixes on the add liquidity flow (#7352) 2023-09-22 09:40:10 -07:00
eddie
0a31428d7a feat: uniswapX opt-out update (#7368) 2023-09-22 09:38:47 -07:00
Zach Pomerantz
fbc7e64032 chore: prohibit barrels (#7362)
* chore: prohibit barrels

* lint

* lint

* more lint

* add motivation to eslint message
2023-09-21 19:38:17 -07:00
eddie
4a8fb760d2 fix: show common bases on mobile (#7365)
* fix: show common bases on mobile

* test: add unit tests
2023-09-21 13:26:59 -07:00
eddie
15c510b742 fix: reset LDO approvals (#7345)
* fix: reset LDO approvals

* fix: constant file, better name

* Update src/components/swap/constants.ts

Co-authored-by: Charles Bachmeier <charles@bachmeier.io>

---------

Co-authored-by: Charles Bachmeier <charles@bachmeier.io>
2023-09-21 13:03:25 -07:00
eddie
e81b0a4d1f test: unit tests for activity tab createGroups, and update cloud test snapshots (#7364)
* test: unit tests for activity tab createGroups

* fix: update cloud test snapshots
2023-09-21 12:03:10 -07:00
Zach Pomerantz
b9fc65ec9a feat: lazy-load safe only if iframed (#7360) 2023-09-21 11:31:38 -07:00
Zach Pomerantz
d73c368ee4 fix: defer popper style recalc until use (#7330) 2023-09-21 11:29:43 -07:00
Zach Pomerantz
5e1c430657 build: improve tree-shaking (#7325)
* build: improve tree-shaking

* dedup terser
2023-09-21 11:29:22 -07:00
eddie
d4f19e42f8 feat: log chain changed (#7350) 2023-09-20 14:58:28 -07:00
cartcrom
60593df077 refactor: price chart organization (#7304)
* refactor: implement chart model type

* refactor: move PriceChart component into charts folder

* refactor: relocate pricechart test file

* lint

* fix: pr comments

* fix: use formatter hook in price chart file
2023-09-20 15:58:41 -04:00
Nate Wienert
aeef2c2356 fix: Fix visual issues in Spore (#7240)
* feat: make meta theme-color adapt to new spore background colors

* fix: make glow behind swap modal use a blur strategy rather than box shadow for a more squared glow

* test: grainy bg

* fix: make pool liquidity add input focus border same as swap

* remove svg grain
2023-09-20 09:44:25 -10:00
Zach Pomerantz
54880d201a feat: eagerly connect outside of react lifecycle (#7334)
* feat: eagerly connect outside of react lifecycle

* test: reflect selected wallet in localStorage

* test: spy only on portfolio balances
2023-09-20 12:31:54 -07:00
Zach Pomerantz
0f6581bf47 fix: preconnect (#7340) 2023-09-20 12:30:20 -07:00
Zach Pomerantz
37c3330897 fix: wait to fetch lists until rehydration (#7332) 2023-09-20 09:40:57 -07:00
Zach Pomerantz
7a981923f6 fix: do not re-init active locale (#7329) 2023-09-20 09:32:50 -07:00
Brendan Wong
9672c2db9a fix: remove settings button from lp page (#7105)
remove settings button from lp page
2023-09-19 14:16:46 -07:00
Charles Bachmeier
13f57d8d73 feat: block dynamic link previews for blocked collections (#7344)
* feat: block link previews for blocked collections

* update collection test

* single invalid

* move blocklist to its own const file

* rename file to blocklist
2023-09-19 11:07:07 -07:00
Charles Bachmeier
43f4d0f1b0 chore: block requested collection (#7342) 2023-09-19 08:06:58 -07:00
Zach Pomerantz
19c83c92ab test: spy only on portfolio balances (#7335) 2023-09-19 07:59:21 -07:00
Jack Short
91c2013522 chore: only exposing useFormatter (#7308) 2023-09-18 20:21:21 -04:00
eddie
cf09e80934 fix: fetch balances when opening token selector in LP page (#7321)
* fix: fetch balances when opening token selector in LP page

* fix: move PrefetchBalancesWrapper
2023-09-18 14:49:39 -07:00
Zach Pomerantz
ad9879b4f9 fix: avoid scrollTo on pageload (#7323) 2023-09-18 13:09:01 -07:00
Zach Pomerantz
c528c6169e fix: block number memoization (#7331) 2023-09-18 13:08:51 -07:00
Zach Pomerantz
33c93b5ded fix: spurious Swap re-renders (#7333) 2023-09-18 13:08:37 -07:00
Charles Bachmeier
5ba046f111 feat: remove overlay cutoff (#7322) 2023-09-18 11:59:01 -07:00
Callil Capuozzo
5414a7c7ef fix: [Spore] polish search (#7297)
* Polish search bar styles

* fix hover state

* Change background to surface1

* update tests

* Update styles
2023-09-16 10:22:38 -04:00
Zach Pomerantz
22fd0cc7bb build: fix sourcemap warnings (#7318) 2023-09-15 19:41:38 -07:00
Kristie Huang
784fbfe7b1 test: add remove-liquidity interface tests (#7309)
* fix: duplicate or single-token remove-liquidity routes should show error page

* use maxUint256 for nonexistent pool

* move tests back to rem-liq

* rename pooled token id

* nit: use uni address from sdk core

* nit: use maxuint256 from sdk core

* nit: use liqudityValue for doublecurrencylogo

---------

Co-authored-by: Kristie Huang <kristie.huang@uniswap.org>
2023-09-15 16:05:56 -04:00
482 changed files with 149447 additions and 8333 deletions

1
.env
View File

@@ -5,6 +5,7 @@ REACT_APP_AWS_API_REGION="us-east-2"
REACT_APP_AWS_API_ENDPOINT="https://beta.api.uniswap.org/v1/graphql"
REACT_APP_BNB_RPC_URL="https://rough-sleek-hill.bsc.quiknode.pro/413cc98cbc776cda8fdf1d0f47003583ff73d9bf"
REACT_APP_INFURA_KEY="4bf032f2d38a4ed6bb975b80d6340847"
REACT_APP_QUICKNODE_MAINNET_RPC_URL="https://magical-alien-tab.quiknode.pro/669e87e569a8277d3fbd9e202f9df93189f19f4c"
REACT_APP_MOONPAY_API="https://api.moonpay.com"
REACT_APP_MOONPAY_LINK="https://us-central1-uniswap-mobile.cloudfunctions.net/signMoonpayLinkV2?platform=web&env=staging"
REACT_APP_MOONPAY_PUBLISHABLE_KEY="pk_test_DycfESRid31UaSxhI5yWKe1r5E5kKSz"

View File

@@ -12,4 +12,5 @@ REACT_APP_MOONPAY_PUBLISHABLE_KEY="pk_live_uQG4BJC4w3cxnqpcSqAfohdBFDTsY6E"
REACT_APP_SENTRY_ENABLED=true
REACT_APP_SENTRY_TRACES_SAMPLE_RATE=0.00003
REACT_APP_STATSIG_PROXY_URL="https://api.uniswap.org/v1/statsig-proxy"
REACT_APP_QUICKNODE_MAINNET_RPC_URL="https://ultra-blue-flower.quiknode.pro/770b22d5f362c537bc8fe19b034c45b22958f880"
THE_GRAPH_SCHEMA_ENDPOINT="https://api.thegraph.com/subgraphs/name/uniswap/uniswap-v3?source=uniswap"

View File

@@ -70,6 +70,13 @@ module.exports = {
],
},
],
'no-restricted-syntax': [
'error',
{
selector: ':matches(ExportAllDeclaration)',
message: 'Barrel exports bloat the bundle size by preventing tree-shaking.',
},
],
},
},
{

View File

@@ -11,6 +11,7 @@ on:
- main
- releases/staging
pull_request:
workflow_dispatch:
jobs:
lint:

1
CODEOWNERS Normal file
View File

@@ -0,0 +1 @@
* @uniswap/web-admins

View File

@@ -9,6 +9,7 @@ ignore:
- "**/styled.tsx"
- "**/constants/**/*"
- "constants/**/*"
- "src/dev/*"
coverage:
status:

View File

@@ -5,7 +5,6 @@ const { execSync } = require('child_process')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const path = require('path')
const ModuleScopePlugin = require('react-dev-utils/ModuleScopePlugin')
const TerserPlugin = require('terser-webpack-plugin')
const { IgnorePlugin, ProvidePlugin } = require('webpack')
const { RetryChunkLoadPlugin } = require('webpack-retry-chunk-load-plugin')
@@ -131,6 +130,12 @@ module.exports = {
},
})
// Retain source maps for node_modules packages:
webpackConfig.module.rules[0] = {
...webpackConfig.module.rules[0],
exclude: /node_modules/,
}
// Configure webpack transpilation (create-react-app specifies transpilation rules in a oneOf):
webpackConfig.module.rules[1].oneOf = webpackConfig.module.rules[1].oneOf.map((rule) => {
if (rule.loader && rule.loader.match(/babel-loader/)) {
@@ -140,18 +145,20 @@ module.exports = {
return rule
})
// Run terser compression on node_modules before tree-shaking, so that tree-shaking is more effective.
// This works by eliminating dead code, so that webpack can identify unused imports and tree-shake them;
// it is only necessary for node_modules - it is done through linting for our own source code -
// see https://medium.com/engineering-housing/dead-code-elimination-and-tree-shaking-at-housing-part-1-307a94b30f23#7e03:
webpackConfig.module.rules.push({
enforce: 'post',
test: /node_modules.*\.(js)$/,
loader: path.join(__dirname, 'scripts/terser-loader.js'),
options: { compress: true, mangle: false },
})
// Configure webpack optimization:
webpackConfig.optimization = Object.assign(
webpackConfig.optimization,
{
minimize: isProduction,
minimizer: [
new TerserPlugin({
minify: TerserPlugin.swcMinify,
parallel: require('os').cpus().length,
}),
],
},
isProduction
? {
splitChunks: {
@@ -170,13 +177,6 @@ module.exports = {
// Configure webpack resolution. webpackConfig.cache is unused with swc-loader, but the resolver can still cache:
webpackConfig.resolve = Object.assign(webpackConfig.resolve, { unsafeCache: true })
webpackConfig.ignoreWarnings = [
// Source mappings for a package will fail if the package does not provide them, but the build will still succeed,
// so it is unnecessary (and bothersome) to log it. This should be turned off when debugging missing sourcemaps.
// See https://webpack.js.org/loaders/source-map-loader#ignoring-warnings.
/Failed to parse source map/,
]
return webpackConfig
},
},

View File

@@ -39,4 +39,30 @@ describe('Landing Page', () => {
cy.get(getTestSelector('pool-nav-link')).last().click()
cy.url().should('include', '/pools')
})
it('does not render uk compliance banner in US', () => {
cy.visit('/swap')
cy.contains('UK disclaimer').should('not.exist')
})
it('renders uk compliance banner in uk', () => {
cy.intercept('https://api.uniswap.org/v1/amplitude-proxy', (req) => {
const requestBody = JSON.stringify(req.body)
const byteSize = new Blob([requestBody]).size
req.alias = 'amplitude'
req.reply(
JSON.stringify({
code: 200,
server_upload_time: Date.now(),
payload_size_bytes: byteSize,
events_ingested: req.body.events.length,
}),
{
'origin-country': 'GB',
}
)
})
cy.visit('/swap')
cy.contains('UK disclaimer')
})
})

View File

@@ -2,43 +2,48 @@ import { getTestSelector } from '../../utils'
describe('Mini Portfolio account drawer', () => {
beforeEach(() => {
cy.intercept(/api.uniswap.org\/v1\/graphql/, cy.spy().as('gqlSpy'))
const portfolioSpy = cy.spy().as('portfolioSpy')
cy.intercept(/api.uniswap.org\/v1\/graphql/, (req) => {
if (req.body.operationName === 'PortfolioBalances') {
portfolioSpy(req)
}
})
cy.visit('/swap')
})
it('fetches balances when account button is first hovered', () => {
// The balances should not be fetched before the account button is hovered
cy.get('@gqlSpy').should('not.have.been.called')
cy.get('@portfolioSpy').should('not.have.been.called')
// Balances should have been fetched once after hover
cy.get(getTestSelector('web3-status-connected')).trigger('mouseover')
cy.get('@gqlSpy').should('have.been.calledOnce')
cy.get('@portfolioSpy').should('have.been.calledOnce')
})
it('should not re-fetch balances on second hover', () => {
// The balances should not be fetched before the account button is hovered
cy.get('@gqlSpy').should('not.have.been.called')
cy.get('@portfolioSpy').should('not.have.been.called')
// Balances should have been fetched once after hover
cy.get(getTestSelector('web3-status-connected')).trigger('mouseover')
cy.get('@gqlSpy').should('have.been.calledOnce')
cy.get('@portfolioSpy').should('have.been.calledOnce')
// Balances should not be refetched upon second hover
cy.get(getTestSelector('web3-status-connected')).trigger('mouseover')
cy.get('@gqlSpy').should('have.been.calledOnce')
cy.get('@portfolioSpy').should('have.been.calledOnce')
})
it('should not re-fetch balances when the account drawer is opened', () => {
// The balances should not be fetched before the account button is hovered
cy.get('@gqlSpy').should('not.have.been.called')
cy.get('@portfolioSpy').should('not.have.been.called')
// Balances should have been fetched once after hover
cy.get(getTestSelector('web3-status-connected')).trigger('mouseover')
cy.get('@gqlSpy').should('have.been.calledOnce')
cy.get('@portfolioSpy').should('have.been.calledOnce')
// Balances should not be refetched upon opening drawer
cy.get(getTestSelector('web3-status-connected')).click()
cy.get('@gqlSpy').should('have.been.calledOnce')
cy.get('@portfolioSpy').should('have.been.calledOnce')
})
it('fetches account information', () => {
@@ -48,7 +53,7 @@ describe('Mini Portfolio account drawer', () => {
// Verify that wallet state loads correctly
cy.get(getTestSelector('mini-portfolio-navbar')).contains('Tokens')
cy.get(getTestSelector('mini-portfolio-page')).contains('Hidden (201)')
cy.get(getTestSelector('mini-portfolio-page')).contains('Hidden (197)')
cy.intercept(/graphql/, { fixture: 'mini-portfolio/nfts.json' })
cy.get(getTestSelector('mini-portfolio-navbar')).contains('NFTs').click()

View File

@@ -1,7 +1,33 @@
import { ChainId, MaxUint256, UNI_ADDRESSES } from '@uniswap/sdk-core'
const UNI_MAINNET = UNI_ADDRESSES[ChainId.MAINNET]
describe('Remove Liquidity', () => {
it('loads the token pair', () => {
cy.visit('/remove/v2/ETH/0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984')
it('loads the token pair in v2', () => {
cy.visit(`/remove/v2/ETH/${UNI_MAINNET}`)
cy.get('#remove-liquidity-tokena-symbol').should('contain.text', 'ETH')
cy.get('#remove-liquidity-tokenb-symbol').should('contain.text', 'UNI')
})
it('loads the token pair in v3', () => {
cy.visit(`/remove/1`)
cy.get('#remove-liquidity-tokens').should('contain.text', 'UNI/ETH')
cy.get('#remove-pooled-tokena-symbol').should('contain.text', 'Pooled UNI')
cy.get('#remove-pooled-tokenb-symbol').should('contain.text', 'Pooled ETH')
})
it('should redirect to error pages if pool does not exist', () => {
// Duplicate-token v2 pools redirect to position unavailable
cy.visit(`/remove/v2/ETH/ETH`)
cy.contains('Position unavailable')
// Single-token pools don't exist
cy.visit('/remove/v2/ETH')
cy.url().should('match', /\/not-found/)
// Nonexistent v3 pool
cy.visit(`/remove/${MaxUint256}`)
cy.contains('Position unavailable')
})
})

View File

@@ -6,10 +6,9 @@ describe('Swap settings', () => {
cy.contains('Settings').should('not.exist')
cy.get(getTestSelector('open-settings-dialog-button')).click()
cy.get(getTestSelector('mobile-settings-menu')).should('not.exist')
cy.contains('Max slippage').should('exist')
cy.contains('Max. slippage').should('exist')
cy.contains('Transaction deadline').should('exist')
cy.contains('UniswapX').should('exist')
cy.contains('Local routing').should('exist')
cy.get(getTestSelector('open-settings-dialog-button')).click()
cy.contains('Settings').should('not.exist')
})
@@ -26,9 +25,8 @@ describe('Swap settings', () => {
cy.get(getTestSelector('mobile-settings-menu'))
.should('exist')
.within(() => {
cy.contains('Max slippage').should('exist')
cy.contains('Max. slippage').should('exist')
cy.contains('UniswapX').should('exist')
cy.contains('Local routing').should('exist')
cy.contains('Transaction deadline').should('exist')
cy.get(getTestSelector('mobile-settings-close')).click()
})

View File

@@ -1,27 +0,0 @@
import { SwapEventName } from '@uniswap/analytics-events'
import { USDC_MAINNET } from 'constants/tokens'
import { getTestSelector } from '../../utils'
describe('Swap inputs with no wallet connected', () => {
it('can input and load a quote with no wallet connected', () => {
cy.visit(`/swap?inputCurrency=ETH&outputCurrency=${USDC_MAINNET.address}`)
cy.get(getTestSelector('web3-status-connected')).click()
// click twice, first time to show confirmation, second to confirm
cy.get(getTestSelector('wallet-disconnect')).click()
cy.get(getTestSelector('wallet-disconnect')).should('contain', 'Disconnect')
cy.get(getTestSelector('wallet-disconnect')).click()
cy.get(getTestSelector('close-account-drawer')).click()
// Enter amount to swap
cy.get('#swap-currency-output .token-amount-input').type('1').should('have.value', '1')
cy.get('#swap-currency-input .token-amount-input').should('not.have.value', '')
// Verify logging
cy.waitForAmplitudeEvent(SwapEventName.SWAP_QUOTE_RECEIVED).then((event: any) => {
cy.wrap(event.event_properties).should('have.property', 'quote_latency_milliseconds')
cy.wrap(event.event_properties.quote_latency_milliseconds).should('be.a', 'number')
cy.wrap(event.event_properties.quote_latency_milliseconds).should('be.gte', 0)
})
})
})

View File

@@ -1,4 +1,5 @@
import { ChainId, CurrencyAmount } from '@uniswap/sdk-core'
import { FeatureFlag } from 'featureFlags'
import { DAI, nativeOnChain, USDC_MAINNET } from '../../../src/constants/tokens'
import { getTestSelector } from '../../utils'
@@ -26,7 +27,9 @@ function stubSwapTxReceipt() {
describe('UniswapX Toggle', () => {
beforeEach(() => {
cy.intercept(QuoteEndpoint, { fixture: QuoteWhereUniswapXIsBetter })
cy.visit(`/swap/?inputCurrency=${USDC_MAINNET.address}&outputCurrency=${DAI.address}`)
cy.visit(`/swap/?inputCurrency=${USDC_MAINNET.address}&outputCurrency=${DAI.address}`, {
featureFlags: [{ name: FeatureFlag.uniswapXDefaultEnabled, value: false }],
})
})
it('only displays uniswapx ui when setting is on', () => {
@@ -76,7 +79,9 @@ describe('UniswapX Orders', () => {
stubSwapTxReceipt()
cy.hardhat().then((hardhat) => hardhat.fund(hardhat.wallet, CurrencyAmount.fromRawAmount(USDC_MAINNET, 3e8)))
cy.visit(`/swap/?inputCurrency=${USDC_MAINNET.address}&outputCurrency=${DAI.address}`)
cy.visit(`/swap/?inputCurrency=${USDC_MAINNET.address}&outputCurrency=${DAI.address}`, {
featureFlags: [{ name: FeatureFlag.uniswapXDefaultEnabled, value: false }],
})
})
it('can swap exact-in trades using uniswapX', () => {
@@ -164,7 +169,9 @@ describe('UniswapX Eth Input', () => {
stubSwapTxReceipt()
cy.visit(`/swap/?inputCurrency=ETH&outputCurrency=${DAI.address}`)
cy.visit(`/swap/?inputCurrency=ETH&outputCurrency=${DAI.address}`, {
featureFlags: [{ name: FeatureFlag.uniswapXDefaultEnabled, value: false }],
})
})
it('can swap using uniswapX with ETH as input', () => {
@@ -249,7 +256,9 @@ describe('UniswapX activity history', () => {
cy.hardhat().then(async (hardhat) => {
await hardhat.fund(hardhat.wallet, CurrencyAmount.fromRawAmount(USDC_MAINNET, 3e8))
})
cy.visit(`/swap/?inputCurrency=${USDC_MAINNET.address}&outputCurrency=${DAI.address}`)
cy.visit(`/swap/?inputCurrency=${USDC_MAINNET.address}&outputCurrency=${DAI.address}`, {
featureFlags: [{ name: FeatureFlag.uniswapXDefaultEnabled, value: false }],
})
})
it('can view UniswapX order status progress in activity', () => {

View File

@@ -48,40 +48,33 @@ describe('Token details', () => {
})
it('token with warning and low trading volume should have all information populated', () => {
// Shiba predator token, low trading volume and also has warning modal
cy.visit('/tokens/ethereum/0xa71d0588EAf47f12B13cF8eC750430d21DF04974')
// Null token created for this test, 0 trading volume and has warning modal
cy.visit('/tokens/ethereum/0x1eFBB78C8b917f67986BcE54cE575069c0143681')
// Should have missing price chart when price unavailable (expected for this token)
if (cy.get('[data-cy="chart-header"]').contains('Price Unavailable')) {
if (cy.get('[data-cy="chart-header"]').contains('Price unavailable')) {
cy.get('[data-cy="missing-chart"]').should('exist')
}
// Stats should have: TVL, 24H Volume, 52W low, 52W high
cy.get(getTestSelector('token-details-stats')).should('exist')
cy.get(getTestSelector('token-details-stats')).within(() => {
cy.get('[data-cy="tvl"]').should('exist')
cy.get('[data-cy="volume-24h"]').should('exist')
cy.get('[data-cy="52w-low"]').should('exist')
cy.get('[data-cy="52w-high"]').should('exist')
})
// Stats should not exist
cy.get(getTestSelector('token-details-stats')).should('not.exist')
// About section should have description of token
cy.get(getTestSelector('token-details-about-section')).should('exist')
cy.contains('QOM is the Shiba Predator').should('exist')
cy.contains('No token information available').should('exist')
// Links section should link out to Etherscan, More analytics, Website, Twitter
// Links section should link out to Etherscan, More analytics
cy.get('[data-cy="resources-container"]').within(() => {
cy.contains('Etherscan')
.should('have.attr', 'href')
.and('include', 'etherscan.io/address/0xa71d0588EAf47f12B13cF8eC750430d21DF04974')
.and('include', 'etherscan.io/address/0x1eFBB78C8b917f67986BcE54cE575069c0143681')
cy.contains('More analytics')
.should('have.attr', 'href')
.and('include', 'info.uniswap.org/#/tokens/0xa71d0588EAf47f12B13cF8eC750430d21DF04974')
cy.contains('Website').should('have.attr', 'href').and('include', 'qom')
cy.contains('Twitter').should('have.attr', 'href').and('include', 'twitter.com/ShibaPredator1')
.and('include', 'info.uniswap.org/#/tokens/0x1eFBB78C8b917f67986BcE54cE575069c0143681')
})
// Contract address should be displayed
cy.contains('0xa71d0588EAf47f12B13cF8eC750430d21DF04974').should('exist')
cy.contains('0x1eFBB78C8b917f67986BcE54cE575069c0143681').should('exist')
// Warning label should show if relevant ([spec](https://www.notion.so/3f7fce6f93694be08a94a6984d50298e))
cy.get('[data-cy="token-safety-message"]')

View File

@@ -65,7 +65,7 @@ describe('Universal search bar', () => {
cy.get(getTestSelector('searchbar-token-row-ETHEREUM-NATIVE'))
// Validate that we go to the searched/selected result.
getSearchBar().type('{enter}')
cy.get(getTestSelector('searchbar-token-row-ETHEREUM-NATIVE')).click()
cy.url().should('contain', 'tokens/ethereum/NATIVE')
}
)

View File

@@ -19,7 +19,7 @@ describe('disconnect wallet', () => {
// Verify wallet has disconnected
cy.contains('Connect a wallet').should('exist')
cy.get(getTestSelector('navbar-connect-wallet')).contains('Connect')
cy.contains('Connect Wallet')
cy.contains('Connect wallet')
// Verify swap input is cleared
cy.get('#swap-currency-input .token-amount-input').should('have.value', '1')

View File

@@ -53,7 +53,7 @@ describe('Wallet Dropdown', () => {
describe('should change locale with feature flag', () => {
beforeEach(() => {
cy.visit('/', { featureFlags: [FeatureFlag.currencyConversion] })
cy.visit('/', { featureFlags: [{ name: FeatureFlag.currencyConversion, value: true }] })
cy.get(getTestSelector('web3-status-connected')).click()
cy.get(getTestSelector('wallet-settings')).click()
})
@@ -147,19 +147,19 @@ describe('Wallet Dropdown', () => {
describe('local currency', () => {
it('loads local currency from the query param', () => {
cy.visit('/', { featureFlags: [FeatureFlag.currencyConversion] })
cy.visit('/', { featureFlags: [{ name: FeatureFlag.currencyConversion, value: true }] })
cy.get(getTestSelector('web3-status-connected')).click()
cy.get(getTestSelector('wallet-settings')).click()
cy.contains('USD')
cy.visit('/?cur=AUD', { featureFlags: [FeatureFlag.currencyConversion] })
cy.visit('/?cur=AUD', { featureFlags: [{ name: FeatureFlag.currencyConversion, value: true }] })
cy.get(getTestSelector('web3-status-connected')).click()
cy.get(getTestSelector('wallet-settings')).click()
cy.contains('AUD')
})
it('loads local currency from menu', () => {
cy.visit('/', { featureFlags: [FeatureFlag.currencyConversion] })
cy.visit('/', { featureFlags: [{ name: FeatureFlag.currencyConversion, value: true }] })
cy.get(getTestSelector('web3-status-connected')).click()
cy.get(getTestSelector('wallet-settings')).click()
cy.contains('USD')

View File

@@ -24,7 +24,7 @@ declare global {
}
interface VisitOptions {
serviceWorker?: true
featureFlags?: Array<FeatureFlag>
featureFlags?: Array<{ name: FeatureFlag; value: boolean }>
/**
* Initial user state.
* @default {@type import('../utils/user-state').CONNECTED_WALLET_USER_STATE}
@@ -53,14 +53,16 @@ Cypress.Commands.overwrite(
setInitialUserState(win, {
...initialState,
hideUniswapWalletBanner: true,
...CONNECTED_WALLET_USER_STATE,
...(options?.userState ?? {}),
})
// Set feature flags, if configured.
if (options?.featureFlags) {
const featureFlags = options.featureFlags.reduce((flags, flag) => ({ ...flags, [flag]: 'enabled' }), {})
const featureFlags = options.featureFlags.reduce(
(flags, flag) => ({ ...flags, [flag.name]: flag.value ? 'enabled' : 'control' }),
{}
)
win.localStorage.setItem('featureFlags', JSON.stringify(featureFlags))
}

View File

@@ -9,8 +9,9 @@ beforeEach(() => {
req.headers['origin'] = 'https://app.uniswap.org'
})
// Infura is disabled for cypress tests - calls should be routed through the connected wallet instead.
// Network RPCs are disabled for cypress tests - calls should be routed through the connected wallet instead.
cy.intercept(/infura.io/, { statusCode: 404 })
cy.intercept(/quiknode.pro/, { statusCode: 404 })
// Log requests to hardhat.
cy.intercept(/:8545/, logJsonRpc)
@@ -26,7 +27,10 @@ beforeEach(() => {
server_upload_time: Date.now(),
payload_size_bytes: byteSize,
events_ingested: req.body.events.length,
})
}),
{
'origin-country': 'US',
}
)
}).intercept('https://*.sentry.io', { statusCode: 200 })

View File

@@ -1,3 +1,4 @@
import { connectionMetaKey } from '../../src/connection/meta'
import { ConnectionType } from '../../src/connection/types'
import { UserState } from '../../src/state/user/reducer'
@@ -10,23 +11,30 @@ export const DISCONNECTED_WALLET_USER_STATE: Partial<UserState> = { selectedWall
* Other persisted slices are not set, so they will be filled with their respective initial values
* when the app runs.
*/
export function setInitialUserState(win: Cypress.AUTWindow, initialUserState: any) {
export function setInitialUserState(win: Cypress.AUTWindow, state: UserState) {
// Selected wallet should also be reflected in localStorage, so that eager connections work.
if (state.selectedWallet) {
win.localStorage.setItem(
connectionMetaKey,
JSON.stringify({
type: state.selectedWallet,
})
)
}
win.indexedDB.deleteDatabase('redux')
const dbRequest = win.indexedDB.open('redux')
dbRequest.onsuccess = function () {
const db = dbRequest.result
const transaction = db.transaction('keyvaluepairs', 'readwrite')
const store = transaction.objectStore('keyvaluepairs')
store.put(
{
user: initialUserState,
user: state,
},
'persist:interface'
)
}
dbRequest.onupgradeneeded = function () {
const db = dbRequest.result
db.createObjectStore('keyvaluepairs')

View File

@@ -11,7 +11,7 @@ export const onRequest: PagesFunction = async ({ request, next }) => {
}
const res = next()
try {
return new HTMLRewriter().on('head', new MetaTagInjector(data)).transform(await res)
return new HTMLRewriter().on('head', new MetaTagInjector(data, request)).transform(await res)
} catch (e) {
return res
}

View File

@@ -2,6 +2,7 @@
import { ImageResponse } from '@vercel/og'
import React from 'react'
import { blocklistedCollections } from '../../../../../src/nft/utils/blocklist'
import { WATERMARK_URL } from '../../../../constants'
import getAsset from '../../../../utils/getAsset'
import getFont from '../../../../utils/getFont'
@@ -15,6 +16,10 @@ export const onRequest: PagesFunction = async ({ params, request }) => {
const tokenId = index[1]?.toString()
const cacheUrl = origin + '/nfts/asset/' + collectionAddress + '/' + tokenId
if (blocklistedCollections.includes(collectionAddress)) {
return new Response('Collection unsupported.', { status: 404 })
}
const data = await getRequest(
cacheUrl,
() => getAsset(collectionAddress, tokenId, cacheUrl),

View File

@@ -18,3 +18,12 @@ test.each(invalidAssetImageUrl)('invalidAssetImageUrl', async (url) => {
const response = await fetch(new Request(url))
expect(response.status).toBe(404)
})
const blockedAssetImageUrl = [
'http://127.0.0.1:3000/api/image/nfts/asset/0xd4d871419714b778ebec2e22c7c53572b573706e/276',
]
test.each(blockedAssetImageUrl)('blockedAssetImageUrl', async (url) => {
const response = await fetch(new Request(url))
expect(response.status).toBe(404)
})

View File

@@ -2,9 +2,10 @@
import { ImageResponse } from '@vercel/og'
import React from 'react'
import { blocklistedCollections } from '../../../../../src/nft/utils/blocklist'
import { getColor } from '../../../../../src/utils/getColor'
import { CHECK_URL, WATERMARK_URL } from '../../../../constants'
import getCollection from '../../../../utils/getCollection'
import getColor from '../../../../utils/getColor'
import getFont from '../../../../utils/getFont'
import { getRequest } from '../../../../utils/getRequest'
@@ -15,6 +16,10 @@ export const onRequest: PagesFunction = async ({ params, request }) => {
const collectionAddress = index?.toString()
const cacheUrl = origin + '/nfts/collection/' + collectionAddress
if (blocklistedCollections.includes(collectionAddress)) {
return new Response('Collection unsupported.', { status: 404 })
}
const data = await getRequest(
cacheUrl,
() => getCollection(collectionAddress, cacheUrl),

View File

@@ -23,3 +23,12 @@ test.each(invalidCollectionImageUrls)('invalidAssetImageUrl', async (url) => {
const response = await fetch(new Request(url))
expect(response.status).toBeOneOf([404, 500])
})
const blockedCollectionImageUrls = [
'http://127.0.0.1:3000/api/image/nfts/collection/0xd4d871419714b778ebec2e22c7c53572b573706e',
]
test.each(blockedCollectionImageUrls)('blockedCollectionImageUrl', async (url) => {
const response = await fetch(new Request(url))
expect(response.status).toBeOneOf([404, 500])
})

View File

@@ -2,8 +2,8 @@
import { ImageResponse } from '@vercel/og'
import React from 'react'
import { getColor } from '../../../../src/utils/getColor'
import { WATERMARK_URL } from '../../../constants'
import getColor from '../../../utils/getColor'
import getFont from '../../../utils/getFont'
import getNetworkLogoUrl from '../../../utils/getNetworkLogoURL'
import { getRequest } from '../../../utils/getRequest'

View File

@@ -6,12 +6,15 @@ test('should append meta tag to element', () => {
} as unknown as Element
const property = 'property'
const content = 'content'
const injector = new MetaTagInjector({
title: 'test',
url: 'testUrl',
image: 'testImage',
description: 'testDescription',
})
const injector = new MetaTagInjector(
{
title: 'test',
url: 'testUrl',
image: 'testImage',
description: 'testDescription',
},
new Request('http://localhost')
)
injector.append(element, property, content)
expect(element.append).toHaveBeenCalledWith(`<meta property="${property}" content="${content}"/>`, { html: true })
@@ -36,3 +39,22 @@ test('should append meta tag to element', () => {
expect(element.append).toHaveBeenCalledTimes(13)
})
test('should pass through header blocked paths', () => {
const element = {
append: jest.fn(),
} as unknown as Element
const request = new Request('http://localhost')
request.headers.set('x-blocked-paths', '/')
const injector = new MetaTagInjector(
{
title: 'test',
url: 'testUrl',
image: 'testImage',
description: 'testDescription',
},
request
)
injector.element(element)
expect(element.append).toHaveBeenCalledWith(`<meta property="x:blocked-paths" content="/"/>`, { html: true })
})

View File

@@ -10,7 +10,7 @@ type MetaTagInjectorInput = {
* to inject meta tags into the <head> of an HTML document.
*/
export class MetaTagInjector implements HTMLRewriterElementContentHandlers {
constructor(private input: MetaTagInjectorInput) {}
constructor(private input: MetaTagInjectorInput, private request: Request) {}
append(element: Element, property: string, content: string) {
element.append(`<meta property="${property}" content="${content}"/>`, { html: true })
@@ -38,5 +38,10 @@ export class MetaTagInjector implements HTMLRewriterElementContentHandlers {
this.append(element, 'twitter:image', this.input.image)
this.append(element, 'twitter:image:alt', this.input.title)
}
const blockedPaths = this.request.headers.get('x-blocked-paths')
if (blockedPaths) {
this.append(element, 'x:blocked-paths', blockedPaths)
}
}
}

View File

@@ -1,76 +1,2 @@
export const WATERMARK_URL = 'https://app.uniswap.org/images/324x74_App_Watermark.png'
export const CHECK_URL = 'https://app.uniswap.org/images/54x54_Verified_Check.svg'
export const DEFAULT_COLOR = [35, 43, 43]
export const predefinedTokenColors: { [key: string]: number[] } = {
// old WBTC
'https://raw.githubusercontent.com/Uniswap/assets/master/blockchains/ethereum/assets/0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599/logo.png':
[240, 146, 65],
// new WBTC
'https://assets.coingecko.com/coins/images/7598/large/wrapped_bitcoin_wbtc.png?1548822744': [240, 146, 65],
// DAI
'https://raw.githubusercontent.com/Uniswap/assets/master/blockchains/ethereum/assets/0x6B175474E89094C44Da98b954EedeAC495271d0F/logo.png':
[250, 176, 27],
// UNI
'https://raw.githubusercontent.com/Uniswap/assets/master/blockchains/ethereum/assets/0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984/logo.png':
[230, 53, 140],
// BUSD
'https://raw.githubusercontent.com/Uniswap/assets/master/blockchains/ethereum/assets/0x4Fabb145d64652a948d72533023f6E7A623C7C53/logo.png':
[239, 186, 9],
// AI-X
'https://s2.coinmarketcap.com/static/img/coins/64x64/26984.png': [41, 161, 241],
// ETH
'https://token-icons.s3.amazonaws.com/eth.png': [73, 112, 213],
// HARRYPOTTERSHIBAINUBITCOIN
'https://assets.coingecko.com/coins/images/30323/large/hpos10i_logo_casino_night-dexview.png?1684117567': [
222, 49, 16,
],
// PEPE
'https://raw.githubusercontent.com/Uniswap/assets/master/blockchains/ethereum/assets/0x6982508145454Ce325dDbE47a25d4ec3d2311933/logo.png':
[62, 174, 20],
// Unibot V2
'https://s2.coinmarketcap.com/static/img/coins/64x64/25436.png': [74, 10, 79],
// UNIBOT v1
'https://assets.coingecko.com/coins/images/30462/small/logonoline_%281%29.png?1687510315': [74, 10, 79],
// USDC
'https://raw.githubusercontent.com/Uniswap/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png':
[0, 102, 217],
// HEX
'https://raw.githubusercontent.com/Uniswap/assets/master/blockchains/ethereum/assets/0x2b591e99afE9f32eAA6214f7B7629768c40Eeb39/logo.png':
[249, 63, 140],
// MONG
'https://raw.githubusercontent.com/Uniswap/assets/master/blockchains/ethereum/assets/0x1ce270557C1f68Cfb577b856766310Bf8B47FD9C/logo.png':
[169, 109, 255],
// ARB
'https://raw.githubusercontent.com/Uniswap/assets/master/blockchains/ethereum/assets/0xB50721BCf8d664c30412Cfbc6cf7a15145234ad1/logo.png':
[41, 161, 241],
// PSYOP
'https://s2.coinmarketcap.com/static/img/coins/64x64/25422.png': [232, 143, 0],
// MATIC
'https://raw.githubusercontent.com/Uniswap/assets/master/blockchains/ethereum/assets/0x7D1AfA7B718fb893dB30A3aBc0Cfc608AaCfeBB0/logo.png':
[169, 109, 255],
// TURBO
'https://raw.githubusercontent.com/Uniswap/assets/master/blockchains/ethereum/assets/0xA35923162C49cF95e6BF26623385eb431ad920D3/logo.png':
[189, 110, 41],
// AIDOGE
'https://assets.coingecko.com/coins/images/29852/large/photo_2023-04-18_14-25-28.jpg?1681799160': [41, 161, 241],
// SIMPSON
'https://assets.coingecko.com/coins/images/30243/large/1111.png?1683692033': [232, 143, 0],
// OX
'https://assets.coingecko.com/coins/images/30604/large/Logo2.png?1685522119': [41, 89, 217],
// ANGLE
'https://assets.coingecko.com/coins/images/19060/large/ANGLE_Token-light.png?1666774221': [255, 85, 85],
// APE
'https://raw.githubusercontent.com/Uniswap/assets/master/blockchains/ethereum/assets/0x4d224452801ACEd8B2F0aebE155379bb5D594381/logo.png':
[5, 74, 169],
// GUSD
'https://raw.githubusercontent.com/Uniswap/assets/master/blockchains/ethereum/assets/0x056Fd409E1d7A124BD7017459dFEa2F387b6d5Cd/logo.png':
[0, 164, 189],
// OGN
'https://raw.githubusercontent.com/Uniswap/assets/master/blockchains/ethereum/assets/0x8207c1FfC5B6804F6024322CcF34F29c3541Ae26/logo.png':
[5, 74, 169],
// RPL
'https://raw.githubusercontent.com/Uniswap/assets/master/blockchains/ethereum/assets/0xD33526068D116cE69F19A9ee46F0bd304F21A51f/logo.png':
[255, 123, 79],
}

View File

@@ -8,7 +8,7 @@ export const onRequest: PagesFunction = async ({ params, request, next }) => {
const { index } = params
const collectionAddress = index[0]?.toString()
const tokenId = index[1]?.toString()
return getMetadataRequest(res, request.url, () => getAsset(collectionAddress, tokenId, request.url))
return getMetadataRequest(res, request, () => getAsset(collectionAddress, tokenId, request.url))
} catch (e) {
return res
}

View File

@@ -17,7 +17,7 @@ exports[`should inject metadata for valid assets 1`] = `
<link rel="apple-touch-icon" sizes="512x512" href="/images/512x512_App_Icon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<meta name="theme-color" content="#FC72FF" />
<meta name="theme-color" content="#fff" />
<meta
http-equiv="Content-Security-Policy"
@@ -37,7 +37,8 @@ exports[`should inject metadata for valid assets 1`] = `
-->
<link rel="manifest" href="/manifest.json" />
<link rel="preconnect" href="https://api.uniswap.org/v1/amplitude-proxy" />
<link rel="preconnect" href="https://api.uniswap.org/" crossorigin/>
<link rel="preconnect" href="https://mainnet.infura.io/" crossorigin/>
<link rel="preload" href="/fonts/Basel-Book.woff" as="font" type="font/woff" crossorigin />
<link rel="preload" href="/fonts/Basel-Medium.woff" as="font" type="font/woff" crossorigin />
@@ -164,7 +165,7 @@ exports[`should inject metadata for valid assets 2`] = `
<link rel="apple-touch-icon" sizes="512x512" href="/images/512x512_App_Icon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<meta name="theme-color" content="#FC72FF" />
<meta name="theme-color" content="#fff" />
<meta
http-equiv="Content-Security-Policy"
@@ -184,7 +185,8 @@ exports[`should inject metadata for valid assets 2`] = `
-->
<link rel="manifest" href="/manifest.json" />
<link rel="preconnect" href="https://api.uniswap.org/v1/amplitude-proxy" />
<link rel="preconnect" href="https://api.uniswap.org/" crossorigin/>
<link rel="preconnect" href="https://mainnet.infura.io/" crossorigin/>
<link rel="preload" href="/fonts/Basel-Book.woff" as="font" type="font/woff" crossorigin />
<link rel="preload" href="/fonts/Basel-Medium.woff" as="font" type="font/woff" crossorigin />
@@ -311,7 +313,7 @@ exports[`should inject metadata for valid assets 3`] = `
<link rel="apple-touch-icon" sizes="512x512" href="/images/512x512_App_Icon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<meta name="theme-color" content="#FC72FF" />
<meta name="theme-color" content="#fff" />
<meta
http-equiv="Content-Security-Policy"
@@ -331,7 +333,8 @@ exports[`should inject metadata for valid assets 3`] = `
-->
<link rel="manifest" href="/manifest.json" />
<link rel="preconnect" href="https://api.uniswap.org/v1/amplitude-proxy" />
<link rel="preconnect" href="https://api.uniswap.org/" crossorigin/>
<link rel="preconnect" href="https://mainnet.infura.io/" crossorigin/>
<link rel="preload" href="/fonts/Basel-Book.woff" as="font" type="font/woff" crossorigin />
<link rel="preload" href="/fonts/Basel-Medium.woff" as="font" type="font/woff" crossorigin />

View File

@@ -7,7 +7,7 @@ export const onRequest: PagesFunction = async ({ params, request, next }) => {
try {
const { index } = params
const collectionAddress = index?.toString()
return getMetadataRequest(res, request.url, () => getCollection(collectionAddress, request.url))
return getMetadataRequest(res, request, () => getCollection(collectionAddress, request.url))
} catch (e) {
return res
}

View File

@@ -17,7 +17,7 @@ exports[`should inject metadata for collections 1`] = `
<link rel="apple-touch-icon" sizes="512x512" href="/images/512x512_App_Icon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<meta name="theme-color" content="#FC72FF" />
<meta name="theme-color" content="#fff" />
<meta
http-equiv="Content-Security-Policy"
@@ -37,7 +37,8 @@ exports[`should inject metadata for collections 1`] = `
-->
<link rel="manifest" href="/manifest.json" />
<link rel="preconnect" href="https://api.uniswap.org/v1/amplitude-proxy" />
<link rel="preconnect" href="https://api.uniswap.org/" crossorigin/>
<link rel="preconnect" href="https://mainnet.infura.io/" crossorigin/>
<link rel="preload" href="/fonts/Basel-Book.woff" as="font" type="font/woff" crossorigin />
<link rel="preload" href="/fonts/Basel-Medium.woff" as="font" type="font/woff" crossorigin />
@@ -164,7 +165,7 @@ exports[`should inject metadata for collections 2`] = `
<link rel="apple-touch-icon" sizes="512x512" href="/images/512x512_App_Icon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<meta name="theme-color" content="#FC72FF" />
<meta name="theme-color" content="#fff" />
<meta
http-equiv="Content-Security-Policy"
@@ -184,7 +185,8 @@ exports[`should inject metadata for collections 2`] = `
-->
<link rel="manifest" href="/manifest.json" />
<link rel="preconnect" href="https://api.uniswap.org/v1/amplitude-proxy" />
<link rel="preconnect" href="https://api.uniswap.org/" crossorigin/>
<link rel="preconnect" href="https://mainnet.infura.io/" crossorigin/>
<link rel="preload" href="/fonts/Basel-Book.woff" as="font" type="font/woff" crossorigin />
<link rel="preload" href="/fonts/Basel-Medium.woff" as="font" type="font/woff" crossorigin />
@@ -311,7 +313,7 @@ exports[`should inject metadata for collections 3`] = `
<link rel="apple-touch-icon" sizes="512x512" href="/images/512x512_App_Icon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<meta name="theme-color" content="#FC72FF" />
<meta name="theme-color" content="#fff" />
<meta
http-equiv="Content-Security-Policy"
@@ -331,7 +333,8 @@ exports[`should inject metadata for collections 3`] = `
-->
<link rel="manifest" href="/manifest.json" />
<link rel="preconnect" href="https://api.uniswap.org/v1/amplitude-proxy" />
<link rel="preconnect" href="https://api.uniswap.org/" crossorigin/>
<link rel="preconnect" href="https://mainnet.infura.io/" crossorigin/>
<link rel="preload" href="/fonts/Basel-Book.woff" as="font" type="font/woff" crossorigin />
<link rel="preload" href="/fonts/Basel-Medium.woff" as="font" type="font/woff" crossorigin />
@@ -458,7 +461,7 @@ exports[`should inject metadata for collections 4`] = `
<link rel="apple-touch-icon" sizes="512x512" href="/images/512x512_App_Icon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<meta name="theme-color" content="#FC72FF" />
<meta name="theme-color" content="#fff" />
<meta
http-equiv="Content-Security-Policy"
@@ -478,7 +481,8 @@ exports[`should inject metadata for collections 4`] = `
-->
<link rel="manifest" href="/manifest.json" />
<link rel="preconnect" href="https://api.uniswap.org/v1/amplitude-proxy" />
<link rel="preconnect" href="https://api.uniswap.org/" crossorigin/>
<link rel="preconnect" href="https://mainnet.infura.io/" crossorigin/>
<link rel="preload" href="/fonts/Basel-Book.woff" as="font" type="font/woff" crossorigin />
<link rel="preload" href="/fonts/Basel-Medium.woff" as="font" type="font/woff" crossorigin />

View File

@@ -11,7 +11,7 @@ export const onRequest: PagesFunction = async ({ params, request, next }) => {
if (!tokenAddress) {
return res
}
return getMetadataRequest(res, request.url, () => getToken(networkName, tokenAddress, request.url))
return getMetadataRequest(res, request, () => getToken(networkName, tokenAddress, request.url))
} catch (e) {
return res
}

View File

@@ -17,7 +17,7 @@ exports[`should inject metadata for valid tokens 1`] = `
<link rel="apple-touch-icon" sizes="512x512" href="/images/512x512_App_Icon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<meta name="theme-color" content="#FC72FF" />
<meta name="theme-color" content="#fff" />
<meta
http-equiv="Content-Security-Policy"
@@ -37,7 +37,8 @@ exports[`should inject metadata for valid tokens 1`] = `
-->
<link rel="manifest" href="/manifest.json" />
<link rel="preconnect" href="https://api.uniswap.org/v1/amplitude-proxy" />
<link rel="preconnect" href="https://api.uniswap.org/" crossorigin/>
<link rel="preconnect" href="https://mainnet.infura.io/" crossorigin/>
<link rel="preload" href="/fonts/Basel-Book.woff" as="font" type="font/woff" crossorigin />
<link rel="preload" href="/fonts/Basel-Medium.woff" as="font" type="font/woff" crossorigin />
@@ -164,7 +165,7 @@ exports[`should inject metadata for valid tokens 2`] = `
<link rel="apple-touch-icon" sizes="512x512" href="/images/512x512_App_Icon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<meta name="theme-color" content="#FC72FF" />
<meta name="theme-color" content="#fff" />
<meta
http-equiv="Content-Security-Policy"
@@ -184,7 +185,8 @@ exports[`should inject metadata for valid tokens 2`] = `
-->
<link rel="manifest" href="/manifest.json" />
<link rel="preconnect" href="https://api.uniswap.org/v1/amplitude-proxy" />
<link rel="preconnect" href="https://api.uniswap.org/" crossorigin/>
<link rel="preconnect" href="https://mainnet.infura.io/" crossorigin/>
<link rel="preload" href="/fonts/Basel-Book.woff" as="font" type="font/woff" crossorigin />
<link rel="preload" href="/fonts/Basel-Medium.woff" as="font" type="font/woff" crossorigin />
@@ -311,7 +313,7 @@ exports[`should inject metadata for valid tokens 3`] = `
<link rel="apple-touch-icon" sizes="512x512" href="/images/512x512_App_Icon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<meta name="theme-color" content="#FC72FF" />
<meta name="theme-color" content="#fff" />
<meta
http-equiv="Content-Security-Policy"
@@ -331,7 +333,8 @@ exports[`should inject metadata for valid tokens 3`] = `
-->
<link rel="manifest" href="/manifest.json" />
<link rel="preconnect" href="https://api.uniswap.org/v1/amplitude-proxy" />
<link rel="preconnect" href="https://api.uniswap.org/" crossorigin/>
<link rel="preconnect" href="https://mainnet.infura.io/" crossorigin/>
<link rel="preload" href="/fonts/Basel-Book.woff" as="font" type="font/woff" crossorigin />
<link rel="preload" href="/fonts/Basel-Medium.woff" as="font" type="font/woff" crossorigin />
@@ -458,7 +461,7 @@ exports[`should inject metadata for valid tokens 4`] = `
<link rel="apple-touch-icon" sizes="512x512" href="/images/512x512_App_Icon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<meta name="theme-color" content="#FC72FF" />
<meta name="theme-color" content="#fff" />
<meta
http-equiv="Content-Security-Policy"
@@ -478,7 +481,8 @@ exports[`should inject metadata for valid tokens 4`] = `
-->
<link rel="manifest" href="/manifest.json" />
<link rel="preconnect" href="https://api.uniswap.org/v1/amplitude-proxy" />
<link rel="preconnect" href="https://api.uniswap.org/" crossorigin/>
<link rel="preconnect" href="https://mainnet.infura.io/" crossorigin/>
<link rel="preload" href="/fonts/Basel-Book.woff" as="font" type="font/woff" crossorigin />
<link rel="preload" href="/fonts/Basel-Medium.woff" as="font" type="font/woff" crossorigin />

View File

@@ -1,39 +0,0 @@
import { DEFAULT_COLOR } from '../constants'
import getColor from './getColor'
test('should return the average color of a black PNG image', async () => {
const image = 'https://static.vecteezy.com/system/resources/previews/001/209/957/original/square-png.png'
const color = await getColor(image)
expect(color).toEqual([0, 0, 0])
})
test('should return the average color of a blue PNG image', async () => {
const image = 'https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcTB2Ztcim-RKbOu57kfjYpXnnS1MO5YMUaUH9Lk5Eg&s'
const color = await getColor(image)
expect(color).toEqual([2, 6, 251])
})
test('should return the average color of a white PNG image', async () => {
const image = 'https://www.cac.cornell.edu/wiki/images/4/44/White_square.png'
const color = await getColor(image)
expect(color).toEqual([255, 255, 255])
})
test('should return the average color of a white PNG image with whiteness dimmed', async () => {
const image = 'https://www.cac.cornell.edu/wiki/images/4/44/White_square.png'
const color = await getColor(image, true)
expect(color).toEqual(DEFAULT_COLOR)
})
test('should return the average color of a black JPG image', async () => {
const image =
'https://imageio.forbes.com/specials-images/imageserve/5ed6636cdd5d320006caf841/0x0.jpg?format=jpg&width=1200'
const color = await getColor(image)
expect(color).toEqual([0, 0, 0])
})
test('should return default color for a gif image', async () => {
const image = 'https://thumbs.gfycat.com/AgitatedLiveAgouti-size_restricted.gif'
const color = await getColor(image)
expect(color).toEqual(DEFAULT_COLOR)
})

View File

@@ -4,13 +4,13 @@ import { Data } from './cache'
export async function getMetadataRequest(
res: Promise<Response>,
url: string,
request: Request,
getData: () => Promise<Data | undefined>
) {
try {
const cachedData = await getRequest(url, getData, (data): data is Data => true)
const cachedData = await getRequest(request.url, getData, (data): data is Data => true)
if (cachedData) {
return new HTMLRewriter().on('head', new MetaTagInjector(cachedData)).transform(await res)
return new HTMLRewriter().on('head', new MetaTagInjector(cachedData, request)).transform(await res)
} else {
return res
}

View File

@@ -13,14 +13,15 @@
"graphql:generate:thegraph": "graphql-codegen --config graphql.thegraph.codegen.config.ts",
"graphql:generate": "yarn graphql:generate:data && yarn graphql:generate:thegraph",
"graphql": "yarn graphql:fetch && yarn graphql:generate",
"sitemap:generate": "node scripts/generate-sitemap.js",
"i18n:extract": "lingui extract --locale en-US",
"i18n:compile": "lingui compile",
"i18n": "yarn i18n:extract --clean && yarn i18n:compile",
"prepare": "concurrently \"npm:ajv\" \"npm:contracts\" \"npm:graphql\" \"npm:i18n\"",
"prepare": "concurrently \"npm:ajv\" \"npm:contracts\" \"npm:graphql\" \"npm:i18n\" \"npm:sitemap:generate\"",
"start": "craco start",
"start:cloud": "NODE_OPTIONS=--dns-result-order=ipv4first PORT=3001 npx wrangler pages dev --compatibility-flags=nodejs_compat --compatibility-date=2023-08-01 --proxy=3001 --port=3000 -- yarn start",
"build": "craco build",
"analyze": "source-map-explorer 'build/static/js/*.js' --only-mapped",
"analyze": "source-map-explorer 'build/static/js/*.js' --no-border-checks --gzip",
"serve": "serve build -s -l 3000",
"lint": "yarn eslint --ignore-path .gitignore --cache --cache-location node_modules/.cache/eslint/ .",
"typecheck": "tsc",
@@ -114,6 +115,7 @@
"@types/ua-parser-js": "^0.7.36",
"@types/uuid": "^8.3.4",
"@types/wcag-contrast": "^3.0.0",
"@types/xml2js": "^0.4.12",
"@uniswap/default-token-list": "^11.2.0",
"@uniswap/eslint-config": "^1.2.0",
"@vanilla-extract/jest-transform": "^1.1.1",
@@ -152,6 +154,7 @@
"source-map-explorer": "^2.5.3",
"start-server-and-test": "^2.0.0",
"swc-loader": "^0.2.3",
"terser": "^5.19.4",
"terser-webpack-plugin": "^5.3.9",
"ts-jest": "^29.1.1",
"ts-transform-graphql-tag": "^0.2.1",
@@ -189,8 +192,8 @@
"@sentry/tracing": "^7.45.0",
"@sentry/types": "^7.45.0",
"@types/react-window-infinite-loader": "^1.0.6",
"@uniswap/analytics": "^1.4.0",
"@uniswap/analytics-events": "^2.22.0",
"@uniswap/analytics": "1.5.0",
"@uniswap/analytics-events": "^2.24.0",
"@uniswap/governance": "^1.0.2",
"@uniswap/liquidity-staker": "^1.0.2",
"@uniswap/merkle-distributor": "^1.0.1",
@@ -219,16 +222,16 @@
"@visx/react-spring": "^2.12.2",
"@visx/responsive": "^2.10.0",
"@visx/shape": "^2.11.1",
"@web3-react/coinbase-wallet": "^8.2.2",
"@web3-react/core": "^8.2.2",
"@web3-react/eip1193": "^8.2.2",
"@web3-react/empty": "^8.2.2",
"@web3-react/gnosis-safe": "^8.2.3",
"@web3-react/metamask": "^8.2.3",
"@web3-react/network": "^8.2.2",
"@web3-react/types": "^8.2.2",
"@web3-react/url": "^8.2.2",
"@web3-react/walletconnect-v2": "^8.5.0",
"@web3-react/coinbase-wallet": "^8.2.3",
"@web3-react/core": "^8.2.3",
"@web3-react/eip1193": "^8.2.3",
"@web3-react/empty": "^8.2.3",
"@web3-react/gnosis-safe": "^8.2.4",
"@web3-react/metamask": "^8.2.4",
"@web3-react/network": "^8.2.3",
"@web3-react/types": "^8.2.3",
"@web3-react/url": "^8.2.3",
"@web3-react/walletconnect-v2": "^8.5.1",
"ajv": "^8.11.0",
"ajv-formats": "^2.1.1",
"array.prototype.flat": "^1.2.4",
@@ -252,6 +255,7 @@
"ms": "^2.1.3",
"multicodec": "^3.0.1",
"multihashes": "^4.0.2",
"nock": "^13.3.3",
"node-vibrant": "^3.2.1-alpha.1",
"numbro": "^2.3.6",
"polished": "^3.3.2",
@@ -291,6 +295,7 @@
"workbox-navigation-preload": "^6.1.0",
"workbox-precaching": "^6.1.0",
"workbox-routing": "^6.1.0",
"xml2js": "^0.6.2",
"zustand": "^4.3.6"
},
"engines": {

View File

@@ -0,0 +1,15 @@
diff --git a/node_modules/@web3-react/gnosis-safe/dist/index.js b/node_modules/@web3-react/gnosis-safe/dist/index.js
index 015a33c..4cd7cde 100644
--- a/node_modules/@web3-react/gnosis-safe/dist/index.js
+++ b/node_modules/@web3-react/gnosis-safe/dist/index.js
@@ -68,8 +68,8 @@ class GnosisSafe extends types_1.Connector {
if (this.eagerConnection)
return;
// kick off import early to minimize waterfalls
- const SafeAppProviderPromise = Promise.resolve().then(() => __importStar(require('@safe-global/safe-apps-provider'))).then(({ SafeAppProvider }) => SafeAppProvider);
- yield (this.eagerConnection = Promise.resolve().then(() => __importStar(require('@safe-global/safe-apps-sdk'))).then((m) => __awaiter(this, void 0, void 0, function* () {
+ const SafeAppProviderPromise = Promise.resolve().then(async () => __importStar(await import('@safe-global/safe-apps-provider'))).then(({ SafeAppProvider }) => SafeAppProvider);
+ yield (this.eagerConnection = Promise.resolve().then(async () => __importStar(await import('@safe-global/safe-apps-sdk'))).then((m) => __awaiter(this, void 0, void 0, function* () {
this.sdk = new m.default(this.options);
const safe = yield Promise.race([
this.sdk.safe.getInfo(),

View File

@@ -3,27 +3,27 @@
"relation": ["delegate_permission/common.handle_all_urls"],
"target": {
"namespace": "android_app",
"package_name": "com.uniswap",
"package_name": "com.uniswap.mobile",
"sha256_cert_fingerprints":
["97:A5:81:51:DA:AF:8F:6E:65:3A:90:1E:82:12:6C:FB:61:2D:36:C7:CF:20:61:6B:A3:4C:52:CA:BC:58:43:8E", "F9:E9:E3:F0:04:28:66:62:81:44:50:7E:D6:A9:5F:B9:65:39:02:70:1D:13:74:15:D3:E1:A3:1B:D4:38:3A:1F"]
["49:D9:3D:5D:FB:AA:64:A4:64:80:85:0F:39:A8:C1:D9:25:D3:D4:BC:8E:6B:1F:45:0C:EA:AF:B1:0C:27:DF:B8", "F9:E9:E3:F0:04:28:66:62:81:44:50:7E:D6:A9:5F:B9:65:39:02:70:1D:13:74:15:D3:E1:A3:1B:D4:38:3A:1F"]
}
},
{
"relation": ["delegate_permission/common.handle_all_urls"],
"target": {
"namespace": "android_app",
"package_name": "com.uniswap.beta",
"package_name": "com.uniswap.mobile.beta",
"sha256_cert_fingerprints":
["E5:39:87:DC:4D:FD:4C:1B:A6:74:36:7D:3A:3B:6B:ED:9E:B3:66:89:92:8A:1B:B8:FC:1B:22:56:56:B4:46:A3", "54:4B:62:33:17:9B:5F:A8:E6:5D:D3:A6:E5:9D:80:5F:A5:02:7F:E2:14:B8:C1:7A:AC:4B:8D:E0:65:49:87:41"]
["75:41:9C:2D:01:4A:88:4E:8D:C6:EF:E5:51:54:28:6B:99:05:31:43:AD:84:B4:EB:39:28:B8:C3:C4:CE:48:E3", "54:4B:62:33:17:9B:5F:A8:E6:5D:D3:A6:E5:9D:80:5F:A5:02:7F:E2:14:B8:C1:7A:AC:4B:8D:E0:65:49:87:41"]
}
},
{
"relation": ["delegate_permission/common.handle_all_urls"],
"target": {
"namespace": "android_app",
"package_name": "com.uniswap.dev",
"package_name": "com.uniswap.mobile.dev",
"sha256_cert_fingerprints":
["5A:6D:23:50:2F:1E:0D:01:DC:96:65:F3:3A:18:4C:4C:8C:67:E0:09:99:9B:B1:9B:BF:44:99:D0:D1:D0:FC:5E", "02:E6:1C:76:8C:75:C3:78:C8:8C:FE:7B:2E:8F:4B:E1:FA:47:F2:F6:1A:DB:57:69:4A:41:99:C6:71:2C:AB:E3", "FA:C6:17:45:DC:09:03:78:6F:B9:ED:E6:2A:96:2B:39:9F:73:48:F0:BB:6F:89:9B:83:32:66:75:91:03:3B:9C"]
["45:F8:15:02:C5:4F:AD:82:E7:51:F0:9C:D1:CA:77:C8:C9:BF:06:A6:D9:5A:55:4F:9E:B8:5F:81:33:2B:D0:DB", "02:E6:1C:76:8C:75:C3:78:C8:8C:FE:7B:2E:8F:4B:E1:FA:47:F2:F6:1A:DB:57:69:4A:41:99:C6:71:2C:AB:E3", "FA:C6:17:45:DC:09:03:78:6F:B9:ED:E6:2A:96:2B:39:9F:73:48:F0:BB:6F:89:9B:83:32:66:75:91:03:3B:9C"]
}
}
]

View File

@@ -14,7 +14,7 @@
<link rel="apple-touch-icon" sizes="512x512" href="%PUBLIC_URL%/images/512x512_App_Icon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<meta name="theme-color" content="#FC72FF" />
<meta name="theme-color" content="#fff" />
<meta
http-equiv="Content-Security-Policy"
<% if (process.env.REACT_APP_CSP_ALLOW_UNSAFE_EVAL) { %>
@@ -36,7 +36,8 @@
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<link rel="preconnect" href="%REACT_APP_AMPLITUDE_PROXY_URL%" />
<link rel="preconnect" href="https://api.uniswap.org/" crossorigin/>
<link rel="preconnect" href="https://mainnet.infura.io/" crossorigin/>
<link rel="preload" href="%PUBLIC_URL%/fonts/Basel-Book.woff" as="font" type="font/woff" crossorigin />
<link rel="preload" href="%PUBLIC_URL%/fonts/Basel-Medium.woff" as="font" type="font/woff" crossorigin />

19
public/sitemap.xml Normal file
View File

@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.sitemaps.org/schemas/sitemap/0.9 http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd">
<url loc="https://app.uniswap.org/" lastmod="2023-10-05T17:48:32.538Z" changefreq="weekly" priority="1"/>
<url loc="https://app.uniswap.org/tokens" lastmod="2023-10-05T17:48:32.538Z" changefreq="weekly" priority="0.8"/>
<url loc="https://app.uniswap.org/send" lastmod="2023-10-05T17:48:32.538Z" changefreq="weekly" priority="0.6"/>
<url loc="https://app.uniswap.org/swap" lastmod="2023-10-05T17:48:32.538Z" changefreq="weekly" priority="0.9"/>
<url loc="https://app.uniswap.org/pool/v2/find" lastmod="2023-10-05T17:48:32.538Z" changefreq="weekly" priority="0.6"/>
<url loc="https://app.uniswap.org/pool/v2" lastmod="2023-10-05T17:48:32.538Z" changefreq="weekly" priority="0.6"/>
<url loc="https://app.uniswap.org/pool" lastmod="2023-10-05T17:48:32.538Z" changefreq="weekly" priority="0.6"/>
<url loc="https://app.uniswap.org/pools/v2/find" lastmod="2023-10-05T17:48:32.538Z" changefreq="weekly" priority="0.6"/>
<url loc="https://app.uniswap.org/pools/v2" lastmod="2023-10-05T17:48:32.538Z" changefreq="weekly" priority="0.6"/>
<url loc="https://app.uniswap.org/pools" lastmod="2023-10-05T17:48:32.538Z" changefreq="weekly" priority="0.7"/>
<url loc="https://app.uniswap.org/add/v2" lastmod="2023-10-05T17:48:32.538Z" changefreq="weekly" priority="0.6"/>
<url loc="https://app.uniswap.org/add" lastmod="2023-10-05T17:48:32.538Z" changefreq="weekly" priority="0.6"/>
<url loc="https://app.uniswap.org/increase" lastmod="2023-10-05T17:48:32.538Z" changefreq="weekly" priority="0.6"/>
<url loc="https://app.uniswap.org/migrate/v2" lastmod="2023-10-05T17:48:32.538Z" changefreq="weekly" priority="0.6"/>
<url loc="https://app.uniswap.org/nfts" lastmod="2023-10-05T17:48:32.538Z" changefreq="weekly" priority="0.6"/>
<url loc="https://app.uniswap.org/nfts/profile" lastmod="2023-10-05T17:48:32.538Z" changefreq="weekly" priority="0.6"/>
</urlset>

View File

@@ -0,0 +1,25 @@
/* eslint-env node */
const fs = require('fs')
const { parseStringPromise, Builder } = require('xml2js')
fs.readFile('./public/sitemap.xml', 'utf8', async (err, data) => {
try {
const sitemap = await parseStringPromise(data)
const lastmodDate = new Date().toISOString()
if (sitemap.urlset.url) {
sitemap.urlset.url.forEach((url) => {
url['$'].lastmod = lastmodDate
})
}
const builder = new Builder()
const xml = builder.buildObject(sitemap)
fs.writeFile('./public/sitemap.xml', xml, (error) => {
if (error) throw error
console.log('Sitemap updated')
})
} catch {
throw new Error('Error parsing sitemap.xml')
}
})

15
scripts/terser-loader.js Normal file
View File

@@ -0,0 +1,15 @@
/* eslint-env node */
const { minify } = require('terser')
module.exports = async function terserLoader(source, map, meta) {
const callback = this.async()
const options = this.getOptions()
try {
const data = await minify(source, options)
const { code } = data || {}
callback(null, code, map, meta)
} catch (e) {
callback(e)
}
}

View File

@@ -52,3 +52,8 @@ export const sendAnalyticsEvent: typeof sendAnalyticsTraceEvent = (event, proper
sendAnalyticsTraceEvent(event, properties)
}
}
// This is only used for initial page load so we can get the user's country
export const sendInitializationEvent: typeof sendAnalyticsTraceEvent = (event, properties) => {
sendAnalyticsTraceEvent(event, properties)
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 3.3 KiB

View File

@@ -1,4 +1,15 @@
<svg width="1024" height="1024" viewBox="0 0 1024 1024" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="512" cy="512" r="512" fill="#8247E5"/>
<path d="M681.469 402.456C669.189 395.312 653.224 395.312 639.716 402.456L543.928 457.228L478.842 492.949L383.055 547.721C370.774 554.865 354.81 554.865 341.301 547.721L265.162 504.856C252.882 497.712 244.286 484.614 244.286 470.325V385.786C244.286 371.498 251.654 358.4 265.162 351.256L340.073 309.581C352.353 302.437 368.318 302.437 381.827 309.581L456.737 351.256C469.018 358.4 477.614 371.498 477.614 385.786V440.558L542.7 403.646V348.874C542.7 334.586 535.332 321.488 521.824 314.344L383.055 235.758C370.774 228.614 354.81 228.614 341.301 235.758L200.076 314.344C186.567 321.488 179.199 334.586 179.199 348.874V507.237C179.199 521.525 186.567 534.623 200.076 541.767L341.301 620.353C353.582 627.498 369.546 627.498 383.055 620.353L478.842 566.772L543.928 529.86L639.716 476.279C651.996 469.135 667.961 469.135 681.469 476.279L756.38 517.953C768.66 525.098 777.257 538.195 777.257 552.484V637.023C777.257 651.312 769.888 664.409 756.38 671.553L681.469 714.419C669.189 721.563 653.224 721.563 639.716 714.419L564.805 672.744C552.525 665.6 543.928 652.502 543.928 638.214V583.442L478.842 620.353V675.125C478.842 689.414 486.21 702.512 499.719 709.656L640.944 788.242C653.224 795.386 669.189 795.386 682.697 788.242L823.922 709.656C836.203 702.512 844.799 689.414 844.799 675.125V516.763C844.799 502.474 837.431 489.377 823.922 482.232L681.469 402.456Z" fill="white"/>
<svg width="490" height="490" viewBox="0 0 490 490" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_7383_35741)">
<circle cx="245" cy="245" r="245" fill="url(#paint0_linear_7383_35741)"/>
<path d="M315.83 297.85L385.12 257.84C388.79 255.72 391.06 251.78 391.06 247.54V167.53C391.06 163.3 388.78 159.35 385.12 157.23L315.83 117.22C312.16 115.1 307.61 115.11 303.94 117.22L234.65 157.23C230.98 159.35 228.71 163.3 228.71 167.53V310.52L180.12 338.57L131.53 310.52V254.41L180.12 226.36L212.17 244.86V207.22L186.06 192.15C184.26 191.11 182.2 190.56 180.11 190.56C178.02 190.56 175.96 191.11 174.17 192.15L104.88 232.16C101.21 234.28 98.9404 238.22 98.9404 242.46V322.47C98.9404 326.7 101.22 330.65 104.88 332.77L174.17 372.78C177.83 374.89 182.39 374.89 186.06 372.78L255.35 332.78C259.02 330.66 261.29 326.71 261.29 322.48V179.49L262.17 178.99L309.88 151.44L358.47 179.49V235.6L309.88 263.65L277.88 245.17V282.81L303.94 297.86C307.61 299.97 312.16 299.97 315.83 297.86V297.85Z" fill="white"/>
</g>
<defs>
<linearGradient id="paint0_linear_7383_35741" x1="-175" y1="4.36391e-07" x2="416" y2="367" gradientUnits="userSpaceOnUse">
<stop stop-color="#A229C5"/>
<stop offset="1" stop-color="#7B3FE4"/>
</linearGradient>
<clipPath id="clip0_7383_35741">
<rect width="490" height="490" fill="white"/>
</clipPath>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -1,16 +1 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 24.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 38.4 33.5" style="enable-background:new 0 0 38.4 33.5;" xml:space="preserve">
<style type="text/css">
.st0{fill:#8247E5;}
</style>
<g>
<path class="st0" d="M29,10.2c-0.7-0.4-1.6-0.4-2.4,0L21,13.5l-3.8,2.1l-5.5,3.3c-0.7,0.4-1.6,0.4-2.4,0L5,16.3
c-0.7-0.4-1.2-1.2-1.2-2.1v-5c0-0.8,0.4-1.6,1.2-2.1l4.3-2.5c0.7-0.4,1.6-0.4,2.4,0L16,7.2c0.7,0.4,1.2,1.2,1.2,2.1v3.3l3.8-2.2V7
c0-0.8-0.4-1.6-1.2-2.1l-8-4.7c-0.7-0.4-1.6-0.4-2.4,0L1.2,5C0.4,5.4,0,6.2,0,7v9.4c0,0.8,0.4,1.6,1.2,2.1l8.1,4.7
c0.7,0.4,1.6,0.4,2.4,0l5.5-3.2l3.8-2.2l5.5-3.2c0.7-0.4,1.6-0.4,2.4,0l4.3,2.5c0.7,0.4,1.2,1.2,1.2,2.1v5c0,0.8-0.4,1.6-1.2,2.1
L29,28.8c-0.7,0.4-1.6,0.4-2.4,0l-4.3-2.5c-0.7-0.4-1.2-1.2-1.2-2.1V21l-3.8,2.2v3.3c0,0.8,0.4,1.6,1.2,2.1l8.1,4.7
c0.7,0.4,1.6,0.4,2.4,0l8.1-4.7c0.7-0.4,1.2-1.2,1.2-2.1V17c0-0.8-0.4-1.6-1.2-2.1L29,10.2z"/>
</g>
</svg>
<?xml version="1.0" encoding="UTF-8"?><svg id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 500 500"><defs><style>.cls-1{fill:url(#linear-gradient);}</style><linearGradient id="linear-gradient" x1="54.83" y1="392.31" x2="459.03" y2="97.58" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#a726c1"/><stop offset=".88" stop-color="#803bdf"/><stop offset="1" stop-color="#7b3fe4"/></linearGradient></defs><path class="cls-1" d="m364.03,335.08l111.55-64.4c5.9-3.41,9.57-9.76,9.57-16.58V125.28c0-6.81-3.67-13.17-9.57-16.58l-111.55-64.4c-5.9-3.41-13.24-3.4-19.14,0l-111.55,64.4c-5.9,3.41-9.57,9.76-9.57,16.58v230.19l-78.22,45.15-78.22-45.15v-90.33l78.22-45.15,51.6,29.78v-60.59l-42.03-24.26c-2.9-1.67-6.21-2.55-9.57-2.55s-6.67.88-9.57,2.55L24.42,229.33c-5.9,3.41-9.57,9.76-9.57,16.58v128.81c0,6.81,3.67,13.17,9.57,16.58l111.55,64.41c5.9,3.4,13.23,3.4,19.14,0l111.55-64.4c5.9-3.41,9.57-9.77,9.57-16.58v-230.19l1.41-.81,76.81-44.34,78.22,45.16v90.32l-78.22,45.16-51.52-29.74v60.59l41.95,24.23c5.9,3.4,13.24,3.4,19.14,0Z"/></svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -2,7 +2,8 @@ import { BrowserEvent, InterfaceElementName, SharedEventName } from '@uniswap/an
import { TraceEvent } from 'analytics'
import { useDisableNFTRoutes } from 'hooks/useDisableNFTRoutes'
import styled from 'styled-components'
import { BREAKPOINTS, ExternalLink, StyledRouterLink } from 'theme'
import { BREAKPOINTS } from 'theme'
import { ExternalLink, StyledRouterLink } from 'theme/components'
import { useIsDarkMode } from 'theme/components/ThemeToggle'
import { DiscordIcon, GithubIcon, TwitterIcon } from './Icons'

View File

@@ -9,11 +9,12 @@ import { Power } from 'components/Icons/Power'
import { Settings } from 'components/Icons/Settings'
import { AutoRow } from 'components/Row'
import { LoadingBubble } from 'components/Tokens/loading'
import { DeltaArrow, formatDelta } from 'components/Tokens/TokenDetails/Delta'
import { DeltaArrow } from 'components/Tokens/TokenDetails/Delta'
import Tooltip from 'components/Tooltip'
import { getConnection } from 'connection'
import { useDisableNFTRoutes } from 'hooks/useDisableNFTRoutes'
import useENSName from 'hooks/useENSName'
import { useIsNotOriginCountry } from 'hooks/useIsNotOriginCountry'
import { useProfilePageState, useSellAsset, useWalletCollections } from 'nft/hooks'
import { useIsNftClaimAvailable } from 'nft/hooks/useIsNftClaimAvailable'
import { ProfilePageStateType } from 'nft/types'
@@ -23,7 +24,7 @@ import { useNavigate } from 'react-router-dom'
import { useAppDispatch } from 'state/hooks'
import { updateSelectedWallet } from 'state/user/reducer'
import styled from 'styled-components'
import { CopyHelper, ExternalLink, ThemedText } from 'theme'
import { CopyHelper, ExternalLink, ThemedText } from 'theme/components'
import { shortenAddress } from 'utils'
import { NumberType, useFormatter } from 'utils/formatNumbers'
@@ -31,11 +32,11 @@ import { useCloseModal, useFiatOnrampAvailability, useOpenModal, useToggleModal
import { ApplicationModal } from '../../state/application/reducer'
import { useUserHasAvailableClaim, useUserUnclaimedAmount } from '../../state/claim/hooks'
import StatusIcon from '../Identicon/StatusIcon'
import { useCachedPortfolioBalancesQuery } from '../PrefetchBalancesWrapper/PrefetchBalancesWrapper'
import { useToggleAccountDrawer } from '.'
import IconButton, { IconHoverText, IconWithConfirmTextButton } from './IconButton'
import MiniPortfolio from './MiniPortfolio'
import { portfolioFadeInAnimation } from './MiniPortfolio/PortfolioRow'
import { useCachedPortfolioBalancesQuery } from './PrefetchBalancesWrapper'
const AuthenticatedHeaderWrapper = styled.div`
padding: 20px 16px;
@@ -159,7 +160,8 @@ export default function AuthenticatedHeader({ account, openSettings }: { account
const resetSellAssets = useSellAsset((state) => state.reset)
const clearCollectionFilters = useWalletCollections((state) => state.clearCollectionFilters)
const isClaimAvailable = useIsNftClaimAvailable((state) => state.isClaimAvailable)
const { formatNumber } = useFormatter()
const shouldShowBuyFiatButton = useIsNotOriginCountry('GB')
const { formatNumber, formatPercent } = useFormatter()
const shouldDisableNFTRoutes = useDisableNFTRoutes()
@@ -282,7 +284,7 @@ export default function AuthenticatedHeader({ account, openSettings }: { account
{`${formatNumber({
input: Math.abs(absoluteChange as number),
type: NumberType.PortfolioBalance,
})} (${formatDelta(percentChange)})`}
})} (${formatPercent(percentChange)})`}
</ThemedText.BodySecondary>
</>
)}
@@ -304,26 +306,28 @@ export default function AuthenticatedHeader({ account, openSettings }: { account
<Trans>View and sell NFTs</Trans>
</HeaderButton>
)}
<HeaderButton
size={ButtonSize.medium}
emphasis={ButtonEmphasis.highSoft}
onClick={handleBuyCryptoClick}
disabled={disableBuyCryptoButton}
data-testid="wallet-buy-crypto"
>
{error ? (
<ThemedText.BodyPrimary>{error}</ThemedText.BodyPrimary>
) : (
<>
{fiatOnrampAvailabilityLoading ? (
<StyledLoadingButtonSpinner />
) : (
<CreditCard height="20px" width="20px" />
)}{' '}
<Trans>Buy crypto</Trans>
</>
)}
</HeaderButton>
{shouldShowBuyFiatButton && (
<HeaderButton
size={ButtonSize.medium}
emphasis={ButtonEmphasis.highSoft}
onClick={handleBuyCryptoClick}
disabled={disableBuyCryptoButton}
data-testid="wallet-buy-crypto"
>
{error ? (
<ThemedText.BodyPrimary>{error}</ThemedText.BodyPrimary>
) : (
<>
{fiatOnrampAvailabilityLoading ? (
<StyledLoadingButtonSpinner />
) : (
<CreditCard height="20px" width="20px" />
)}{' '}
<Trans>Buy crypto</Trans>
</>
)}
</HeaderButton>
)}
{Boolean(!fiatOnrampAvailable && fiatOnrampAvailabilityChecked) && (
<FiatOnrampNotAvailableText marginTop="8px">
<Trans>Not available in your region</Trans>

View File

@@ -1,7 +1,7 @@
import { InterfaceElementName } from '@uniswap/analytics-events'
import { PropsWithChildren, useCallback } from 'react'
import styled from 'styled-components'
import { ClickableStyle } from 'theme'
import { ClickableStyle } from 'theme/components'
import { openDownloadApp } from 'utils/openDownloadApp'
const StyledButton = styled.button<{ padded?: boolean; branded?: boolean }>`

View File

@@ -2,7 +2,7 @@ import { Trans } from '@lingui/macro'
import Tooltip from 'components/Tooltip'
import useCopyClipboard from 'hooks/useCopyClipboard'
import styled from 'styled-components'
import { ThemedText } from 'theme'
import { ThemedText } from 'theme/components'
const Container = styled.div`
width: 100%;

View File

@@ -8,7 +8,7 @@ import { TransactionStatus } from 'graphql/data/__generated__/types-and-hooks'
import useENSName from 'hooks/useENSName'
import { useCallback } from 'react'
import styled from 'styled-components'
import { EllipsisStyle, ThemedText } from 'theme'
import { EllipsisStyle, ThemedText } from 'theme/components'
import { shortenAddress } from 'utils'
import { ExplorerDataType, getExplorerLink } from 'utils/getExplorerLink'

View File

@@ -17,7 +17,7 @@ import { InterfaceTrade } from 'state/routing/types'
import { useOrder } from 'state/signatures/hooks'
import { UniswapXOrderDetails } from 'state/signatures/types'
import styled from 'styled-components'
import { ExternalLink, ThemedText } from 'theme'
import { ExternalLink, ThemedText } from 'theme/components'
import { ExplorerDataType, getExplorerLink } from 'utils/getExplorerLink'
type SelectedOrderInfo = {

View File

@@ -0,0 +1,374 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`parseRemote parseRemoteActivities should parse NFT approval 1`] = `
Object {
"chainId": 1,
"descriptor": "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045",
"from": "0x50EC05ADe8280758E2077fcBC08D878D4aef79C3",
"hash": "someHash",
"logos": Array [],
"nonce": 12345,
"status": "CONFIRMED",
"timestamp": 10000,
"title": "Unknown Approval",
}
`;
exports[`parseRemote parseRemoteActivities should parse NFT approval for all 1`] = `
Object {
"chainId": 1,
"descriptor": "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045",
"from": "0x50EC05ADe8280758E2077fcBC08D878D4aef79C3",
"hash": "someHash",
"logos": Array [],
"nonce": 12345,
"status": "CONFIRMED",
"timestamp": 10000,
"title": "Unknown Approval",
}
`;
exports[`parseRemote parseRemoteActivities should parse NFT receive 1`] = `
Object {
"chainId": 1,
"currencies": undefined,
"descriptor": "1 SomeCollectionName from ",
"from": "0x50EC05ADe8280758E2077fcBC08D878D4aef79C3",
"hash": "someHash",
"logos": Array [
"imageUrl",
],
"nonce": 12345,
"otherAccount": "0x50EC05ADe8280758E2077fcBC08D878D4aef79C3",
"status": "CONFIRMED",
"timestamp": 10000,
"title": "Received",
}
`;
exports[`parseRemote parseRemoteActivities should parse NFT transfer 1`] = `
Object {
"chainId": 1,
"descriptor": "1 SomeCollectionName",
"from": "0x50EC05ADe8280758E2077fcBC08D878D4aef79C3",
"hash": "someHash",
"logos": Array [
"imageUrl",
],
"nonce": 12345,
"status": "CONFIRMED",
"timestamp": 10000,
"title": "Minted",
}
`;
exports[`parseRemote parseRemoteActivities should parse closed UniswapX order 1`] = `
Object {
"chainId": 1,
"currencies": Array [
Token {
"address": "0x6B175474E89094C44Da98b954EedeAC495271d0F",
"chainId": 1,
"decimals": 18,
"isNative": false,
"isToken": true,
"name": "DAI",
"symbol": "DAI",
},
Token {
"address": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
"chainId": 1,
"decimals": 18,
"isNative": false,
"isToken": true,
"name": "Wrapped Ether",
"symbol": "WETH",
},
],
"descriptor": "100 DAI for 200 WETH",
"from": "someOfferer",
"hash": "someHash",
"logos": Array [
"someUrl",
"someUrl",
],
"offchainOrderStatus": "expired",
"prefixIconSrc": "bolt.svg",
"status": "FAILED",
"statusMessage": "Your swap could not be fulfilled at this time. Please try again.",
"timestamp": 10000,
"title": "Swap expired",
}
`;
exports[`parseRemote parseRemoteActivities should parse eth wrap 1`] = `
Object {
"chainId": 1,
"currencies": Array [
ExtendedEther {
"chainId": 1,
"decimals": 18,
"isNative": true,
"isToken": false,
"name": "Ether",
"symbol": "ETH",
},
Token {
"address": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
"chainId": 1,
"decimals": 18,
"isNative": false,
"isToken": true,
"name": "Wrapped Ether",
"symbol": "WETH",
},
],
"descriptor": "100 ETH for 100 WETH",
"from": "0x50EC05ADe8280758E2077fcBC08D878D4aef79C3",
"hash": "someHash",
"logos": Array [
"https://token-icons.s3.amazonaws.com/eth.png",
"https://raw.githubusercontent.com/Uniswap/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png",
],
"nonce": 12345,
"status": "CONFIRMED",
"timestamp": 10000,
"title": "Wrapped",
}
`;
exports[`parseRemote parseRemoteActivities should parse moonpay purchase 1`] = `
Object {
"chainId": 1,
"currencies": Array [
Token {
"address": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
"chainId": 1,
"decimals": 18,
"isNative": false,
"isToken": true,
"name": "Wrapped Ether",
"symbol": "WETH",
},
],
"descriptor": "100 WETH for 100",
"from": "0x50EC05ADe8280758E2077fcBC08D878D4aef79C3",
"hash": "someHash",
"logos": Array [
"moonpay.svg",
],
"nonce": 12345,
"status": "CONFIRMED",
"timestamp": 10000,
"title": "Purchased",
}
`;
exports[`parseRemote parseRemoteActivities should parse nft purchase 1`] = `
Object {
"chainId": 1,
"descriptor": "1 SomeCollectionName",
"from": "0x50EC05ADe8280758E2077fcBC08D878D4aef79C3",
"hash": "someHash",
"logos": Array [
"imageUrl",
],
"nonce": 12345,
"status": "CONFIRMED",
"timestamp": 10000,
"title": "Bought",
}
`;
exports[`parseRemote parseRemoteActivities should parse receive 1`] = `
Object {
"chainId": 1,
"currencies": Array [
Token {
"address": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
"chainId": 1,
"decimals": 18,
"isNative": false,
"isToken": true,
"name": "Wrapped Ether",
"symbol": "WETH",
},
],
"descriptor": "100 WETH from ",
"from": "0x50EC05ADe8280758E2077fcBC08D878D4aef79C3",
"hash": "someHash",
"logos": Array [
"logoUrl",
],
"nonce": 12345,
"otherAccount": "0x50EC05ADe8280758E2077fcBC08D878D4aef79C3",
"status": "CONFIRMED",
"timestamp": 10000,
"title": "Received",
}
`;
exports[`parseRemote parseRemoteActivities should parse remove liquidity 1`] = `
Object {
"chainId": 1,
"currencies": Array [
Token {
"address": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
"chainId": 1,
"decimals": 18,
"isNative": false,
"isToken": true,
"name": "Wrapped Ether",
"symbol": "WETH",
},
Token {
"address": "0x6B175474E89094C44Da98b954EedeAC495271d0F",
"chainId": 1,
"decimals": 18,
"isNative": false,
"isToken": true,
"name": "DAI",
"symbol": "DAI",
},
],
"descriptor": "100 WETH and 100 DAI",
"from": "0x50EC05ADe8280758E2077fcBC08D878D4aef79C3",
"hash": "someHash",
"logos": Array [
"logoUrl",
"logoUrl",
],
"nonce": 12345,
"status": "CONFIRMED",
"timestamp": 10000,
"title": "Removed Liquidity",
}
`;
exports[`parseRemote parseRemoteActivities should parse send 1`] = `
Object {
"chainId": 1,
"currencies": Array [
Token {
"address": "0x6B175474E89094C44Da98b954EedeAC495271d0F",
"chainId": 1,
"decimals": 18,
"isNative": false,
"isToken": true,
"name": "DAI",
"symbol": "DAI",
},
],
"descriptor": "100 DAI to ",
"from": "0x50EC05ADe8280758E2077fcBC08D878D4aef79C3",
"hash": "someHash",
"logos": Array [
"logoUrl",
],
"nonce": 12345,
"otherAccount": "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045",
"status": "CONFIRMED",
"timestamp": 10000,
"title": "Sent",
}
`;
exports[`parseRemote parseRemoteActivities should parse swap 1`] = `
Object {
"chainId": 1,
"currencies": Array [
Token {
"address": "0x6B175474E89094C44Da98b954EedeAC495271d0F",
"chainId": 1,
"decimals": 18,
"isNative": false,
"isToken": true,
"name": "DAI",
"symbol": "DAI",
},
Token {
"address": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
"chainId": 1,
"decimals": 18,
"isNative": false,
"isToken": true,
"name": "Wrapped Ether",
"symbol": "WETH",
},
],
"descriptor": "100 DAI for 100 WETH",
"from": "0x50EC05ADe8280758E2077fcBC08D878D4aef79C3",
"hash": "someHash",
"logos": Array [
"logoUrl",
],
"nonce": 12345,
"status": "CONFIRMED",
"timestamp": 10000,
"title": "Swapped",
}
`;
exports[`parseRemote parseRemoteActivities should parse swap order 1`] = `
Object {
"chainId": 1,
"currencies": Array [
Token {
"address": "0x6B175474E89094C44Da98b954EedeAC495271d0F",
"chainId": 1,
"decimals": 18,
"isNative": false,
"isToken": true,
"name": "DAI",
"symbol": "DAI",
},
Token {
"address": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
"chainId": 1,
"decimals": 18,
"isNative": false,
"isToken": true,
"name": "Wrapped Ether",
"symbol": "WETH",
},
],
"descriptor": "100 DAI for 100 WETH",
"from": "0x50EC05ADe8280758E2077fcBC08D878D4aef79C3",
"hash": "someHash",
"logos": Array [
"logoUrl",
],
"nonce": 12345,
"prefixIconSrc": "bolt.svg",
"status": "CONFIRMED",
"timestamp": 10000,
"title": "Swapped",
}
`;
exports[`parseRemote parseRemoteActivities should parse token approval 1`] = `
Object {
"chainId": 1,
"currencies": Array [
Token {
"address": "0x6B175474E89094C44Da98b954EedeAC495271d0F",
"chainId": 1,
"decimals": 18,
"isNative": false,
"isToken": true,
"name": "DAI",
"symbol": "DAI",
},
],
"descriptor": "DAI",
"from": "0x50EC05ADe8280758E2077fcBC08D878D4aef79C3",
"hash": "someHash",
"logos": Array [
"logoUrl",
],
"nonce": 12345,
"status": "CONFIRMED",
"timestamp": 10000,
"title": "Approved",
}
`;

View File

@@ -0,0 +1,522 @@
import { ChainId, NONFUNGIBLE_POSITION_MANAGER_ADDRESSES, WETH9 } from '@uniswap/sdk-core'
import { DAI } from 'constants/tokens'
import {
AssetActivityPartsFragment,
Chain,
Currency,
NftStandard,
SwapOrderStatus,
TokenStandard,
TransactionDirection,
TransactionStatus,
TransactionType,
} from 'graphql/data/__generated__/types-and-hooks'
import { MOONPAY_SENDER_ADDRESSES } from '../../constants'
const MockOrderTimestamp = 10000
const MockRecipientAddress = '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045'
const MockSenderAddress = '0x50EC05ADe8280758E2077fcBC08D878D4aef79C3'
const mockAssetActivityPartsFragment = {
__typename: 'AssetActivity',
id: 'activityId',
timestamp: MockOrderTimestamp,
chain: Chain.Ethereum,
details: {
__typename: 'SwapOrderDetails',
id: 'detailsId',
offerer: 'offererId',
hash: 'someHash',
inputTokenQuantity: '100',
outputTokenQuantity: '200',
orderStatus: SwapOrderStatus.Open,
inputToken: {
__typename: 'Token',
id: 'tokenId',
chain: Chain.Ethereum,
standard: TokenStandard.Erc20,
},
outputToken: {
__typename: 'Token',
id: 'tokenId',
chain: Chain.Ethereum,
standard: TokenStandard.Erc20,
},
},
}
const mockSwapOrderDetailsPartsFragment = {
__typename: 'SwapOrderDetails',
id: 'someId',
offerer: 'someOfferer',
hash: 'someHash',
inputTokenQuantity: '100',
outputTokenQuantity: '200',
orderStatus: SwapOrderStatus.Open,
inputToken: {
__typename: 'Token',
id: DAI.address,
name: 'DAI',
symbol: DAI.symbol,
address: DAI.address,
decimals: 18,
chain: Chain.Ethereum,
standard: TokenStandard.Erc20,
project: {
__typename: 'TokenProject',
id: 'projectId',
isSpam: false,
logo: {
__typename: 'Image',
id: 'imageId',
url: 'someUrl',
},
},
},
outputToken: {
__typename: 'Token',
id: WETH9[1].address,
name: 'Wrapped Ether',
symbol: 'WETH',
address: WETH9[1].address,
decimals: 18,
chain: Chain.Ethereum,
standard: TokenStandard.Erc20,
project: {
__typename: 'TokenProject',
id: 'projectId',
isSpam: false,
logo: {
__typename: 'Image',
id: 'imageId',
url: 'someUrl',
},
},
},
}
const mockNftApprovalPartsFragment = {
__typename: 'NftApproval',
id: 'approvalId',
nftStandard: NftStandard.Erc721, // Replace with actual enum value
approvedAddress: '0xApprovedAddress',
asset: {
__typename: 'NftAsset',
id: 'assetId',
name: 'SomeNftName',
tokenId: 'tokenId123',
nftContract: {
__typename: 'NftContract',
id: 'nftContractId',
chain: Chain.Ethereum, // Replace with actual enum value
address: '0xContractAddress',
},
image: {
__typename: 'Image',
id: 'imageId',
url: 'imageUrl',
},
collection: {
__typename: 'NftCollection',
id: 'collectionId',
name: 'SomeCollectionName',
},
},
}
const mockNftApproveForAllPartsFragment = {
__typename: 'NftApproveForAll',
id: 'approveForAllId',
nftStandard: NftStandard.Erc721, // Replace with actual enum value
operatorAddress: '0xOperatorAddress',
approved: true,
asset: {
__typename: 'NftAsset',
id: 'assetId',
name: 'SomeNftName',
tokenId: 'tokenId123',
nftContract: {
__typename: 'NftContract',
id: 'nftContractId',
chain: Chain.Ethereum, // Replace with actual enum value
address: '0xContractAddress',
},
image: {
__typename: 'Image',
id: 'imageId',
url: 'imageUrl',
},
collection: {
__typename: 'NftCollection',
id: 'collectionId',
name: 'SomeCollectionName',
},
},
}
const mockNftTransferPartsFragment = {
__typename: 'NftTransfer',
id: 'transferId',
nftStandard: NftStandard.Erc721,
sender: MockSenderAddress,
recipient: MockRecipientAddress,
direction: TransactionDirection.Out,
asset: {
__typename: 'NftAsset',
id: 'assetId',
name: 'SomeNftName',
tokenId: 'tokenId123',
nftContract: {
__typename: 'NftContract',
id: 'nftContractId',
chain: Chain.Ethereum,
address: '0xContractAddress',
},
image: {
__typename: 'Image',
id: 'imageId',
url: 'imageUrl',
},
collection: {
__typename: 'NftCollection',
id: 'collectionId',
name: 'SomeCollectionName',
},
},
}
const mockTokenTransferOutPartsFragment = {
__typename: 'TokenTransfer',
id: 'tokenTransferId',
tokenStandard: TokenStandard.Erc20,
quantity: '100',
sender: MockSenderAddress,
recipient: MockRecipientAddress,
direction: TransactionDirection.Out,
asset: {
__typename: 'Token',
id: DAI.address,
name: 'DAI',
symbol: 'DAI',
address: DAI.address,
decimals: 18,
chain: Chain.Ethereum,
standard: TokenStandard.Erc20,
project: {
__typename: 'TokenProject',
id: 'projectId',
isSpam: false,
logo: {
__typename: 'Image',
id: 'logoId',
url: 'logoUrl',
},
},
},
transactedValue: {
__typename: 'Amount',
id: 'amountId',
currency: Currency.Usd,
value: 100,
},
}
const mockNativeTokenTransferOutPartsFragment = {
__typename: 'TokenTransfer',
id: 'tokenTransferId',
asset: {
__typename: 'Token',
id: 'ETH',
name: 'Ether',
symbol: 'ETH',
address: null,
decimals: 18,
chain: 'ETHEREUM',
standard: null,
project: {
__typename: 'TokenProject',
id: 'Ethereum',
isSpam: false,
logo: {
__typename: 'Image',
id: 'ETH_logo',
url: 'https://token-icons.s3.amazonaws.com/eth.png',
},
},
},
tokenStandard: 'NATIVE',
quantity: '0.25',
sender: MockSenderAddress,
recipient: MockRecipientAddress,
direction: 'OUT',
transactedValue: {
__typename: 'Amount',
id: 'ETH_amount',
currency: 'USD',
value: 399.0225,
},
}
const mockWrappedEthTransferInPartsFragment = {
__typename: 'TokenTransfer',
id: 'tokenTransferId',
asset: {
__typename: 'Token',
id: 'WETH',
name: 'Wrapped Ether',
symbol: 'WETH',
address: '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2',
decimals: 18,
chain: 'ETHEREUM',
standard: 'ERC20',
project: {
__typename: 'TokenProject',
id: 'weth_project_id',
isSpam: false,
logo: {
__typename: 'Image',
id: 'weth_image',
url: 'https://raw.githubusercontent.com/Uniswap/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png',
},
},
},
tokenStandard: 'ERC20',
quantity: '0.25',
sender: MockSenderAddress,
recipient: MockRecipientAddress,
direction: 'IN',
transactedValue: {
__typename: 'Amount',
id: 'mockWethAmountId',
currency: 'USD',
value: 399.1334007875,
},
}
const mockTokenTransferInPartsFragment = {
__typename: 'TokenTransfer',
id: 'tokenTransferId',
tokenStandard: TokenStandard.Erc20,
quantity: '1',
sender: MockSenderAddress,
recipient: MockRecipientAddress,
direction: TransactionDirection.In,
asset: {
__typename: 'Token',
id: WETH9[1].address,
name: 'Wrapped Ether',
symbol: 'WETH',
address: WETH9[1].address,
decimals: 18,
chain: Chain.Ethereum,
standard: TokenStandard.Erc20,
project: {
__typename: 'TokenProject',
id: 'projectId',
isSpam: false,
logo: {
__typename: 'Image',
id: 'logoId',
url: 'logoUrl',
},
},
},
transactedValue: {
__typename: 'Amount',
id: 'amountId',
currency: Currency.Usd,
value: 100,
},
}
const mockTokenApprovalPartsFragment = {
__typename: 'TokenApproval',
id: 'tokenApprovalId',
tokenStandard: TokenStandard.Erc20,
approvedAddress: DAI.address,
quantity: '50',
asset: {
__typename: 'Token',
id: 'tokenId',
name: 'DAI',
symbol: 'DAI',
address: DAI.address,
decimals: 18,
chain: Chain.Ethereum,
standard: TokenStandard.Erc20,
project: {
__typename: 'TokenProject',
id: 'projectId',
isSpam: false,
logo: {
__typename: 'Image',
id: 'logoId',
url: 'logoUrl',
},
},
},
}
export const MockOpenUniswapXOrder = {
...mockAssetActivityPartsFragment,
details: mockSwapOrderDetailsPartsFragment,
} as AssetActivityPartsFragment
export const MockClosedUniswapXOrder = {
...mockAssetActivityPartsFragment,
details: {
...mockSwapOrderDetailsPartsFragment,
orderStatus: SwapOrderStatus.Expired,
},
} as AssetActivityPartsFragment
const commonTransactionDetailsFields = {
__typename: 'TransactionDetails',
from: MockSenderAddress,
hash: 'someHash',
id: 'transactionId',
nonce: 12345,
status: TransactionStatus.Confirmed,
to: MockRecipientAddress,
}
export const MockNFTApproval = {
...mockAssetActivityPartsFragment,
details: {
...commonTransactionDetailsFields,
type: TransactionType.Approve,
assetChanges: [mockNftApprovalPartsFragment],
},
} as AssetActivityPartsFragment
export const MockNFTApprovalForAll = {
...mockAssetActivityPartsFragment,
details: {
...commonTransactionDetailsFields,
type: TransactionType.Approve,
assetChanges: [mockNftApproveForAllPartsFragment],
},
} as AssetActivityPartsFragment
export const MockNFTTransfer = {
...mockAssetActivityPartsFragment,
details: {
...commonTransactionDetailsFields,
type: TransactionType.Mint,
assetChanges: [mockNftTransferPartsFragment],
},
} as AssetActivityPartsFragment
export const MockTokenTransfer = {
...mockAssetActivityPartsFragment,
details: {
...commonTransactionDetailsFields,
type: TransactionType.Swap,
assetChanges: [mockTokenTransferOutPartsFragment, mockTokenTransferInPartsFragment],
},
} as AssetActivityPartsFragment
export const MockSwapOrder = {
...mockAssetActivityPartsFragment,
details: {
...commonTransactionDetailsFields,
type: TransactionType.SwapOrder,
assetChanges: [mockTokenTransferOutPartsFragment, mockTokenTransferInPartsFragment],
},
} as AssetActivityPartsFragment
export const MockTokenApproval = {
...mockAssetActivityPartsFragment,
details: {
...commonTransactionDetailsFields,
type: TransactionType.Approve,
assetChanges: [mockTokenApprovalPartsFragment],
},
} as AssetActivityPartsFragment
export const MockTokenSend = {
...mockAssetActivityPartsFragment,
details: {
...commonTransactionDetailsFields,
type: TransactionType.Send,
assetChanges: [mockTokenTransferOutPartsFragment],
},
} as AssetActivityPartsFragment
export const MockTokenReceive = {
...mockAssetActivityPartsFragment,
details: {
...commonTransactionDetailsFields,
type: TransactionType.Receive,
assetChanges: [mockTokenTransferInPartsFragment],
},
} as AssetActivityPartsFragment
export const MockRemoveLiquidity = {
...mockAssetActivityPartsFragment,
details: {
...commonTransactionDetailsFields,
to: NONFUNGIBLE_POSITION_MANAGER_ADDRESSES[ChainId.MAINNET],
type: TransactionType.Receive,
assetChanges: [
mockTokenTransferInPartsFragment,
{
...mockTokenTransferOutPartsFragment,
direction: TransactionDirection.In,
},
],
},
} as AssetActivityPartsFragment
export const MockMoonpayPurchase = {
...mockAssetActivityPartsFragment,
details: {
...commonTransactionDetailsFields,
type: TransactionType.Receive,
assetChanges: [
{
...mockTokenTransferInPartsFragment,
sender: MOONPAY_SENDER_ADDRESSES[0],
},
],
},
} as AssetActivityPartsFragment
export const MockNFTReceive = {
...mockAssetActivityPartsFragment,
details: {
...commonTransactionDetailsFields,
type: TransactionType.Receive,
assetChanges: [
{
...mockNftTransferPartsFragment,
direction: TransactionDirection.In,
},
],
},
} as AssetActivityPartsFragment
export const MockNFTPurchase = {
...mockAssetActivityPartsFragment,
details: {
...commonTransactionDetailsFields,
type: TransactionType.Swap,
assetChanges: [
mockTokenTransferOutPartsFragment,
{
...mockNftTransferPartsFragment,
direction: TransactionDirection.In,
},
],
},
} as AssetActivityPartsFragment
export const MockWrap = {
...mockAssetActivityPartsFragment,
details: {
...commonTransactionDetailsFields,
type: TransactionType.Lend,
assetChanges: [mockNativeTokenTransferOutPartsFragment, mockWrappedEthTransferInPartsFragment],
},
} as AssetActivityPartsFragment

View File

@@ -2,6 +2,7 @@ import { TransactionStatus, useActivityQuery } from 'graphql/data/__generated__/
import { useEffect, useMemo } from 'react'
import { usePendingOrders } from 'state/signatures/hooks'
import { usePendingTransactions, useTransactionCanceller } from 'state/transactions/hooks'
import { useFormatter } from 'utils/formatNumbers'
import { useLocalActivities } from './parseLocal'
import { parseRemoteActivities } from './parseRemote'
@@ -55,6 +56,7 @@ function combineActivities(localMap: ActivityMap = {}, remoteMap: ActivityMap =
}
export function useAllActivities(account: string) {
const { formatNumberOrString } = useFormatter()
const { data, loading, refetch } = useActivityQuery({
variables: { account },
errorPolicy: 'all',
@@ -62,7 +64,10 @@ export function useAllActivities(account: string) {
})
const localMap = useLocalActivities(account)
const remoteMap = useMemo(() => parseRemoteActivities(data?.portfolios?.[0].assetActivities), [data?.portfolios])
const remoteMap = useMemo(
() => parseRemoteActivities(formatNumberOrString, data?.portfolios?.[0].assetActivities),
[data?.portfolios, formatNumberOrString]
)
const updateCancelledTx = useTransactionCanceller()
/* Updates locally stored pendings tx's when remote data contains a conflicting cancellation tx */

View File

@@ -1,80 +1,17 @@
import { t } from '@lingui/macro'
import { useAccountDrawer } from 'components/AccountDrawer'
import Column from 'components/Column'
import { LoadingBubble } from 'components/Tokens/loading'
import { getYear, isSameDay, isSameMonth, isSameWeek, isSameYear } from 'date-fns'
import { TransactionStatus } from 'graphql/data/__generated__/types-and-hooks'
import { PollingInterval } from 'graphql/data/util'
import { atom, useAtom } from 'jotai'
import { EmptyWalletModule } from 'nft/components/profile/view/EmptyWalletContent'
import { useEffect, useMemo } from 'react'
import styled from 'styled-components'
import { ThemedText } from 'theme'
import { ThemedText } from 'theme/components'
import { PortfolioSkeleton, PortfolioTabWrapper } from '../PortfolioRow'
import { ActivityRow } from './ActivityRow'
import { useAllActivities } from './hooks'
import { Activity } from './types'
interface ActivityGroup {
title: string
transactions: Array<Activity>
}
const sortActivities = (a: Activity, b: Activity) => b.timestamp - a.timestamp
const createGroups = (activities?: Array<Activity>) => {
if (!activities) return undefined
const now = Date.now()
const pending: Array<Activity> = []
const today: Array<Activity> = []
const currentWeek: Array<Activity> = []
const last30Days: Array<Activity> = []
const currentYear: Array<Activity> = []
const yearMap: { [key: string]: Array<Activity> } = {}
// TODO(cartcrom): create different time bucket system for activities to fall in based on design wants
activities.forEach((activity) => {
if (activity.status === TransactionStatus.Pending) {
pending.push(activity)
return
}
const addedTime = activity.timestamp * 1000
if (isSameDay(now, addedTime)) {
today.push(activity)
} else if (isSameWeek(addedTime, now)) {
currentWeek.push(activity)
} else if (isSameMonth(addedTime, now)) {
last30Days.push(activity)
} else if (isSameYear(addedTime, now)) {
currentYear.push(activity)
} else {
const year = getYear(addedTime)
if (!yearMap[year]) {
yearMap[year] = [activity]
} else {
yearMap[year].push(activity)
}
}
})
const sortedYears = Object.keys(yearMap)
.sort((a, b) => parseInt(b) - parseInt(a))
.map((year) => ({ title: year, transactions: yearMap[year] }))
const transactionGroups: Array<ActivityGroup> = [
{ title: t`Pending`, transactions: pending.sort(sortActivities) },
{ title: t`Today`, transactions: today.sort(sortActivities) },
{ title: t`This week`, transactions: currentWeek.sort(sortActivities) },
{ title: t`This month`, transactions: last30Days.sort(sortActivities) },
{ title: t`This year`, transactions: currentYear.sort(sortActivities) },
...sortedYears,
]
return transactionGroups.filter((transactionInformation) => transactionInformation.transactions.length > 0)
}
import { createGroups } from './utils'
const ActivityGroupWrapper = styled(Column)`
margin-top: 16px;

View File

@@ -11,6 +11,7 @@ import {
TransactionType as MockTxType,
} from 'state/transactions/types'
import { renderHook } from 'test-utils/render'
import { useFormatter } from 'utils/formatNumbers'
import { UniswapXOrderStatus } from '../../../../lib/hooks/orders/types'
import { SignatureDetails, SignatureType } from '../../../../state/signatures/types'
@@ -237,6 +238,8 @@ jest.mock('../../../../state/transactions/hooks', () => {
describe('parseLocalActivity', () => {
it('returns swap activity fields with known tokens, exact input', () => {
const { formatNumber } = renderHook(() => useFormatter()).result.current
const details = {
info: mockSwapInfo(
MockTradeType.EXACT_INPUT,
@@ -251,7 +254,7 @@ describe('parseLocalActivity', () => {
},
} as TransactionDetails
const chainId = ChainId.MAINNET
expect(transactionToActivity(details, chainId, mockTokenAddressMap)).toEqual({
expect(transactionToActivity(details, chainId, mockTokenAddressMap, formatNumber)).toEqual({
chainId: 1,
currencies: [MockUSDC_MAINNET, MockDAI],
descriptor: '1.00 USDC for 1.00 DAI',
@@ -264,6 +267,8 @@ describe('parseLocalActivity', () => {
})
it('returns swap activity fields with known tokens, exact output', () => {
const { formatNumber } = renderHook(() => useFormatter()).result.current
const details = {
info: mockSwapInfo(
MockTradeType.EXACT_OUTPUT,
@@ -278,7 +283,7 @@ describe('parseLocalActivity', () => {
},
} as TransactionDetails
const chainId = ChainId.MAINNET
expect(transactionToActivity(details, chainId, mockTokenAddressMap)).toMatchObject({
expect(transactionToActivity(details, chainId, mockTokenAddressMap, formatNumber)).toMatchObject({
chainId: 1,
currencies: [MockUSDC_MAINNET, MockDAI],
descriptor: '1.00 USDC for 1.00 DAI',
@@ -288,6 +293,8 @@ describe('parseLocalActivity', () => {
})
it('returns swap activity fields with unknown tokens', () => {
const { formatNumber } = renderHook(() => useFormatter()).result.current
const details = {
info: mockSwapInfo(
MockTradeType.EXACT_INPUT,
@@ -303,7 +310,7 @@ describe('parseLocalActivity', () => {
} as TransactionDetails
const chainId = ChainId.MAINNET
const tokens = {} as ChainTokenMap
expect(transactionToActivity(details, chainId, tokens)).toMatchObject({
expect(transactionToActivity(details, chainId, tokens, formatNumber)).toMatchObject({
chainId: 1,
currencies: [undefined, undefined],
descriptor: 'Unknown for Unknown',
@@ -496,13 +503,16 @@ describe('parseLocalActivity', () => {
})
it('Signature to activity - returns undefined if is on chain order', () => {
const { formatNumber } = renderHook(() => useFormatter()).result.current
expect(
signatureToActivity(
{
type: SignatureType.SIGN_UNISWAPX_ORDER,
status: UniswapXOrderStatus.FILLED,
} as SignatureDetails,
{}
{},
formatNumber
)
).toBeUndefined()
@@ -512,7 +522,8 @@ describe('parseLocalActivity', () => {
type: SignatureType.SIGN_UNISWAPX_ORDER,
status: UniswapXOrderStatus.CANCELLED,
} as SignatureDetails,
{}
{},
formatNumber
)
).toBeUndefined()
})

View File

@@ -2,7 +2,6 @@ import { BigNumber } from '@ethersproject/bignumber'
import { t } from '@lingui/macro'
import { ChainId, Currency, CurrencyAmount, TradeType } from '@uniswap/sdk-core'
import UniswapXBolt from 'assets/svg/bolt.svg'
import { SupportedLocale } from 'constants/locales'
import { nativeOnChain } from 'constants/tokens'
import { TransactionStatus } from 'graphql/data/__generated__/types-and-hooks'
import { ChainTokenMap, useAllTokensMultichain } from 'hooks/Tokens'
@@ -24,11 +23,13 @@ import {
TransactionType,
WrapTransactionInfo,
} from 'state/transactions/types'
import { formatCurrencyAmount, useFormatterLocales } from 'utils/formatNumbers'
import { NumberType, useFormatter } from 'utils/formatNumbers'
import { CancelledTransactionTitleTable, getActivityTitle, OrderTextTable } from '../constants'
import { Activity, ActivityMap } from './types'
type FormatNumberFunctionType = ReturnType<typeof useFormatter>['formatNumber']
function getCurrency(currencyId: string, chainId: ChainId, tokens: ChainTokenMap): Currency | undefined {
return currencyId === 'ETH' ? nativeOnChain(chainId) : tokens[chainId]?.[currencyId]
}
@@ -38,15 +39,21 @@ function buildCurrencyDescriptor(
amtA: string,
currencyB: Currency | undefined,
amtB: string,
delimiter = t`for`,
locale?: SupportedLocale
formatNumber: FormatNumberFunctionType,
delimiter = t`for`
) {
const formattedA = currencyA
? formatCurrencyAmount({ amount: CurrencyAmount.fromRawAmount(currencyA, amtA), locale })
? formatNumber({
input: parseFloat(CurrencyAmount.fromRawAmount(currencyA, amtA).toSignificant()),
type: NumberType.TokenNonTx,
})
: t`Unknown`
const symbolA = currencyA?.symbol ?? ''
const formattedB = currencyB
? formatCurrencyAmount({ amount: CurrencyAmount.fromRawAmount(currencyB, amtB), locale })
? formatNumber({
input: parseFloat(CurrencyAmount.fromRawAmount(currencyB, amtB).toSignificant()),
type: NumberType.TokenNonTx,
})
: t`Unknown`
const symbolB = currencyB?.symbol ?? ''
return [formattedA, symbolA, delimiter, formattedB, symbolB].filter(Boolean).join(' ')
@@ -56,7 +63,7 @@ function parseSwap(
swap: ExactInputSwapTransactionInfo | ExactOutputSwapTransactionInfo,
chainId: ChainId,
tokens: ChainTokenMap,
locale?: SupportedLocale
formatNumber: FormatNumberFunctionType
): Partial<Activity> {
const tokenIn = getCurrency(swap.inputCurrencyId, chainId, tokens)
const tokenOut = getCurrency(swap.outputCurrencyId, chainId, tokens)
@@ -66,18 +73,29 @@ function parseSwap(
: [swap.expectedInputCurrencyAmountRaw, swap.outputCurrencyAmountRaw]
return {
descriptor: buildCurrencyDescriptor(tokenIn, inputRaw, tokenOut, outputRaw, undefined, locale),
descriptor: buildCurrencyDescriptor(tokenIn, inputRaw, tokenOut, outputRaw, formatNumber, undefined),
currencies: [tokenIn, tokenOut],
prefixIconSrc: swap.isUniswapXOrder ? UniswapXBolt : undefined,
}
}
function parseWrap(wrap: WrapTransactionInfo, chainId: ChainId, status: TransactionStatus): Partial<Activity> {
function parseWrap(
wrap: WrapTransactionInfo,
chainId: ChainId,
status: TransactionStatus,
formatNumber: FormatNumberFunctionType
): Partial<Activity> {
const native = nativeOnChain(chainId)
const wrapped = native.wrapped
const [input, output] = wrap.unwrapped ? [wrapped, native] : [native, wrapped]
const descriptor = buildCurrencyDescriptor(input, wrap.currencyAmountRaw, output, wrap.currencyAmountRaw)
const descriptor = buildCurrencyDescriptor(
input,
wrap.currencyAmountRaw,
output,
wrap.currencyAmountRaw,
formatNumber
)
const title = getActivityTitle(TransactionType.WRAP, status, wrap.unwrapped)
const currencies = wrap.unwrapped ? [wrapped, native] : [native, wrapped]
@@ -107,11 +125,16 @@ type GenericLPInfo = Omit<
AddLiquidityV3PoolTransactionInfo | RemoveLiquidityV3TransactionInfo | AddLiquidityV2PoolTransactionInfo,
'type'
>
function parseLP(lp: GenericLPInfo, chainId: ChainId, tokens: ChainTokenMap): Partial<Activity> {
function parseLP(
lp: GenericLPInfo,
chainId: ChainId,
tokens: ChainTokenMap,
formatNumber: FormatNumberFunctionType
): Partial<Activity> {
const baseCurrency = getCurrency(lp.baseCurrencyId, chainId, tokens)
const quoteCurrency = getCurrency(lp.quoteCurrencyId, chainId, tokens)
const [baseRaw, quoteRaw] = [lp.expectedAmountBaseRaw, lp.expectedAmountQuoteRaw]
const descriptor = buildCurrencyDescriptor(baseCurrency, baseRaw, quoteCurrency, quoteRaw, t`and`)
const descriptor = buildCurrencyDescriptor(baseCurrency, baseRaw, quoteCurrency, quoteRaw, formatNumber, t`and`)
return { descriptor, currencies: [baseCurrency, quoteCurrency] }
}
@@ -119,7 +142,8 @@ function parseLP(lp: GenericLPInfo, chainId: ChainId, tokens: ChainTokenMap): Pa
function parseCollectFees(
collect: CollectFeesTransactionInfo,
chainId: ChainId,
tokens: ChainTokenMap
tokens: ChainTokenMap,
formatNumber: FormatNumberFunctionType
): Partial<Activity> {
// Adapts CollectFeesTransactionInfo to generic LP type
const {
@@ -128,7 +152,12 @@ function parseCollectFees(
expectedCurrencyOwed0: expectedAmountBaseRaw,
expectedCurrencyOwed1: expectedAmountQuoteRaw,
} = collect
return parseLP({ baseCurrencyId, quoteCurrencyId, expectedAmountBaseRaw, expectedAmountQuoteRaw }, chainId, tokens)
return parseLP(
{ baseCurrencyId, quoteCurrencyId, expectedAmountBaseRaw, expectedAmountQuoteRaw },
chainId,
tokens,
formatNumber
)
}
function parseMigrateCreateV3(
@@ -157,7 +186,7 @@ export function transactionToActivity(
details: TransactionDetails,
chainId: ChainId,
tokens: ChainTokenMap,
locale?: SupportedLocale
formatNumber: FormatNumberFunctionType
): Activity | undefined {
try {
const status = getTransactionStatus(details)
@@ -176,19 +205,19 @@ export function transactionToActivity(
let additionalFields: Partial<Activity> = {}
const info = details.info
if (info.type === TransactionType.SWAP) {
additionalFields = parseSwap(info, chainId, tokens, locale)
additionalFields = parseSwap(info, chainId, tokens, formatNumber)
} else if (info.type === TransactionType.APPROVAL) {
additionalFields = parseApproval(info, chainId, tokens, status)
} else if (info.type === TransactionType.WRAP) {
additionalFields = parseWrap(info, chainId, status)
additionalFields = parseWrap(info, chainId, status, formatNumber)
} else if (
info.type === TransactionType.ADD_LIQUIDITY_V3_POOL ||
info.type === TransactionType.REMOVE_LIQUIDITY_V3 ||
info.type === TransactionType.ADD_LIQUIDITY_V2_POOL
) {
additionalFields = parseLP(info, chainId, tokens)
additionalFields = parseLP(info, chainId, tokens, formatNumber)
} else if (info.type === TransactionType.COLLECT_FEES) {
additionalFields = parseCollectFees(info, chainId, tokens)
additionalFields = parseCollectFees(info, chainId, tokens, formatNumber)
} else if (info.type === TransactionType.MIGRATE_LIQUIDITY_V3 || info.type === TransactionType.CREATE_V3_POOL) {
additionalFields = parseMigrateCreateV3(info, chainId, tokens)
}
@@ -210,7 +239,7 @@ export function transactionToActivity(
export function signatureToActivity(
signature: SignatureDetails,
tokens: ChainTokenMap,
locale?: SupportedLocale
formatNumber: FormatNumberFunctionType
): Activity | undefined {
switch (signature.type) {
case SignatureType.SIGN_UNISWAPX_ORDER: {
@@ -229,7 +258,7 @@ export function signatureToActivity(
from: signature.offerer,
statusMessage,
prefixIconSrc: UniswapXBolt,
...parseSwap(signature.swapInfo, signature.chainId, tokens, locale),
...parseSwap(signature.swapInfo, signature.chainId, tokens, formatNumber),
}
}
default:
@@ -241,24 +270,24 @@ export function useLocalActivities(account: string): ActivityMap {
const allTransactions = useMultichainTransactions()
const allSignatures = useAllSignatures()
const tokens = useAllTokensMultichain()
const { formatterLocale } = useFormatterLocales()
const { formatNumber } = useFormatter()
return useMemo(() => {
const activityMap: ActivityMap = {}
for (const [transaction, chainId] of allTransactions) {
if (transaction.from !== account) continue
const activity = transactionToActivity(transaction, chainId, tokens, formatterLocale)
const activity = transactionToActivity(transaction, chainId, tokens, formatNumber)
if (activity) activityMap[transaction.hash] = activity
}
for (const signature of Object.values(allSignatures)) {
if (signature.offerer !== account) continue
const activity = signatureToActivity(signature, tokens, formatterLocale)
const activity = signatureToActivity(signature, tokens, formatNumber)
if (activity) activityMap[signature.id] = activity
}
return activityMap
}, [account, allSignatures, allTransactions, formatterLocale, tokens])
}, [account, allSignatures, allTransactions, formatNumber, tokens])
}

View File

@@ -0,0 +1,131 @@
import { act, renderHook } from '@testing-library/react'
import ms from 'ms'
import {
MockClosedUniswapXOrder,
MockMoonpayPurchase,
MockNFTApproval,
MockNFTApprovalForAll,
MockNFTPurchase,
MockNFTReceive,
MockNFTTransfer,
MockOpenUniswapXOrder,
MockRemoveLiquidity,
MockSwapOrder,
MockTokenApproval,
MockTokenReceive,
MockTokenSend,
MockTokenTransfer,
MockWrap,
} from './fixtures/activity'
import { parseRemoteActivities, useTimeSince } from './parseRemote'
describe('parseRemote', () => {
beforeEach(() => {
jest.useFakeTimers()
})
describe('parseRemoteActivities', () => {
it('should not parse open UniswapX order', () => {
const result = parseRemoteActivities(jest.fn(), [MockOpenUniswapXOrder])
expect(result).toEqual({})
})
it('should parse closed UniswapX order', () => {
const result = parseRemoteActivities(jest.fn(), [MockClosedUniswapXOrder])
expect(result?.['someHash']).toMatchSnapshot()
})
it('should parse NFT approval', () => {
const result = parseRemoteActivities(jest.fn(), [MockNFTApproval])
expect(result?.['someHash']).toMatchSnapshot()
})
it('should parse NFT approval for all', () => {
const result = parseRemoteActivities(jest.fn(), [MockNFTApprovalForAll])
expect(result?.['someHash']).toMatchSnapshot()
})
it('should parse NFT transfer', () => {
const result = parseRemoteActivities(jest.fn(), [MockNFTTransfer])
expect(result?.['someHash']).toMatchSnapshot()
})
it('should parse swap', () => {
const result = parseRemoteActivities(jest.fn().mockReturnValue('100'), [MockTokenTransfer])
expect(result?.['someHash']).toMatchSnapshot()
})
it('should parse nft purchase', () => {
const result = parseRemoteActivities(jest.fn().mockReturnValue('100'), [MockNFTPurchase])
expect(result?.['someHash']).toMatchSnapshot()
})
it('should parse token approval', () => {
const result = parseRemoteActivities(jest.fn(), [MockTokenApproval])
expect(result?.['someHash']).toMatchSnapshot()
})
it('should parse send', () => {
const result = parseRemoteActivities(jest.fn().mockReturnValue(100), [MockTokenSend])
expect(result?.['someHash']).toMatchSnapshot()
})
it('should parse receive', () => {
const result = parseRemoteActivities(jest.fn().mockReturnValue(100), [MockTokenReceive])
expect(result?.['someHash']).toMatchSnapshot()
})
it('should parse NFT receive', () => {
const result = parseRemoteActivities(jest.fn().mockReturnValue(100), [MockNFTReceive])
expect(result?.['someHash']).toMatchSnapshot()
})
it('should parse remove liquidity', () => {
const result = parseRemoteActivities(jest.fn().mockReturnValue(100), [MockRemoveLiquidity])
expect(result?.['someHash']).toMatchSnapshot()
})
it('should parse moonpay purchase', () => {
const result = parseRemoteActivities(jest.fn().mockReturnValue('100'), [MockMoonpayPurchase])
expect(result?.['someHash']).toMatchSnapshot()
})
it('should parse swap order', () => {
const result = parseRemoteActivities(jest.fn().mockReturnValue('100'), [MockSwapOrder])
expect(result?.['someHash']).toMatchSnapshot()
})
it('should parse eth wrap', () => {
const result = parseRemoteActivities(jest.fn().mockReturnValue('100'), [MockWrap])
expect(result?.['someHash']).toMatchSnapshot()
})
})
describe('useTimeSince', () => {
beforeEach(() => {
jest.useFakeTimers()
})
afterEach(() => {
jest.useRealTimers()
})
it('should initialize with the correct time since', () => {
const timestamp = Math.floor(Date.now() / 1000) - 60 // 60 seconds ago
const { result } = renderHook(() => useTimeSince(timestamp))
expect(result.current).toBe('1m')
})
it('should update time since every second', async () => {
const timestamp = Math.floor(Date.now() / 1000) - 50 // 50 seconds ago
const { result, rerender } = renderHook(() => useTimeSince(timestamp))
act(() => {
jest.advanceTimersByTime(ms('1.1s'))
})
rerender()
expect(result.current).toBe('51s')
})
it('should stop updating after 61 seconds', () => {
const timestamp = Math.floor(Date.now() / 1000) - 61 // 61 seconds ago
const { result, rerender } = renderHook(() => useTimeSince(timestamp))
act(() => {
jest.advanceTimersByTime(ms('121.1s'))
})
rerender()
// maxes out at 1m
expect(result.current).toBe('1m')
})
})
})

View File

@@ -21,7 +21,8 @@ import { gqlToCurrency, logSentryErrorForUnsupportedChain, supportedChainIdFromG
import ms from 'ms'
import { useEffect, useState } from 'react'
import { isAddress } from 'utils'
import { formatFiatPrice, formatNumberOrString, NumberType } from 'utils/formatNumbers'
import { isSameAddress } from 'utils/addresses'
import { NumberType, useFormatter } from 'utils/formatNumbers'
import { MOONPAY_SENDER_ADDRESSES, OrderStatusTable, OrderTextTable } from '../constants'
import { Activity } from './types'
@@ -34,6 +35,8 @@ type TransactionChanges = {
NftApproveForAll: NftApproveForAllPartsFragment[]
}
type FormatNumberOrStringFunctionType = ReturnType<typeof useFormatter>['formatNumberOrString']
// TODO: Move common contract metadata to a backend service
const UNI_IMG =
'https://raw.githubusercontent.com/Uniswap/assets/master/blockchains/ethereum/assets/0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984/logo.png'
@@ -75,10 +78,6 @@ const COMMON_CONTRACTS: { [key: string]: Partial<Activity> | undefined } = {
},
}
function isSameAddress(a?: string, b?: string) {
return a === b || a?.toLowerCase() === b?.toLowerCase() // Lazy-lowercases the addresses
}
function callsPositionManagerContract(assetActivity: TransactionActivity) {
const supportedChain = supportedChainIdFromGQLChain(assetActivity.chain)
if (!supportedChain) return false
@@ -140,13 +139,13 @@ function getSwapDescriptor({
* @param transactedValue Transacted value amount from TokenTransfer API response
* @returns parsed & formatted USD value as a string if currency is of type USD
*/
function formatTransactedValue(transactedValue: TokenTransferPartsFragment['transactedValue']): string {
if (!transactedValue) return '-'
function getTransactedValue(transactedValue: TokenTransferPartsFragment['transactedValue']): number | undefined {
if (!transactedValue) return undefined
const price = transactedValue?.currency === GQLCurrency.Usd ? transactedValue.value ?? undefined : undefined
return formatFiatPrice(price)
return price
}
function parseSwap(changes: TransactionChanges) {
function parseSwap(changes: TransactionChanges, formatNumberOrString: FormatNumberOrStringFunctionType) {
if (changes.NftTransfer.length > 0 && changes.TokenTransfer.length === 1) {
const collectionCounts = getCollectionCounts(changes.NftTransfer)
@@ -168,8 +167,8 @@ function parseSwap(changes: TransactionChanges) {
if (sent && received) {
const adjustedInput = parseFloat(sent.quantity) - parseFloat(refund?.quantity ?? '0')
const inputAmount = formatNumberOrString(adjustedInput, NumberType.TokenNonTx)
const outputAmount = formatNumberOrString(received.quantity, NumberType.TokenNonTx)
const inputAmount = formatNumberOrString({ input: adjustedInput, type: NumberType.TokenNonTx })
const outputAmount = formatNumberOrString({ input: received.quantity, type: NumberType.TokenNonTx })
return {
title: getSwapTitle(sent, received),
descriptor: getSwapDescriptor({ tokenIn: sent.asset, inputAmount, tokenOut: received.asset, outputAmount }),
@@ -180,8 +179,21 @@ function parseSwap(changes: TransactionChanges) {
return { title: t`Unknown Swap` }
}
function parseSwapOrder(changes: TransactionChanges) {
return { ...parseSwap(changes), prefixIconSrc: UniswapXBolt }
/**
* Wrap/unwrap transactions are labelled as lend transactions on the backend.
* This function parses the transaction changes to determine if the transaction is a wrap/unwrap transaction.
*/
function parseLend(changes: TransactionChanges, formatNumberOrString: FormatNumberOrStringFunctionType) {
const native = changes.TokenTransfer.find((t) => t.tokenStandard === 'NATIVE')?.asset
const erc20 = changes.TokenTransfer.find((t) => t.tokenStandard === 'ERC20')?.asset
if (native && erc20 && gqlToCurrency(native)?.wrapped.address === gqlToCurrency(erc20)?.wrapped.address) {
return parseSwap(changes, formatNumberOrString)
}
return { title: t`Unknown Lend` }
}
function parseSwapOrder(changes: TransactionChanges, formatNumberOrString: FormatNumberOrStringFunctionType) {
return { ...parseSwap(changes, formatNumberOrString), prefixIconSrc: UniswapXBolt }
}
function parseApprove(changes: TransactionChanges) {
@@ -194,12 +206,12 @@ function parseApprove(changes: TransactionChanges) {
return { title: t`Unknown Approval` }
}
function parseLPTransfers(changes: TransactionChanges) {
function parseLPTransfers(changes: TransactionChanges, formatNumberOrString: FormatNumberOrStringFunctionType) {
const poolTokenA = changes.TokenTransfer[0]
const poolTokenB = changes.TokenTransfer[1]
const tokenAQuanitity = formatNumberOrString(poolTokenA.quantity, NumberType.TokenNonTx)
const tokenBQuantity = formatNumberOrString(poolTokenB.quantity, NumberType.TokenNonTx)
const tokenAQuanitity = formatNumberOrString({ input: poolTokenA.quantity, type: NumberType.TokenNonTx })
const tokenBQuantity = formatNumberOrString({ input: poolTokenB.quantity, type: NumberType.TokenNonTx })
return {
descriptor: `${tokenAQuanitity} ${poolTokenA.asset.symbol} and ${tokenBQuantity} ${poolTokenB.asset.symbol}`,
@@ -211,11 +223,15 @@ function parseLPTransfers(changes: TransactionChanges) {
type TransactionActivity = AssetActivityPartsFragment & { details: TransactionDetailsPartsFragment }
type OrderActivity = AssetActivityPartsFragment & { details: SwapOrderDetailsPartsFragment }
function parseSendReceive(changes: TransactionChanges, assetActivity: TransactionActivity) {
function parseSendReceive(
changes: TransactionChanges,
formatNumberOrString: FormatNumberOrStringFunctionType,
assetActivity: TransactionActivity
) {
// TODO(cartcrom): remove edge cases after backend implements
// Edge case: Receiving two token transfers in interaction w/ V3 manager === removing liquidity. These edge cases should potentially be moved to backend
if (changes.TokenTransfer.length === 2 && callsPositionManagerContract(assetActivity)) {
return { title: t`Removed Liquidity`, ...parseLPTransfers(changes) }
return { title: t`Removed Liquidity`, ...parseLPTransfers(changes, formatNumberOrString) }
}
let transfer: NftTransferPartsFragment | TokenTransferPartsFragment | undefined
@@ -230,7 +246,7 @@ function parseSendReceive(changes: TransactionChanges, assetActivity: Transactio
} else if (changes.TokenTransfer.length === 1) {
transfer = changes.TokenTransfer[0]
assetName = transfer.asset.symbol
amount = formatNumberOrString(transfer.quantity, NumberType.TokenNonTx)
amount = formatNumberOrString({ input: transfer.quantity, type: NumberType.TokenNonTx })
currencies = [gqlToCurrency(transfer.asset)]
}
@@ -241,7 +257,10 @@ function parseSendReceive(changes: TransactionChanges, assetActivity: Transactio
return isMoonpayPurchase && transfer.__typename === 'TokenTransfer'
? {
title: t`Purchased`,
descriptor: `${amount} ${assetName} ${t`for`} ${formatTransactedValue(transfer.transactedValue)}`,
descriptor: `${amount} ${assetName} ${t`for`} ${formatNumberOrString({
input: getTransactedValue(transfer.transactedValue),
type: NumberType.FiatTokenPrice,
})}`,
logos: [moonpayLogoSrc],
currencies,
}
@@ -263,27 +282,40 @@ function parseSendReceive(changes: TransactionChanges, assetActivity: Transactio
return { title: t`Unknown Send` }
}
function parseMint(changes: TransactionChanges, assetActivity: TransactionActivity) {
function parseMint(
changes: TransactionChanges,
formatNumberOrString: FormatNumberOrStringFunctionType,
assetActivity: TransactionActivity
) {
const collectionMap = getCollectionCounts(changes.NftTransfer)
if (Object.keys(collectionMap).length === 1) {
const collectionName = Object.keys(collectionMap)[0]
// Edge case: Minting a v3 positon represents adding liquidity
if (changes.TokenTransfer.length === 2 && callsPositionManagerContract(assetActivity)) {
return { title: t`Added Liquidity`, ...parseLPTransfers(changes) }
return { title: t`Added Liquidity`, ...parseLPTransfers(changes, formatNumberOrString) }
}
return { title: t`Minted`, descriptor: `${collectionMap[collectionName]} ${collectionName}` }
}
return { title: t`Unknown Mint` }
}
function parseUnknown(_changes: TransactionChanges, assetActivity: TransactionActivity) {
function parseUnknown(
_changes: TransactionChanges,
_formatNumberOrString: FormatNumberOrStringFunctionType,
assetActivity: TransactionActivity
) {
return { title: t`Contract Interaction`, ...COMMON_CONTRACTS[assetActivity.details.to.toLowerCase()] }
}
type ActivityTypeParser = (changes: TransactionChanges, assetActivity: TransactionActivity) => Partial<Activity>
type ActivityTypeParser = (
changes: TransactionChanges,
formatNumberOrString: FormatNumberOrStringFunctionType,
assetActivity: TransactionActivity
) => Partial<Activity>
const ActivityParserByType: { [key: string]: ActivityTypeParser | undefined } = {
[ActivityType.Swap]: parseSwap,
[ActivityType.Lend]: parseLend,
[ActivityType.SwapOrder]: parseSwapOrder,
[ActivityType.Approve]: parseApprove,
[ActivityType.Send]: parseSendReceive,
@@ -345,7 +377,10 @@ function parseUniswapXOrder({ details, chain, timestamp }: OrderActivity): Activ
}
}
function parseRemoteActivity(assetActivity: AssetActivityPartsFragment): Activity | undefined {
function parseRemoteActivity(
assetActivity: AssetActivityPartsFragment,
formatNumberOrString: FormatNumberOrStringFunctionType
): Activity | undefined {
try {
if (assetActivity.details.__typename === 'SwapOrderDetails') {
return parseUniswapXOrder(assetActivity as OrderActivity)
@@ -371,6 +406,7 @@ function parseRemoteActivity(assetActivity: AssetActivityPartsFragment): Activit
})
return undefined
}
const defaultFields = {
hash: assetActivity.details.hash,
chainId: supportedChain,
@@ -385,6 +421,7 @@ function parseRemoteActivity(assetActivity: AssetActivityPartsFragment): Activit
const parsedFields = ActivityParserByType[assetActivity.details.type]?.(
changes,
formatNumberOrString,
assetActivity as TransactionActivity
)
return { ...defaultFields, ...parsedFields }
@@ -394,9 +431,12 @@ function parseRemoteActivity(assetActivity: AssetActivityPartsFragment): Activit
}
}
export function parseRemoteActivities(assetActivities?: readonly AssetActivityPartsFragment[]) {
export function parseRemoteActivities(
formatNumberOrString: FormatNumberOrStringFunctionType,
assetActivities?: readonly AssetActivityPartsFragment[]
) {
return assetActivities?.reduce((acc: { [hash: string]: Activity }, assetActivity) => {
const activity = parseRemoteActivity(assetActivity)
const activity = parseRemoteActivity(assetActivity, formatNumberOrString)
if (activity) acc[activity.hash] = activity
return acc
}, {})

View File

@@ -0,0 +1,42 @@
import { TransactionStatus } from 'graphql/data/__generated__/types-and-hooks' // Replace with the actual import if this is incorrect
import { Activity } from './types'
import { createGroups } from './utils'
describe('createGroups', () => {
it('should return undefined if activities is undefined', () => {
expect(createGroups(undefined)).toBeUndefined()
})
it('should return an empty array if activities is empty', () => {
expect(createGroups([])).toEqual([])
})
it('should sort and group activities based on status and time', () => {
const mockActivities = [
{ timestamp: 1700000000, status: TransactionStatus.Pending },
{ timestamp: 1650000000, status: TransactionStatus.Confirmed },
{ timestamp: Date.now() / 1000 - 300, status: TransactionStatus.Confirmed },
] as Activity[]
const result = createGroups(mockActivities)
expect(result).toContainEqual(
expect.objectContaining({
title: 'Pending',
transactions: expect.arrayContaining([
expect.objectContaining({ timestamp: 1700000000, status: TransactionStatus.Pending }),
]),
})
)
expect(result).toContainEqual(
expect.objectContaining({
title: 'Today',
transactions: expect.arrayContaining([
expect.objectContaining({ timestamp: expect.any(Number), status: TransactionStatus.Confirmed }),
]),
})
)
})
})

View File

@@ -0,0 +1,65 @@
import { t } from '@lingui/macro'
import { getYear, isSameDay, isSameMonth, isSameWeek, isSameYear } from 'date-fns'
import { TransactionStatus } from 'graphql/data/__generated__/types-and-hooks'
import { Activity } from './types'
interface ActivityGroup {
title: string
transactions: Array<Activity>
}
const sortActivities = (a: Activity, b: Activity) => b.timestamp - a.timestamp
export const createGroups = (activities?: Array<Activity>) => {
if (!activities) return undefined
const now = Date.now()
const pending: Array<Activity> = []
const today: Array<Activity> = []
const currentWeek: Array<Activity> = []
const last30Days: Array<Activity> = []
const currentYear: Array<Activity> = []
const yearMap: { [key: string]: Array<Activity> } = {}
// TODO(cartcrom): create different time bucket system for activities to fall in based on design wants
activities.forEach((activity) => {
if (activity.status === TransactionStatus.Pending) {
pending.push(activity)
return
}
const addedTime = activity.timestamp * 1000
if (isSameDay(now, addedTime)) {
today.push(activity)
} else if (isSameWeek(addedTime, now)) {
currentWeek.push(activity)
} else if (isSameMonth(addedTime, now)) {
last30Days.push(activity)
} else if (isSameYear(addedTime, now)) {
currentYear.push(activity)
} else {
const year = getYear(addedTime)
if (!yearMap[year]) {
yearMap[year] = [activity]
} else {
yearMap[year].push(activity)
}
}
})
const sortedYears = Object.keys(yearMap)
.sort((a, b) => parseInt(b) - parseInt(a))
.map((year) => ({ title: year, transactions: yearMap[year] }))
const transactionGroups: Array<ActivityGroup> = [
{ title: t`Pending`, transactions: pending.sort(sortActivities) },
{ title: t`Today`, transactions: today.sort(sortActivities) },
{ title: t`This week`, transactions: currentWeek.sort(sortActivities) },
{ title: t`This month`, transactions: last30Days.sort(sortActivities) },
{ title: t`This year`, transactions: currentYear.sort(sortActivities) },
...sortedYears,
]
return transactionGroups.filter((transactionInformation) => transactionInformation.transactions.length > 0)
}

View File

@@ -4,7 +4,7 @@ import Row from 'components/Row'
import { PropsWithChildren } from 'react'
import { ChevronDown } from 'react-feather'
import styled from 'styled-components'
import { ThemedText } from 'theme'
import { ThemedText } from 'theme/components'
const ExpandIcon = styled(ChevronDown)<{ $expanded: boolean }>`
color: ${({ theme }) => theme.neutral2};

View File

@@ -11,7 +11,7 @@ import { WalletAsset } from 'nft/types'
import { floorFormatter } from 'nft/utils'
import { useNavigate } from 'react-router-dom'
import styled from 'styled-components'
import { ThemedText } from 'theme'
import { ThemedText } from 'theme/components'
const FloorPrice = styled(Row)`
opacity: 0;

View File

@@ -9,8 +9,9 @@ import MulticallJSON from '@uniswap/v3-periphery/artifacts/contracts/lens/Uniswa
import NFTPositionManagerJSON from '@uniswap/v3-periphery/artifacts/contracts/NonfungiblePositionManager.sol/NonfungiblePositionManager.json'
import { useWeb3React } from '@web3-react/core'
import { isSupportedChain } from 'constants/chains'
import { RPC_PROVIDERS } from 'constants/providers'
import { DEPRECATED_RPC_PROVIDERS, RPC_PROVIDERS } from 'constants/providers'
import { BaseContract } from 'ethers/lib/ethers'
import { useFallbackProviderEnabled } from 'featureFlags/flags/fallbackProvider'
import { ContractInput, useUniswapPricesQuery } from 'graphql/data/__generated__/types-and-hooks'
import { toContractInput } from 'graphql/data/util'
import useStablecoinPrice from 'hooks/useStablecoinPrice'
@@ -31,6 +32,8 @@ function useContractMultichain<T extends BaseContract>(
): ContractMap<T> {
const { chainId: walletChainId, provider: walletProvider } = useWeb3React()
const networkProviders = useFallbackProviderEnabled() ? RPC_PROVIDERS : DEPRECATED_RPC_PROVIDERS
return useMemo(() => {
const relevantChains =
chainIds ??
@@ -43,14 +46,14 @@ function useContractMultichain<T extends BaseContract>(
walletProvider && walletChainId === chainId
? walletProvider
: isSupportedChain(chainId)
? RPC_PROVIDERS[chainId]
? networkProviders[chainId]
: undefined
if (provider) {
acc[chainId] = getContract(addressMap[chainId] ?? '', ABI, provider) as T
}
return acc
}, {})
}, [ABI, addressMap, chainIds, walletChainId, walletProvider])
}, [ABI, addressMap, chainIds, networkProviders, walletChainId, walletProvider])
}
export function useV3ManagerContracts(chainIds: ChainId[]): ContractMap<NonfungiblePositionManager> {

View File

@@ -1,8 +1,5 @@
import { BigNumber } from '@ethersproject/bignumber'
import { ChainId, WETH9 } from '@uniswap/sdk-core'
import { FeeAmount, Pool, Position } from '@uniswap/v3-sdk'
import { USDC_MAINNET } from 'constants/tokens'
import { mocked } from 'test-utils/mocked'
import { owner, useMultiChainPositionsReturnValue } from 'test-utils/pools/fixtures'
import { render } from 'test-utils/render'
import Pools from '.'
@@ -12,53 +9,6 @@ jest.mock('./useMultiChainPositions')
jest.spyOn(console, 'warn').mockImplementation()
const owner = '0xf5b6bb25f5beaea03dd014c6ef9fa9f3926bf36c'
const pool = new Pool(
USDC_MAINNET,
WETH9[ChainId.MAINNET],
FeeAmount.MEDIUM,
'1851127709498178402383049949138810',
'7076437181775065414',
201189
)
const position = new Position({
pool,
liquidity: 1341008833950736,
tickLower: 200040,
tickUpper: 202560,
})
const details = {
nonce: BigNumber.from('0'),
tokenId: BigNumber.from('0'),
operator: '0x0',
token0: USDC_MAINNET.address,
token1: WETH9[ChainId.MAINNET].address,
fee: FeeAmount.MEDIUM,
tickLower: -100,
tickUpper: 100,
liquidity: BigNumber.from('9000'),
feeGrowthInside0LastX128: BigNumber.from('0'),
feeGrowthInside1LastX128: BigNumber.from('0'),
tokensOwed0: BigNumber.from('0'),
tokensOwed1: BigNumber.from('0'),
}
const useMultiChainPositionsReturnValue = {
positions: [
{
owner,
chainId: ChainId.MAINNET,
position,
pool,
details,
inRange: true,
closed: false,
},
],
loading: false,
}
beforeEach(() => {
mocked(useMultiChainPositions).mockReturnValue(useMultiChainPositionsReturnValue)
})

View File

@@ -12,7 +12,7 @@ import { EmptyWalletModule } from 'nft/components/profile/view/EmptyWalletConten
import { useCallback, useMemo, useReducer } from 'react'
import { useNavigate } from 'react-router-dom'
import styled from 'styled-components'
import { ThemedText } from 'theme'
import { ThemedText } from 'theme/components'
import { NumberType, useFormatter } from 'utils/formatNumbers'
import { ExpandoRow } from '../ExpandoRow'

View File

@@ -1,8 +1,8 @@
import { BrowserEvent, InterfaceElementName, SharedEventName } from '@uniswap/analytics-events'
import { TraceEvent } from 'analytics'
import { useCachedPortfolioBalancesQuery } from 'components/AccountDrawer/PrefetchBalancesWrapper'
import { useCachedPortfolioBalancesQuery } from 'components/PrefetchBalancesWrapper/PrefetchBalancesWrapper'
import Row from 'components/Row'
import { DeltaArrow, formatDelta } from 'components/Tokens/TokenDetails/Delta'
import { DeltaArrow } from 'components/Tokens/TokenDetails/Delta'
import { TokenBalance } from 'graphql/data/__generated__/types-and-hooks'
import { getTokenDetailsURL, gqlToCurrency, logSentryErrorForUnsupportedChain } from 'graphql/data/util'
import { useAtomValue } from 'jotai/utils'
@@ -10,7 +10,7 @@ import { EmptyWalletModule } from 'nft/components/profile/view/EmptyWalletConten
import { useCallback, useMemo, useState } from 'react'
import { useNavigate } from 'react-router-dom'
import styled from 'styled-components'
import { EllipsisStyle, ThemedText } from 'theme'
import { EllipsisStyle, ThemedText } from 'theme/components'
import { NumberType, useFormatter } from 'utils/formatNumbers'
import { splitHiddenTokens } from 'utils/splitHiddenTokens'
@@ -71,6 +71,7 @@ const TokenNameText = styled(ThemedText.SubHeader)`
type PortfolioToken = NonNullable<TokenBalance['token']>
function TokenRow({ token, quantity, denominatedValue, tokenProjectMarket }: TokenBalance & { token: PortfolioToken }) {
const { formatPercent } = useFormatter()
const percentChange = tokenProjectMarket?.pricePercentChange?.value ?? 0
const navigate = useNavigate()
@@ -120,7 +121,7 @@ function TokenRow({ token, quantity, denominatedValue, tokenProjectMarket }: Tok
</ThemedText.SubHeader>
<Row justify="flex-end">
<DeltaArrow delta={percentChange} />
<ThemedText.BodySecondary>{formatDelta(percentChange)}</ThemedText.BodySecondary>
<ThemedText.BodySecondary>{formatPercent(percentChange)}</ThemedText.BodySecondary>
</Row>
</>
)

View File

@@ -8,7 +8,8 @@ import { useDisableNFTRoutes } from 'hooks/useDisableNFTRoutes'
import { useIsNftPage } from 'hooks/useIsNftPage'
import { useEffect, useState } from 'react'
import styled, { useTheme } from 'styled-components'
import { BREAKPOINTS, ThemedText } from 'theme'
import { BREAKPOINTS } from 'theme'
import { ThemedText } from 'theme/components'
import { ActivityTab } from './Activity'
import { usePendingActivity } from './Activity/hooks'

View File

@@ -8,7 +8,7 @@ import { useActiveLocale } from 'hooks/useActiveLocale'
import { ReactNode } from 'react'
import { ChevronRight } from 'react-feather'
import styled from 'styled-components'
import { ClickableStyle, ThemedText } from 'theme'
import { ClickableStyle, ThemedText } from 'theme/components'
import ThemeToggle from 'theme/components/ThemeToggle'
import { AnalyticsToggle } from './AnalyticsToggle'

View File

@@ -2,7 +2,7 @@ import Column from 'components/Column'
import Row from 'components/Row'
import Toggle from 'components/Toggle'
import styled from 'styled-components'
import { ThemedText } from 'theme'
import { ThemedText } from 'theme/components'
const StyledColumn = styled(Column)`
width: 100%;

View File

@@ -2,7 +2,7 @@ import Column from 'components/Column'
import { ScrollBarStyles } from 'components/Common'
import { ArrowLeft } from 'react-feather'
import styled from 'styled-components'
import { ClickableStyle, ThemedText } from 'theme'
import { ClickableStyle, ThemedText } from 'theme/components'
const Menu = styled(Column)`
width: 100%;

View File

@@ -12,7 +12,7 @@ import { UniwalletConnect as UniwalletConnectV2 } from 'connection/WalletConnect
import { QRCodeSVG } from 'qrcode.react'
import { useEffect, useState } from 'react'
import styled, { useTheme } from 'styled-components'
import { CloseIcon, ThemedText } from 'theme'
import { CloseIcon, ThemedText } from 'theme/components'
import { isIOS } from 'utils/userAgent'
import uniPng from '../../assets/images/uniwallet_modal_icon.png'

View File

@@ -2,6 +2,7 @@ import { BrowserEvent, InterfaceEventName } from '@uniswap/analytics-events'
import { TraceEvent } from 'analytics'
import { ScrollBarStyles } from 'components/Common'
import useDisableScrolling from 'hooks/useDisableScrolling'
import usePrevious from 'hooks/usePrevious'
import { useWindowSize } from 'hooks/useWindowSize'
import { atom } from 'jotai'
import { useAtomValue, useUpdateAtom } from 'jotai/utils'
@@ -9,7 +10,8 @@ import { useCallback, useEffect, useRef, useState } from 'react'
import { ChevronsRight } from 'react-feather'
import { useGesture } from 'react-use-gesture'
import styled from 'styled-components'
import { BREAKPOINTS, ClickableStyle } from 'theme'
import { BREAKPOINTS } from 'theme'
import { ClickableStyle } from 'theme/components'
import { Z_INDEX } from 'theme/zIndex'
import { isMobile } from 'utils/userAgent'
@@ -166,12 +168,13 @@ const CloseDrawer = styled.div`
function AccountDrawer() {
const [walletDrawerOpen, toggleWalletDrawer] = useAccountDrawer()
const wasWalletDrawerOpen = usePrevious(walletDrawerOpen)
const scrollRef = useRef<HTMLDivElement>(null)
useEffect(() => {
if (!walletDrawerOpen) {
if (wasWalletDrawerOpen && !walletDrawerOpen) {
scrollRef.current?.scrollTo({ top: 0, behavior: 'smooth' })
}
}, [walletDrawerOpen])
}, [walletDrawerOpen, wasWalletDrawerOpen])
// close on escape keypress
useEffect(() => {

View File

@@ -5,7 +5,8 @@ import { Check } from 'react-feather'
import type { To } from 'react-router-dom'
import { Link } from 'react-router-dom'
import styled, { useTheme } from 'styled-components'
import { BREAKPOINTS, ClickableStyle, ThemedText } from 'theme'
import { BREAKPOINTS } from 'theme'
import { ClickableStyle, ThemedText } from 'theme/components'
const InternalLinkMenuItem = styled(Link)`
${ClickableStyle}

View File

@@ -4,10 +4,10 @@ import { t } from '@lingui/macro'
import { useWeb3React } from '@web3-react/core'
import { ChangeEvent, ReactNode, useCallback } from 'react'
import styled, { useTheme } from 'styled-components'
import { ExternalLink, ThemedText } from 'theme/components'
import { flexColumnNoWrap } from 'theme/styles'
import useENS from '../../hooks/useENS'
import { ExternalLink, ThemedText } from '../../theme'
import { ExplorerDataType, getExplorerLink } from '../../utils/getExplorerLink'
import { AutoColumn } from '../Column'
import { RowBetween } from '../Row'

View File

@@ -10,14 +10,14 @@ import Loader from 'components/Icons/LoadingSpinner'
import { useContract } from 'hooks/useContract'
import { ChevronRightIcon } from 'nft/components/icons'
import { useIsNftClaimAvailable } from 'nft/hooks/useIsNftClaimAvailable'
import { CollectionRewardsFetcher } from 'nft/queries/genie/GetAirdorpMerkle'
import { CollectionRewardsFetcher } from 'nft/queries/genie'
import { Airdrop, Rewards } from 'nft/types/airdrop'
import { useEffect, useState } from 'react'
import { AlertTriangle } from 'react-feather'
import { useModalIsOpen, useToggleModal } from 'state/application/hooks'
import { ApplicationModal } from 'state/application/reducer'
import styled from 'styled-components'
import { CloseIcon, ThemedText } from 'theme'
import { CloseIcon, ThemedText } from 'theme/components'
import Modal from '../Modal'

View File

@@ -55,7 +55,7 @@ export default function RangeBadge({ removed, inRange }: { removed?: boolean; in
>
<LabelText color={theme.success}>
<BadgeText>
<Trans>In Range</Trans>
<Trans>In range</Trans>
</BadgeText>
<ActiveDot />
</LabelText>

View File

@@ -7,7 +7,7 @@ import baseLogoUrl from 'assets/svg/base_background_icon.svg'
import { useScreenSize } from 'hooks/useScreenSize'
import { useLocation } from 'react-router-dom'
import { useHideBaseWalletBanner } from 'state/user/hooks'
import { ThemedText } from 'theme'
import { ThemedText } from 'theme/components'
import { openDownloadApp, openWalletMicrosite } from 'utils/openDownloadApp'
import { isIOS, isMobileSafari } from 'utils/userAgent'

View File

@@ -1,4 +1,4 @@
import { SpinnerSVG } from 'theme'
import { SpinnerSVG } from 'theme/components'
const ButtonLoadingSpinner = (props: React.ComponentPropsWithoutRef<'svg'>) => (
<SpinnerSVG width="20" height="20" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg" {...props}>

View File

@@ -0,0 +1,70 @@
import { ScaleLinear, scaleLinear } from 'd3'
import { PricePoint } from 'graphql/data/util'
import { cleanPricePoints, getPriceBounds } from './utils'
export enum ChartErrorType {
NO_DATA_AVAILABLE,
NO_RECENT_VOLUME,
INVALID_CHART,
}
type ChartDimensions = {
width: number
height: number
marginTop: number
marginBottom: number
}
export type ErroredChartModel = { error: ChartErrorType; dimensions: ChartDimensions }
export type ChartModel = {
prices: PricePoint[]
startingPrice: PricePoint
endingPrice: PricePoint
lastValidPrice: PricePoint
blanks: PricePoint[][]
timeScale: ScaleLinear<number, number>
priceScale: ScaleLinear<number, number>
dimensions: ChartDimensions
error: undefined
}
type ChartModelArgs = { prices?: PricePoint[]; dimensions: ChartDimensions }
export function buildChartModel({ dimensions, prices }: ChartModelArgs): ChartModel | ErroredChartModel {
if (!prices) {
return { error: ChartErrorType.NO_DATA_AVAILABLE, dimensions }
}
const innerHeight = dimensions.height - dimensions.marginTop - dimensions.marginBottom
if (innerHeight < 0) {
return { error: ChartErrorType.INVALID_CHART, dimensions }
}
const { prices: fixedPrices, blanks, lastValidPrice } = cleanPricePoints(prices)
if (fixedPrices.length < 2 || !lastValidPrice) {
return { error: ChartErrorType.NO_RECENT_VOLUME, dimensions }
}
const startingPrice = prices[0]
const endingPrice = prices[prices.length - 1]
const { min, max } = getPriceBounds(prices)
// x-axis scale
const timeScale = scaleLinear().domain([startingPrice.timestamp, endingPrice.timestamp]).range([0, dimensions.width])
// y-axis scale
const priceScale = scaleLinear().domain([min, max]).range([innerHeight, 0])
return {
prices: fixedPrices,
startingPrice,
endingPrice,
lastValidPrice,
blanks,
timeScale,
priceScale,
dimensions,
error: undefined,
}
}

View File

@@ -15,9 +15,13 @@ exports[`PriceChart renders correctly with all prices filled 1`] = `
}
.c1 {
font-size: 36px;
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
gap: 6px;
font-size: 24px;
line-height: 44px;
font-weight: 485;
}
.c2 {
@@ -38,11 +42,15 @@ exports[`PriceChart renders correctly with all prices filled 1`] = `
class="c0"
data-cy="chart-header"
>
<span
<div
class="c1"
>
$1.00
</span>
<div
class="css-15popx1"
>
$1.00
</div>
</div>
<div
class="c2"
>
@@ -337,10 +345,10 @@ exports[`PriceChart renders correctly with all prices filled 1`] = `
</DocumentFragment>
`;
exports[`PriceChart renders correctly with no prices filled 1`] = `
exports[`PriceChart renders correctly with empty price array 1`] = `
<DocumentFragment>
.c3 {
color: #222222;
.c1 {
color: #CECECE;
}
.c0 {
@@ -351,41 +359,22 @@ exports[`PriceChart renders correctly with no prices filled 1`] = `
animation-duration: 250ms;
}
.c1 {
font-size: 36px;
line-height: 44px;
font-weight: 485;
}
.c2 {
font-size: 24px;
line-height: 44px;
color: #CECECE;
}
<div
class="c0"
data-cy="chart-header"
>
<span
class="c1 c2"
>
Price Unavailable
</span>
<div
class="c3 css-142zc9n"
style="color: rgb(206, 206, 206);"
class="c1 css-slqfkh"
>
Missing chart data
Price unavailable
</div>
<div
class="c1 css-142zc9n"
>
Missing price data due to recently low trading volume on Uniswap v3
</div>
</div>
.c0 text {
font-size: 12px;
font-weight: 485;
}
<svg
class="c0"
<svg
data-cy="missing-chart"
height="392"
style="min-width: 100%;"
@@ -398,23 +387,18 @@ exports[`PriceChart renders correctly with no prices filled 1`] = `
stroke="#22222212"
stroke-width="2"
/>
<text
fill="#CECECE"
x="20"
y="377"
/>
</svg>
</DocumentFragment>
`;
exports[`PriceChart renders correctly with some prices filled 1`] = `
<DocumentFragment>
.c4 {
.c2 {
display: inline-block;
height: inherit;
}
.c6 {
.c4 {
color: #7D7D7D;
}
@@ -424,19 +408,20 @@ exports[`PriceChart renders correctly with some prices filled 1`] = `
animation: iAjNNh 125ms ease-in;
-webkit-animation-duration: 250ms;
animation-duration: 250ms;
}
.c3 {
font-size: 36px;
line-height: 44px;
font-weight: 485;
}
.c1 {
color: #7D7D7D;
}
.c5 {
.c1 {
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
gap: 6px;
font-size: 24px;
line-height: 44px;
}
.c3 {
height: 16px;
display: -webkit-box;
display: -webkit-flex;
@@ -450,16 +435,6 @@ exports[`PriceChart renders correctly with some prices filled 1`] = `
color: #7D7D7D;
}
.c2 {
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
gap: 6px;
font-size: 24px;
line-height: 44px;
}
<div
class="c0"
data-cy="chart-header"
@@ -468,69 +443,66 @@ exports[`PriceChart renders correctly with some prices filled 1`] = `
class="c1"
>
<div
class="c2"
class="css-15popx1"
>
<span
class="c3"
>
$1.00
</span>
<div
class="c4"
>
<div>
<svg
fill="none"
height="16"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
viewBox="0 0 24 24"
width="16"
xmlns="http://www.w3.org/2000/svg"
>
<circle
cx="12"
cy="12"
r="10"
/>
<line
x1="12"
x2="12"
y1="16"
y2="12"
/>
<line
x1="12"
x2="12.01"
y1="8"
y2="8"
/>
</svg>
</div>
</div>
$1.00
</div>
<div
class="c5"
class="c2"
>
0.00%
<svg
aria-label="up"
class="c6"
fill="none"
height="16"
viewBox="0 0 24 24"
width="16"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M13.3021 7.7547L17.6821 14.2475C18.4182 15.3388 17.7942 17 16.6482 17L7.3518 17C6.2058 17 5.5818 15.3376 6.3179 14.2475L10.6979 7.7547C11.377 6.7484 12.623 6.7484 13.3021 7.7547Z"
fill="currentColor"
/>
</svg>
<div>
<svg
data-testid="chart-stale-icon"
fill="none"
height="16"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
viewBox="0 0 24 24"
width="16"
xmlns="http://www.w3.org/2000/svg"
>
<circle
cx="12"
cy="12"
r="10"
/>
<line
x1="12"
x2="12"
y1="16"
y2="12"
/>
<line
x1="12"
x2="12.01"
y1="8"
y2="8"
/>
</svg>
</div>
</div>
</div>
<div
class="c3"
>
0.00%
<svg
aria-label="up"
class="c4"
fill="none"
height="16"
viewBox="0 0 24 24"
width="16"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M13.3021 7.7547L17.6821 14.2475C18.4182 15.3388 17.7942 17 16.6482 17L7.3518 17C6.2058 17 5.5818 15.3376 6.3179 14.2475L10.6979 7.7547C11.377 6.7484 12.623 6.7484 13.3021 7.7547Z"
fill="currentColor"
/>
</svg>
</div>
</div>
<svg
data-cy="price-chart"
@@ -805,3 +777,548 @@ exports[`PriceChart renders correctly with some prices filled 1`] = `
</svg>
</DocumentFragment>
`;
exports[`PriceChart renders correctly with undefined prices 1`] = `
<DocumentFragment>
.c1 {
color: #CECECE;
}
.c0 {
position: absolute;
-webkit-animation: iAjNNh 125ms ease-in;
animation: iAjNNh 125ms ease-in;
-webkit-animation-duration: 250ms;
animation-duration: 250ms;
}
<div
class="c0"
data-cy="chart-header"
>
<div
class="c1 css-slqfkh"
>
Price unavailable
</div>
<div
class="c1 css-142zc9n"
>
Missing chart data
</div>
</div>
<svg
data-cy="missing-chart"
height="392"
style="min-width: 100%;"
width="780"
>
<path
d="M 0 241 Q 104 171, 208 241 T 416 241
M 416 241 Q 520 171, 624 241 T 832 241"
fill="transparent"
stroke="#22222212"
stroke-width="2"
/>
</svg>
</DocumentFragment>
`;
exports[`PriceChart renders stale UI 1`] = `
<DocumentFragment>
.c2 {
display: inline-block;
height: inherit;
}
.c4 {
color: #7D7D7D;
}
.c0 {
position: absolute;
-webkit-animation: iAjNNh 125ms ease-in;
animation: iAjNNh 125ms ease-in;
-webkit-animation-duration: 250ms;
animation-duration: 250ms;
color: #7D7D7D;
}
.c1 {
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
gap: 6px;
font-size: 24px;
line-height: 44px;
}
.c3 {
height: 16px;
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
-webkit-align-items: center;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
margin-top: 4px;
color: #7D7D7D;
}
<div
class="c0"
data-cy="chart-header"
>
<div
class="c1"
>
<div
class="css-15popx1"
>
$1.00
</div>
<div
class="c2"
>
<div>
<svg
data-testid="chart-stale-icon"
fill="none"
height="16"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
viewBox="0 0 24 24"
width="16"
xmlns="http://www.w3.org/2000/svg"
>
<circle
cx="12"
cy="12"
r="10"
/>
<line
x1="12"
x2="12"
y1="16"
y2="12"
/>
<line
x1="12"
x2="12.01"
y1="8"
y2="8"
/>
</svg>
</div>
</div>
</div>
<div
class="c3"
>
0.00%
<svg
aria-label="up"
class="c4"
fill="none"
height="16"
viewBox="0 0 24 24"
width="16"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M13.3021 7.7547L17.6821 14.2475C18.4182 15.3388 17.7942 17 16.6482 17L7.3518 17C6.2058 17 5.5818 15.3376 6.3179 14.2475L10.6979 7.7547C11.377 6.7484 12.623 6.7484 13.3021 7.7547Z"
fill="currentColor"
/>
</svg>
</div>
</div>
<svg
data-cy="price-chart"
height="392"
style="min-width: 100%;"
width="780"
>
<g
class="visx-group visx-axis visx-axis-bottom"
transform="translate(0, 391)"
>
<g
class="visx-group visx-axis-tick"
transform="translate(0, 0)"
>
<svg
font-size="10"
style="overflow: visible;"
x="0"
y="0.25em"
>
<text
fill="#222"
font-family="Arial"
font-size="10"
text-anchor="middle"
transform=""
x="48.75"
y="18"
>
<tspan
dy="0em"
x="48.75"
>
1,694,538,840
</tspan>
</text>
</svg>
</g>
<g
class="visx-group visx-axis-tick"
transform="translate(0, 0)"
>
<svg
font-size="10"
style="overflow: visible;"
x="0"
y="0.25em"
>
<text
fill="#222"
font-family="Arial"
font-size="10"
text-anchor="middle"
transform=""
x="109.6875"
y="18"
>
<tspan
dy="0em"
x="109.6875"
>
1,694,538,845
</tspan>
</text>
</svg>
</g>
<g
class="visx-group visx-axis-tick"
transform="translate(0, 0)"
>
<svg
font-size="10"
style="overflow: visible;"
x="0"
y="0.25em"
>
<text
fill="#222"
font-family="Arial"
font-size="10"
text-anchor="middle"
transform=""
x="170.625"
y="18"
>
<tspan
dy="0em"
x="170.625"
>
1,694,538,850
</tspan>
</text>
</svg>
</g>
<g
class="visx-group visx-axis-tick"
transform="translate(0, 0)"
>
<svg
font-size="10"
style="overflow: visible;"
x="0"
y="0.25em"
>
<text
fill="#222"
font-family="Arial"
font-size="10"
text-anchor="middle"
transform=""
x="231.5625"
y="18"
>
<tspan
dy="0em"
x="231.5625"
>
1,694,538,855
</tspan>
</text>
</svg>
</g>
<g
class="visx-group visx-axis-tick"
transform="translate(0, 0)"
>
<svg
font-size="10"
style="overflow: visible;"
x="0"
y="0.25em"
>
<text
fill="#222"
font-family="Arial"
font-size="10"
text-anchor="middle"
transform=""
x="292.5"
y="18"
>
<tspan
dy="0em"
x="292.5"
>
1,694,538,860
</tspan>
</text>
</svg>
</g>
<g
class="visx-group visx-axis-tick"
transform="translate(0, 0)"
>
<svg
font-size="10"
style="overflow: visible;"
x="0"
y="0.25em"
>
<text
fill="#222"
font-family="Arial"
font-size="10"
text-anchor="middle"
transform=""
x="353.4375"
y="18"
>
<tspan
dy="0em"
x="353.4375"
>
1,694,538,865
</tspan>
</text>
</svg>
</g>
<g
class="visx-group visx-axis-tick"
transform="translate(0, 0)"
>
<svg
font-size="10"
style="overflow: visible;"
x="0"
y="0.25em"
>
<text
fill="#222"
font-family="Arial"
font-size="10"
text-anchor="middle"
transform=""
x="414.375"
y="18"
>
<tspan
dy="0em"
x="414.375"
>
1,694,538,870
</tspan>
</text>
</svg>
</g>
<g
class="visx-group visx-axis-tick"
transform="translate(0, 0)"
>
<svg
font-size="10"
style="overflow: visible;"
x="0"
y="0.25em"
>
<text
fill="#222"
font-family="Arial"
font-size="10"
text-anchor="middle"
transform=""
x="475.3125"
y="18"
>
<tspan
dy="0em"
x="475.3125"
>
1,694,538,875
</tspan>
</text>
</svg>
</g>
<g
class="visx-group visx-axis-tick"
transform="translate(0, 0)"
>
<svg
font-size="10"
style="overflow: visible;"
x="0"
y="0.25em"
>
<text
fill="#222"
font-family="Arial"
font-size="10"
text-anchor="middle"
transform=""
x="536.25"
y="18"
>
<tspan
dy="0em"
x="536.25"
>
1,694,538,880
</tspan>
</text>
</svg>
</g>
<g
class="visx-group visx-axis-tick"
transform="translate(0, 0)"
>
<svg
font-size="10"
style="overflow: visible;"
x="0"
y="0.25em"
>
<text
fill="#222"
font-family="Arial"
font-size="10"
text-anchor="middle"
transform=""
x="597.1875"
y="18"
>
<tspan
dy="0em"
x="597.1875"
>
1,694,538,885
</tspan>
</text>
</svg>
</g>
<g
class="visx-group visx-axis-tick"
transform="translate(0, 0)"
>
<svg
font-size="10"
style="overflow: visible;"
x="0"
y="0.25em"
>
<text
fill="#222"
font-family="Arial"
font-size="10"
text-anchor="middle"
transform=""
x="658.125"
y="18"
>
<tspan
dy="0em"
x="658.125"
>
1,694,538,890
</tspan>
</text>
</svg>
</g>
<g
class="visx-group visx-axis-tick"
transform="translate(0, 0)"
>
<svg
font-size="10"
style="overflow: visible;"
x="0"
y="0.25em"
>
<text
fill="#222"
font-family="Arial"
font-size="10"
text-anchor="middle"
transform=""
x="719.0625"
y="18"
>
<tspan
dy="0em"
x="719.0625"
>
1,694,538,895
</tspan>
</text>
</svg>
</g>
<g
class="visx-group visx-axis-tick"
transform="translate(0, 0)"
>
<svg
font-size="10"
style="overflow: visible;"
x="0"
y="0.25em"
>
<text
fill="#222"
font-family="Arial"
font-size="10"
text-anchor="middle"
transform=""
x="780"
y="18"
>
<tspan
dy="0em"
x="780"
>
1,694,538,900
</tspan>
</text>
</svg>
</g>
</g>
<rect
fill="transparent"
height="392"
width="780"
x="0"
y="0"
/>
</svg>
</DocumentFragment>
`;

View File

@@ -0,0 +1,74 @@
import { TimePeriod } from 'graphql/data/util'
import { render, screen } from 'test-utils/render'
import { PriceChart } from '.'
jest.mock('components/Charts/AnimatedInLineChart', () => ({
__esModule: true,
default: jest.fn(() => null),
}))
jest.mock('components/Charts/FadeInLineChart', () => ({
__esModule: true,
default: jest.fn(() => null),
}))
describe('PriceChart', () => {
it('renders correctly with all prices filled', () => {
const mockPrices = Array.from({ length: 13 }, (_, i) => ({
value: 1,
timestamp: i * 3600,
}))
const { asFragment } = render(
<PriceChart prices={mockPrices} width={780} height={392} timePeriod={TimePeriod.HOUR} />
)
expect(asFragment()).toMatchSnapshot()
expect(asFragment().textContent).toContain('$1.00')
expect(asFragment().textContent).toContain('0.00%')
})
it('renders correctly with some prices filled', () => {
const mockPrices = Array.from({ length: 13 }, (_, i) => ({
value: i < 10 ? 1 : 0,
timestamp: i * 3600,
}))
const { asFragment } = render(
<PriceChart prices={mockPrices} width={780} height={392} timePeriod={TimePeriod.HOUR} />
)
expect(asFragment()).toMatchSnapshot()
expect(asFragment().textContent).toContain('$1.00')
expect(asFragment().textContent).toContain('0.00%')
})
it('renders correctly with empty price array', () => {
const { asFragment } = render(<PriceChart prices={[]} width={780} height={392} timePeriod={TimePeriod.HOUR} />)
expect(asFragment()).toMatchSnapshot()
expect(asFragment().textContent).toContain('Price unavailable')
expect(asFragment().textContent).toContain('Missing price data due to recently low trading volume on Uniswap v3')
})
it('renders correctly with undefined prices', () => {
const { asFragment } = render(
<PriceChart prices={undefined} width={780} height={392} timePeriod={TimePeriod.HOUR} />
)
expect(asFragment()).toMatchSnapshot()
expect(asFragment().textContent).toContain('Price unavailable')
expect(asFragment().textContent).toContain('Missing chart data')
})
it('renders stale UI', () => {
const { asFragment } = render(
<PriceChart
prices={[
{ value: 1, timestamp: 1694538836 },
{ value: 1, timestamp: 1694538840 },
{ value: 1, timestamp: 1694538844 },
{ value: 0, timestamp: 1694538900 },
]}
width={780}
height={392}
timePeriod={TimePeriod.HOUR}
/>
)
expect(asFragment()).toMatchSnapshot()
expect(asFragment().textContent).toContain('$1.00')
expect(screen.getByTestId('chart-stale-icon')).toBeInTheDocument()
})
})

View File

@@ -0,0 +1,308 @@
import { Trans } from '@lingui/macro'
import { AxisBottom } from '@visx/axis'
import { localPoint } from '@visx/event'
import { EventType } from '@visx/event/lib/types'
import { GlyphCircle } from '@visx/glyph'
import { Line } from '@visx/shape'
import AnimatedInLineChart from 'components/Charts/AnimatedInLineChart'
import FadedInLineChart from 'components/Charts/FadeInLineChart'
import { buildChartModel, ChartErrorType, ChartModel, ErroredChartModel } from 'components/Charts/PriceChart/ChartModel'
import { getTimestampFormatter, TimestampFormatterType } from 'components/Charts/PriceChart/format'
import { getNearestPricePoint, getTicks } from 'components/Charts/PriceChart/utils'
import { MouseoverTooltip } from 'components/Tooltip'
import { curveCardinal } from 'd3'
import { PricePoint, TimePeriod } from 'graphql/data/util'
import { useActiveLocale } from 'hooks/useActiveLocale'
import { ReactNode, useCallback, useEffect, useMemo, useState } from 'react'
import { Info } from 'react-feather'
import styled, { useTheme } from 'styled-components'
import { ThemedText } from 'theme/components'
import { textFadeIn } from 'theme/styles'
import { useFormatter } from 'utils/formatNumbers'
import { calculateDelta, DeltaArrow } from '../../Tokens/TokenDetails/Delta'
const CHART_MARGIN = { top: 100, bottom: 48, crosshair: 72 }
const ChartHeaderWrapper = styled.div<{ stale?: boolean }>`
position: absolute;
${textFadeIn};
animation-duration: ${({ theme }) => theme.transition.duration.medium};
${({ theme, stale }) => stale && `color: ${theme.neutral2}`};
`
const PriceContainer = styled.div`
display: flex;
gap: 6px;
font-size: 24px;
line-height: 44px;
`
const DeltaContainer = styled.div`
height: 16px;
display: flex;
align-items: center;
margin-top: 4px;
color: ${({ theme }) => theme.neutral2};
`
interface ChartDeltaProps {
startingPrice: PricePoint
endingPrice: PricePoint
noColor?: boolean
}
function ChartDelta({ startingPrice, endingPrice, noColor }: ChartDeltaProps) {
const delta = calculateDelta(startingPrice.value, endingPrice.value)
const { formatPercent } = useFormatter()
return (
<DeltaContainer>
{formatPercent(delta)}
<DeltaArrow delta={delta} noColor={noColor} />
</DeltaContainer>
)
}
interface ChartHeaderProps {
crosshairPrice?: PricePoint
chart: ChartModel
}
function ChartHeader({ crosshairPrice, chart }: ChartHeaderProps) {
const { formatFiatPrice } = useFormatter()
const { startingPrice, endingPrice, lastValidPrice } = chart
const priceOutdated = lastValidPrice !== endingPrice
const displayPrice = crosshairPrice ?? (priceOutdated ? lastValidPrice : endingPrice)
const displayIsStale = priceOutdated && !crosshairPrice
return (
<ChartHeaderWrapper data-cy="chart-header" stale={displayIsStale}>
<PriceContainer>
<ThemedText.HeadlineLarge color="inherit">
{formatFiatPrice({ price: displayPrice.value })}
</ThemedText.HeadlineLarge>
{displayIsStale && (
<MouseoverTooltip text={<Trans>This price may not be up-to-date due to low trading volume.</Trans>}>
<Info size={16} data-testid="chart-stale-icon" />
</MouseoverTooltip>
)}
</PriceContainer>
<ChartDelta startingPrice={startingPrice} endingPrice={displayPrice} noColor={priceOutdated} />
</ChartHeaderWrapper>
)
}
function ChartBody({ chart, timePeriod }: { chart: ChartModel; timePeriod: TimePeriod }) {
const locale = useActiveLocale()
const { prices, blanks, timeScale, priceScale, dimensions } = chart
const { ticks, tickTimestampFormatter, crosshairTimestampFormatter } = useMemo(() => {
// Limits the number of ticks based on graph width
const maxTicks = Math.floor(dimensions.width / 100)
const ticks = getTicks(chart.startingPrice.timestamp, chart.endingPrice.timestamp, timePeriod, maxTicks)
const tickTimestampFormatter = getTimestampFormatter(timePeriod, locale, TimestampFormatterType.TICK)
const crosshairTimestampFormatter = getTimestampFormatter(timePeriod, locale, TimestampFormatterType.CROSSHAIR)
return { ticks, tickTimestampFormatter, crosshairTimestampFormatter }
}, [dimensions.width, chart.startingPrice.timestamp, chart.endingPrice.timestamp, timePeriod, locale])
const theme = useTheme()
const [crosshair, setCrosshair] = useState<{ x: number; y: number; price: PricePoint }>()
const resetCrosshair = useCallback(() => setCrosshair(undefined), [setCrosshair])
const setCrosshairOnHover = useCallback(
(event: Element | EventType) => {
const { x } = localPoint(event) || { x: 0 }
const price = getNearestPricePoint(x, prices, timeScale)
if (price) {
const x = timeScale(price.timestamp)
const y = priceScale(price.value)
setCrosshair({ x, y, price })
}
},
[priceScale, timeScale, prices]
)
// Resets the crosshair when the time period is changed, to avoid stale UI
useEffect(() => resetCrosshair(), [resetCrosshair, timePeriod])
const crosshairEdgeMax = dimensions.width * 0.85
const crosshairAtEdge = !!crosshair && crosshair.x > crosshairEdgeMax
// Default curve doesn't look good for the HOUR chart.
// Higher values make the curve more rigid, lower values smooth the curve but make it less "sticky" to real data points,
// making it unacceptable for shorter durations / smaller variances.
const curveTension = timePeriod === TimePeriod.HOUR ? 1 : 0.9
const getX = useCallback((p: PricePoint) => timeScale(p.timestamp), [timeScale])
const getY = useCallback((p: PricePoint) => priceScale(p.value), [priceScale])
const curve = useMemo(() => curveCardinal.tension(curveTension), [curveTension])
return (
<>
<ChartHeader chart={chart} crosshairPrice={crosshair?.price} />
<svg data-cy="price-chart" width={dimensions.width} height={dimensions.height} style={{ minWidth: '100%' }}>
<AnimatedInLineChart
data={prices}
getX={getX}
getY={getY}
marginTop={dimensions.marginTop}
curve={curve}
strokeWidth={2}
/>
{blanks.map((blank, index) => (
<FadedInLineChart
key={index}
data={blank}
getX={getX}
getY={getY}
marginTop={dimensions.marginTop}
curve={curve}
strokeWidth={2}
color={theme.neutral3}
dashed
/>
))}
{crosshair !== undefined ? (
<g>
<AxisBottom
top={dimensions.height - 1}
scale={timeScale}
stroke={theme.surface3}
hideTicks={true}
tickValues={ticks}
tickFormat={tickTimestampFormatter}
tickLabelProps={() => ({
fill: theme.neutral2,
fontSize: 12,
textAnchor: 'middle',
transform: 'translate(0 -29)',
})}
/>
<text
x={crosshair.x + (crosshairAtEdge ? -4 : 4)}
y={CHART_MARGIN.crosshair + 10}
textAnchor={crosshairAtEdge ? 'end' : 'start'}
fontSize={12}
fill={theme.neutral2}
>
{crosshairTimestampFormatter(crosshair.price.timestamp)}
</text>
<Line
from={{ x: crosshair.x, y: CHART_MARGIN.crosshair }}
to={{ x: crosshair.x, y: dimensions.height }}
stroke={theme.surface3}
strokeWidth={1}
pointerEvents="none"
strokeDasharray="4,4"
/>
<GlyphCircle
left={crosshair.x}
top={crosshair.y + dimensions.marginTop}
size={50}
fill={theme.accent1}
stroke={theme.surface3}
strokeWidth={0.5}
/>
</g>
) : (
<AxisBottom
hideAxisLine={true}
scale={timeScale}
stroke={theme.surface3}
top={dimensions.height - 1}
hideTicks
/>
)}
{!dimensions.width && (
// Ensures an axis is drawn even if the width is not yet initialized.
<line
x1={0}
y1={dimensions.height - 1}
x2="100%"
y2={dimensions.height - 1}
fill="transparent"
shapeRendering="crispEdges"
stroke={theme.surface3}
strokeWidth={1}
/>
)}
<rect
x={0}
y={0}
width={dimensions.width}
height={dimensions.height}
fill="transparent"
onTouchStart={setCrosshairOnHover}
onTouchMove={setCrosshairOnHover}
onMouseMove={setCrosshairOnHover}
onMouseLeave={resetCrosshair}
/>
</svg>
</>
)
}
const CHART_ERROR_MESSAGES: Record<ChartErrorType, ReactNode> = {
[ChartErrorType.NO_DATA_AVAILABLE]: <Trans>Missing chart data</Trans>,
[ChartErrorType.NO_RECENT_VOLUME]: <Trans>Missing price data due to recently low trading volume on Uniswap v3</Trans>,
[ChartErrorType.INVALID_CHART]: <Trans>Invalid chart</Trans>,
}
function MissingPriceChart({ chart }: { chart: ErroredChartModel }) {
const theme = useTheme()
const midPoint = chart.dimensions.height / 2 + 45
return (
<>
<ChartHeaderWrapper data-cy="chart-header">
<ThemedText.HeadlineLarge fontSize={24} color="neutral3">
Price unavailable
</ThemedText.HeadlineLarge>
<ThemedText.BodySmall color="neutral3">{CHART_ERROR_MESSAGES[chart.error]}</ThemedText.BodySmall>
</ChartHeaderWrapper>
<svg
data-cy="missing-chart"
width={chart.dimensions.width}
height={chart.dimensions.height}
style={{ minWidth: '100%' }}
>
<path
d={`M 0 ${midPoint} Q 104 ${midPoint - 70}, 208 ${midPoint} T 416 ${midPoint}
M 416 ${midPoint} Q 520 ${midPoint - 70}, 624 ${midPoint} T 832 ${midPoint}`}
stroke={theme.surface3}
fill="transparent"
strokeWidth="2"
/>
</svg>
</>
)
}
interface PriceChartProps {
width: number
height: number
prices?: PricePoint[]
timePeriod: TimePeriod
}
export function PriceChart({ width, height, prices, timePeriod }: PriceChartProps) {
const chart = useMemo(
() =>
buildChartModel({
dimensions: { width, height, marginBottom: CHART_MARGIN.bottom, marginTop: CHART_MARGIN.top },
prices,
}),
[width, height, prices]
)
if (chart.error !== undefined) {
return <MissingPriceChart chart={chart} />
}
return <ChartBody chart={chart} timePeriod={timePeriod} />
}

View File

@@ -2,9 +2,9 @@ import { Trans } from '@lingui/macro'
import Column from 'components/Column'
import { BlockedIcon } from 'components/TokenSafety/TokenSafetyIcon'
import styled, { useTheme } from 'styled-components'
import { ExternalLink, ThemedText } from 'theme'
import { ExternalLink, ThemedText } from 'theme/components'
import { CopyHelper } from 'theme/components'
import { CopyHelper } from '../../theme'
import Modal from '../Modal'
const ContentWrapper = styled(Column)`
@@ -25,7 +25,7 @@ export default function ConnectedAccountBlocked(props: ConnectedAccountBlockedPr
<ContentWrapper>
<BlockedIcon size="22px" />
<ThemedText.DeprecatedLargeHeader lineHeight={2} marginBottom={1} marginTop={1}>
<Trans>Blocked Address</Trans>
<Trans>Blocked address</Trans>
</ThemedText.DeprecatedLargeHeader>
<ThemedText.DeprecatedDarkGray fontSize={12} marginBottom={12}>
{props.account}

View File

@@ -5,7 +5,7 @@ import { LoadingBubble } from 'components/Tokens/loading'
import { MouseoverTooltip } from 'components/Tooltip'
import { useMemo } from 'react'
import styled from 'styled-components'
import { ThemedText } from 'theme'
import { ThemedText } from 'theme/components'
import { NumberType, useFormatter } from 'utils/formatNumbers'
import { warningSeverity } from 'utils/prices'

View File

@@ -4,10 +4,10 @@ import { Currency, CurrencyAmount, Percent } from '@uniswap/sdk-core'
import { Pair } from '@uniswap/v2-sdk'
import { useWeb3React } from '@web3-react/core'
import { TraceEvent } from 'analytics'
import PrefetchBalancesWrapper from 'components/AccountDrawer/PrefetchBalancesWrapper'
import { AutoColumn } from 'components/Column'
import { LoadingOpacityContainer, loadingOpacityMixin } from 'components/Loader/styled'
import CurrencyLogo from 'components/Logo/CurrencyLogo'
import PrefetchBalancesWrapper from 'components/PrefetchBalancesWrapper/PrefetchBalancesWrapper'
import Tooltip from 'components/Tooltip'
import { isSupportedChain } from 'constants/chains'
import ms from 'ms'
@@ -15,12 +15,12 @@ import { darken } from 'polished'
import { forwardRef, ReactNode, useCallback, useEffect, useState } from 'react'
import { Lock } from 'react-feather'
import styled, { useTheme } from 'styled-components'
import { ThemedText } from 'theme/components'
import { flexColumnNoWrap, flexRowNoWrap } from 'theme/styles'
import { NumberType, useFormatter } from 'utils/formatNumbers'
import { ReactComponent as DropDown } from '../../assets/images/dropdown.svg'
import { useCurrencyBalance } from '../../state/connection/hooks'
import { ThemedText } from '../../theme'
import { ButtonGray } from '../Button'
import DoubleCurrencyLogo from '../DoubleLogo'
import { Input as NumericalInput } from '../NumericalInput'
@@ -67,8 +67,8 @@ const CurrencySelect = styled(ButtonGray)<{
opacity: ${({ disabled }) => (!disabled ? 1 : 0.4)};
color: ${({ selected, theme }) => (selected ? theme.neutral1 : theme.white)};
cursor: pointer;
height: unset;
border-radius: 16px;
height: 36px;
border-radius: 18px;
outline: none;
user-select: none;
border: 1px solid ${({ selected, theme }) => (selected ? theme.surface3 : theme.accent1)};

View File

@@ -5,16 +5,17 @@ import { Pair } from '@uniswap/v2-sdk'
import { useWeb3React } from '@web3-react/core'
import { TraceEvent } from 'analytics'
import { LoadingOpacityContainer, loadingOpacityMixin } from 'components/Loader/styled'
import PrefetchBalancesWrapper from 'components/PrefetchBalancesWrapper/PrefetchBalancesWrapper'
import { isSupportedChain } from 'constants/chains'
import { darken } from 'polished'
import { ReactNode, useCallback, useState } from 'react'
import styled, { useTheme } from 'styled-components'
import { ThemedText } from 'theme/components'
import { flexColumnNoWrap, flexRowNoWrap } from 'theme/styles'
import { formatCurrencyAmount } from 'utils/formatCurrencyAmount'
import { ReactComponent as DropDown } from '../../assets/images/dropdown.svg'
import { useCurrencyBalance } from '../../state/connection/hooks'
import { ThemedText } from '../../theme'
import { ButtonGray } from '../Button'
import DoubleCurrencyLogo from '../DoubleLogo'
import CurrencyLogo from '../Logo/CurrencyLogo'
@@ -160,6 +161,10 @@ const StyledNumericalInput = styled(NumericalInput)<{ $loading: boolean }>`
text-align: left;
`
const StyledPrefetchBalancesWrapper = styled(PrefetchBalancesWrapper)<{ $fullWidth: boolean }>`
width: ${({ $fullWidth }) => ($fullWidth ? '100%' : 'auto')};
`
interface CurrencyInputPanelProps {
value: string
onUserInput: (value: string) => void
@@ -230,45 +235,50 @@ export default function CurrencyInputPanel({
/>
)}
<CurrencySelect
disabled={!chainAllowed}
visible={currency !== undefined}
selected={!!currency}
hideInput={hideInput}
className="open-currency-select-button"
onClick={() => {
if (onCurrencySelect) {
setModalOpen(true)
}
}}
pointerEvents={!onCurrencySelect ? 'none' : undefined}
>
<Aligner>
<RowFixed>
{pair ? (
<span style={{ marginRight: '0.5rem' }}>
<DoubleCurrencyLogo currency0={pair.token0} currency1={pair.token1} size={24} margin={true} />
</span>
) : (
currency && <CurrencyLogo style={{ marginRight: '0.5rem' }} currency={currency} size="24px" />
)}
{pair ? (
<StyledTokenName className="pair-name-container">
{pair?.token0.symbol}:{pair?.token1.symbol}
</StyledTokenName>
) : (
<StyledTokenName className="token-symbol-container" active={Boolean(currency && currency.symbol)}>
{(currency && currency.symbol && currency.symbol.length > 20
? currency.symbol.slice(0, 4) +
'...' +
currency.symbol.slice(currency.symbol.length - 5, currency.symbol.length)
: currency?.symbol) || <Trans>Select a token</Trans>}
</StyledTokenName>
)}
</RowFixed>
{onCurrencySelect && <StyledDropDown selected={!!currency} />}
</Aligner>
</CurrencySelect>
<StyledPrefetchBalancesWrapper shouldFetchOnAccountUpdate={modalOpen} $fullWidth={hideInput}>
<CurrencySelect
disabled={!chainAllowed}
visible={currency !== undefined}
selected={!!currency}
hideInput={hideInput}
className="open-currency-select-button"
onClick={() => {
if (onCurrencySelect) {
setModalOpen(true)
}
}}
pointerEvents={!onCurrencySelect ? 'none' : undefined}
>
<Aligner>
<RowFixed>
{pair ? (
<span style={{ marginRight: '0.5rem' }}>
<DoubleCurrencyLogo currency0={pair.token0} currency1={pair.token1} size={24} margin={true} />
</span>
) : (
currency && <CurrencyLogo style={{ marginRight: '0.5rem' }} currency={currency} size="24px" />
)}
{pair ? (
<StyledTokenName className="pair-name-container">
{pair?.token0.symbol}:{pair?.token1.symbol}
</StyledTokenName>
) : (
<StyledTokenName
className="token-symbol-container"
active={Boolean(currency && currency.symbol)}
>
{(currency && currency.symbol && currency.symbol.length > 20
? currency.symbol.slice(0, 4) +
'...' +
currency.symbol.slice(currency.symbol.length - 5, currency.symbol.length)
: currency?.symbol) || <Trans>Select a token</Trans>}
</StyledTokenName>
)}
</RowFixed>
{onCurrencySelect && <StyledDropDown selected={!!currency} />}
</Aligner>
</CurrencySelect>
</StyledPrefetchBalancesWrapper>
</InputRow>
{Boolean(!hideInput && !hideBalance && currency) && (
<FiatRow>

View File

@@ -7,9 +7,9 @@ import { useIsMobile } from 'nft/hooks'
import React, { PropsWithChildren, useState } from 'react'
import { Copy } from 'react-feather'
import styled from 'styled-components'
import { CopyToClipboard, ExternalLink, ThemedText } from 'theme/components'
import { isSentryEnabled } from 'utils/env'
import { CopyToClipboard, ExternalLink, ThemedText } from '../../theme'
import { Column } from '../Column'
const FallbackWrapper = styled.div`

View File

@@ -1,12 +1,14 @@
import Column from 'components/Column'
import { BaseVariant, FeatureFlag, featureFlagSettings, useUpdateFlag } from 'featureFlags'
import { useCurrencyConversionFlag } from 'featureFlags/flags/currencyConversion'
import { useFallbackProviderEnabledFlag } from 'featureFlags/flags/fallbackProvider'
import { useFotAdjustmentsFlag } from 'featureFlags/flags/fotAdjustments'
import { useInfoExploreFlag } from 'featureFlags/flags/infoExplore'
import { useInfoLiveViewsFlag } from 'featureFlags/flags/infoLiveViews'
import { useInfoPoolPageFlag } from 'featureFlags/flags/infoPoolPage'
import { useInfoTDPFlag } from 'featureFlags/flags/infoTDP'
import { useMultichainUXFlag } from 'featureFlags/flags/multichainUx'
import { useQuickRouteMainnetFlag } from 'featureFlags/flags/quickRouteMainnet'
import { TraceJsonRpcVariant, useTraceJsonRpcFlag } from 'featureFlags/flags/traceJsonRpc'
import { useUniswapXDefaultEnabledFlag } from 'featureFlags/flags/uniswapXDefault'
import { useUniswapXEthOutputFlag } from 'featureFlags/flags/uniswapXEthOutput'
@@ -229,6 +231,12 @@ export default function FeatureFlagModal() {
<X size={24} />
</CloseButton>
</Header>
<FeatureFlagOption
variant={BaseVariant}
value={useFallbackProviderEnabledFlag()}
featureFlag={FeatureFlag.fallbackProvider}
label="Enable fallback provider"
/>
<FeatureFlagOption
variant={BaseVariant}
value={useCurrencyConversionFlag()}
@@ -247,6 +255,14 @@ export default function FeatureFlagModal() {
featureFlag={FeatureFlag.fotAdjustedmentsEnabled}
label="Enable fee-on-transfer UI and slippage adjustments"
/>
<FeatureFlagGroup name="Quick routes">
<FeatureFlagOption
variant={BaseVariant}
value={useQuickRouteMainnetFlag()}
featureFlag={FeatureFlag.quickRouteMainnet}
label="Enable quick routes for Mainnet"
/>
</FeatureFlagGroup>
<FeatureFlagGroup name="UniswapX Flags">
<FeatureFlagOption
variant={BaseVariant}

View File

@@ -6,7 +6,7 @@ import { useFeeTierDistribution } from 'hooks/useFeeTierDistribution'
import { PoolState } from 'hooks/usePools'
import React from 'react'
import styled from 'styled-components'
import { ThemedText } from 'theme'
import { ThemedText } from 'theme/components'
import { FeeTierPercentageBadge } from './FeeTierPercentageBadge'
import { FEE_AMOUNT_DETAIL } from './shared'

View File

@@ -4,7 +4,7 @@ import Badge from 'components/Badge'
import { useFeeTierDistribution } from 'hooks/useFeeTierDistribution'
import { PoolState } from 'hooks/usePools'
import React from 'react'
import { ThemedText } from 'theme'
import { ThemedText } from 'theme/components'
export function FeeTierPercentageBadge({
feeAmount,

View File

@@ -15,7 +15,7 @@ import { DynamicSection } from 'pages/AddLiquidity/styled'
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { Box } from 'rebass'
import styled, { keyframes } from 'styled-components'
import { ThemedText } from 'theme'
import { ThemedText } from 'theme/components'
import { FeeOption } from './FeeOption'
import { FeeTierPercentageBadge } from './FeeTierPercentageBadge'

View File

@@ -0,0 +1,17 @@
export const MOONPAY_SUPPORTED_CURRENCY_CODES = [
'eth',
'eth_arbitrum',
'eth_optimism',
'eth_polygon',
'weth',
'wbtc',
'matic_polygon',
'polygon',
'usdc_arbitrum',
'usdc_optimism',
'usdc_polygon',
'usdc',
'usdt',
] as const
export type MoonpaySupportedCurrencyCode = (typeof MOONPAY_SUPPORTED_CURRENCY_CODES)[number]

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