Compare commits

...

111 Commits

Author SHA1 Message Date
Kristie Huang
0bec35c7ea fix: add native USDC to Optimism quick-select (#7499) (#7519)
* fix: add native USDC to Optimism quick-select

* fix jest test
2023-10-30 14:44:57 -04:00
charity-sock-pacifist
f0e1747bb6 fix: learn more url [prod] (#7482)
* fix: learn more url

* fix: snapshots

---------

Co-authored-by: charity-sock-pacifist <interface-github@example.com>
2023-10-17 13:18:51 -04:00
charity-sock-pacifist
8ffa601ab6 feat: swap fees (#7477) 2023-10-16 19:31:17 -04:00
UL Service Account
b887d0f607 ci: add global CODEOWNERS 2023-10-13 20:54:28 +00:00
UL Service Account
6160c446ea ci(t9n): download translations from crowdin 2023-10-13 20:54:28 +00:00
eddie
f556f745fb fix: remove sitemap step from ci (#7470) 2023-10-13 13:20:10 -07:00
cartcrom
7f597c0fab refactor: use bips base constant (#7464)
* refactor: use bips base constant

* fix: missed usage
2023-10-13 16:15:28 -04:00
Tina
cfaf5d79c1 feat: Remove local routing setting (#7462)
* remove client side router preference

* update e2e test

* fix comment
2023-10-13 14:33:47 -04:00
eddie
e9fbf61375 fix: position X opt in tooltip on TDP (#7465) 2023-10-13 10:20:10 -07:00
eddie
749c9b40ea fix: specify canonical URLs (#7455)
* fix: app.uniswap.org canonicals

* fix: deps test

* fix: use window.location.origin
2023-10-13 09:27:15 -07:00
eddie
b553a6fcd8 feat: nfts sitemap (#7456)
* feat: add nft collections to sitemap

* feat: size check on sitemap

* fix: update generate-sitemap script
2023-10-13 09:17:57 -07:00
eddie
ad1e2c60a1 fix: delay setting user.router_preference until statsig and redux initialize (#7458) 2023-10-12 15:42:57 -07:00
eddie
7001452f89 fix: move user prop updater into statsigProvider (#7442) 2023-10-12 11:30:02 -07:00
eddie
5fee3c6fdd fix: sitemap format (#7453) 2023-10-11 15:23:15 -07:00
eddie
40b1e40721 fix: remove bridge usdc arbitrum (#7446) 2023-10-11 12:16:15 -07:00
eddie
aee4df10a8 fix: change default tx deadline to 10m (#7451)
* fix: change deadline to 10m

* test: add unit tests

* fix: improve unit tests
2023-10-11 12:15:51 -07:00
Charles Bachmeier
82a194987a feat: [info] Add Token Description Component (#7400)
* working token details section

* update decription styling

* different chain explorers

* remove wrap check for color extraction

* move token description to its own component, add copy, make simple project query

* rename styled components and add tests

* remove old comment

* await test fragment

* fix: update description truncation from TokenDescription (#7413)

* fix: update description truncation from TokenDescription

* fix: use better name

* fix: test if description is hidden or not (#7422)

---------

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

* make darker or lighter

* showCopy default false

* update test

* remove unused styles

---------

Co-authored-by: eddie <66155195+just-toby@users.noreply.github.com>
2023-10-11 10:32:33 -07:00
eddie
1882b14690 fix: hide slippage warning when trade is x (#7439) 2023-10-11 10:21:43 -07:00
gnewfield
d56030a920 feat: add progress_indicator_v2 flag (#7445)
* feat: add progress_indicator_v2 flag

* rename flag hook
2023-10-11 12:53:10 -04:00
Kristie Huang
b75438bc8b fix: fix sitemaps routes test (#7441)
* fix: fix sitemaps routes test

* avoid checking all top-tokens paths
2023-10-11 12:53:01 -04:00
Thomas Thachil
9f44e48cf1 chore(): update deeplink package names (#7399) 2023-10-11 12:17:45 -04:00
cartcrom
48855f487f feat: animated review expando (#7440)
* feat: animated review expando

* test: update snapshots

* fix: pr comments + spacing
2023-10-11 09:23:28 -04:00
eddie
f09ded1a3f fix: user prop updater staging (#7443)
fix: move user prop updater into statsigProvider
2023-10-10 15:51:26 -07:00
cartcrom
e16348e2e0 feat: new chain logos (#7438)
* feat: new consolidated chain logos

* test: update snapshots

* refactor: simplify border radius formula

* fix: pass style prop to portofolio logo container

* lint

* fix: accessibility

* fix: pr nits'

* fix: unnused styled component
2023-10-10 18:25:50 -04:00
Kristie Huang
45c3e1dc78 feat: add dynamicconfig modal setting (#7395)
* feat: add dynamicconfig feature flags setting

* better typing

* use diff atom for configs

* fix

* add config to devflagsbox

* fix devbox intiailization

* lint
2023-10-10 15:37:47 -04:00
Charles Bachmeier
24ddace1eb feat: [info] update color extract default and have PDP fallback (#7432)
* feat: [info] update color extract default and have a fallback

* uncomment section removed for testing

* update snapshot
2023-10-10 12:24:57 -07:00
eddie
b38ce038e6 feat: remove sitemap script from prepare step (#7437) 2023-10-10 10:41:08 -07:00
Charles Bachmeier
04bf075826 fix: broken Checkmark image for Mobile NFT Sort dropdown (#7401)
fix broken image
2023-10-10 10:02:21 -07:00
eddie
27ec2e018c feat: add robots.txt (#7435) 2023-10-06 17:09:04 -07:00
eddie
3ffe7693cf feat: add top token urls to sitemap and improve script (#7429)
* feat: add top token urls to sitemap and improve script

* fix: remove unnecessary header

* fix: test
2023-10-06 17:08:46 -07: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
541 changed files with 158249 additions and 9115 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

@@ -0,0 +1,149 @@
import { CurrencyAmount } from '@uniswap/sdk-core'
import { FeatureFlag } from 'featureFlags'
import { USDC_MAINNET } from '../../../src/constants/tokens'
import { getBalance, getTestSelector } from '../../utils'
describe('Swap with fees', () => {
describe('Classic swaps', () => {
beforeEach(() => {
cy.visit('/swap', { featureFlags: [{ name: FeatureFlag.feesEnabled, value: true }] })
// Store trade quote into alias
cy.intercept({ url: 'https://api.uniswap.org/v2/quote' }, (req) => {
// Avoid tracking stablecoin pricing fetches
if (JSON.parse(req.body).intent !== 'pricing') req.alias = 'quoteFetch'
})
})
it('displays $0 fee on swaps without fees', () => {
// Set up a stablecoin <> stablecoin swap (no fees)
cy.get('#swap-currency-input .open-currency-select-button').click()
cy.contains('DAI').click()
cy.get('#swap-currency-output .open-currency-select-button').click()
cy.contains('USDC').click()
cy.get('#swap-currency-output .token-amount-input').type('1')
// Verify 0 fee UI is displayed
cy.get(getTestSelector('swap-details-header-row')).click()
cy.contains('Fee')
cy.contains('$0')
})
it('swaps ETH for USDC exact-out with swap fee', () => {
cy.hardhat().then((hardhat) => {
getBalance(USDC_MAINNET).then((initialBalance) => {
// Set up swap
cy.get('#swap-currency-output .open-currency-select-button').click()
cy.contains('USDC').click()
cy.get('#swap-currency-output .token-amount-input').type('1')
cy.wait('@quoteFetch')
.its('response.body')
.then(({ quote: { portionBips, portionRecipient, portionAmount } }) => {
// Fees are generally expected to always be enabled for ETH -> USDC swaps
// If the routing api does not include a fee, end the test early rather than manually update routes and hardcode fee vars
if (portionRecipient) return
cy.then(() => hardhat.getBalance(portionRecipient, USDC_MAINNET)).then((initialRecipientBalance) => {
const feeCurrencyAmount = CurrencyAmount.fromRawAmount(USDC_MAINNET, portionAmount)
// Initiate transaction
cy.get('#swap-button').click()
cy.contains('Review swap')
// Verify fee percentage and amount is displayed
cy.contains(`Fee (${portionBips / 100}%)`)
// Confirm transaction
cy.contains('Confirm swap').click()
// Verify transaction
cy.get(getTestSelector('web3-status-connected')).should('not.contain', 'Pending')
cy.get(getTestSelector('popups')).contains('Swapped')
// Verify the post-fee output is the expected exact-out amount
const finalBalance = initialBalance + 1
cy.get('#swap-currency-output').contains(`Balance: ${finalBalance}`)
getBalance(USDC_MAINNET).should('eq', finalBalance)
// Verify fee recipient received fee
cy.then(() => hardhat.getBalance(portionRecipient, USDC_MAINNET)).then((finalRecipientBalance) => {
const expectedFinalRecipientBalance = initialRecipientBalance.add(feeCurrencyAmount)
cy.then(() => finalRecipientBalance.equalTo(expectedFinalRecipientBalance)).should('be.true')
})
})
})
})
})
})
it('swaps ETH for USDC exact-in with swap fee', () => {
cy.hardhat().then((hardhat) => {
// Set up swap
cy.get('#swap-currency-output .open-currency-select-button').click()
cy.contains('USDC').click()
cy.get('#swap-currency-input .token-amount-input').type('.01')
cy.wait('@quoteFetch')
.its('response.body')
.then(({ quote: { portionBips, portionRecipient } }) => {
// Fees are generally expected to always be enabled for ETH -> USDC swaps
// If the routing api does not include a fee, end the test early rather than manually update routes and hardcode fee vars
if (portionRecipient) return
cy.then(() => hardhat.getBalance(portionRecipient, USDC_MAINNET)).then((initialRecipientBalance) => {
// Initiate transaction
cy.get('#swap-button').click()
cy.contains('Review swap')
// Verify fee percentage and amount is displayed
cy.contains(`Fee (${portionBips / 100}%)`)
// Confirm transaction
cy.contains('Confirm swap').click()
// Verify transaction
cy.get(getTestSelector('web3-status-connected')).should('not.contain', 'Pending')
cy.get(getTestSelector('popups')).contains('Swapped')
// Verify fee recipient received fee
cy.then(() => hardhat.getBalance(portionRecipient, USDC_MAINNET)).then((finalRecipientBalance) => {
cy.then(() => finalRecipientBalance.greaterThan(initialRecipientBalance)).should('be.true')
})
})
})
})
})
})
describe('UniswapX swaps', () => {
it('displays UniswapX fee in UI', () => {
cy.visit('/swap', {
featureFlags: [
{ name: FeatureFlag.feesEnabled, value: true },
{ name: FeatureFlag.uniswapXDefaultEnabled, value: true },
],
})
// Intercept the trade quote
cy.intercept({ url: 'https://api.uniswap.org/v2/quote' }, (req) => {
// Avoid intercepting stablecoin pricing fetches
if (JSON.parse(req.body).intent !== 'pricing') {
req.reply({ fixture: 'uniswapx/feeQuote.json' })
}
})
// Setup swap
cy.get('#swap-currency-input .open-currency-select-button').click()
cy.contains('USDC').click()
cy.get('#swap-currency-output .open-currency-select-button').click()
cy.contains('ETH').click()
cy.get('#swap-currency-input .token-amount-input').type('200')
// Verify fee UI is displayed
cy.get(getTestSelector('swap-details-header-row')).click()
cy.contains('Fee (0.15%)')
})
})
})

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

@@ -69,6 +69,6 @@ describe('Token explore', () => {
cy.get(getTestSelector('tokens-network-filter-selected')).click()
cy.get(getTestSelector('tokens-network-filter-option-optimism')).click()
cy.get(getTestSelector('tokens-network-filter-selected')).should('contain', 'Optimism')
cy.get(getTestSelector('chain-selector-logo')).invoke('attr', 'alt').should('eq', 'Ethereum')
cy.get(getTestSelector('chain-selector-logo')).find('title').should('include.text', 'Ethereum logo')
})
})

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

@@ -2,7 +2,7 @@ import { createDeferredPromise } from '../../../src/test-utils/promise'
import { getTestSelector } from '../../utils'
function waitsForActiveChain(chain: string) {
cy.get(getTestSelector('chain-selector-logo')).invoke('attr', 'alt').should('eq', chain)
cy.get(getTestSelector('chain-selector-logo')).find('title').should('include.text', `${chain} logo`)
}
function switchChain(chain: string) {

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

@@ -0,0 +1,562 @@
{
"routing": "DUTCH_LIMIT",
"quote": {
"orderInfo": {
"chainId": 1,
"permit2Address": "0x000000000022d473030f116ddee9f6b43ac78ba3",
"reactor": "0x6000da47483062A0D734Ba3dc7576Ce6A0B645C4",
"swapper": "0x0938a82F93D5DAB110Dc6277FC236b5b082DC10F",
"nonce": "1993353164669688581970088190602701610528397285201889446578254799128576197633",
"deadline": 1697481666,
"additionalValidationContract": "0x0000000000000000000000000000000000000000",
"additionalValidationData": "0x",
"decayStartTime": 1697481594,
"decayEndTime": 1697481654,
"exclusiveFiller": "0xaAFb85ad4a412dd8adC49611496a7695A22f4aeb",
"exclusivityOverrideBps": "100",
"input": {
"token": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
"startAmount": "200000000",
"endAmount": "200000000"
},
"outputs": [
{
"token": "0x0000000000000000000000000000000000000000",
"startAmount": "123803169993201727",
"endAmount": "117908377342236273",
"recipient": "0x0938a82F93D5DAB110Dc6277FC236b5b082DC10F"
},
{
"token": "0x0000000000000000000000000000000000000000",
"startAmount": "185983730585681",
"endAmount": "177128258400955",
"recipient": "0x37a8f295612602f2774d331e562be9e61B83a327"
}
]
},
"encodedOrder": "0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000012000000000000000000000000000000000000000000000000000000000652d837a00000000000000000000000000000000000000000000000000000000652d83b6000000000000000000000000aafb85ad4a412dd8adc49611496a7695a22f4aeb0000000000000000000000000000000000000000000000000000000000000064000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000000000000000000000000000000000000bebc200000000000000000000000000000000000000000000000000000000000bebc20000000000000000000000000000000000000000000000000000000000000002000000000000000000000000006000da47483062a0d734ba3dc7576ce6a0b645c40000000000000000000000000938a82f93d5dab110dc6277fc236b5b082dc10f046832aa305880d33daa871e5041a0cd4853599a9ead518917239e206765040100000000000000000000000000000000000000000000000000000000652d83c2000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001b7d653c183183f00000000000000000000000000000000000000000000000001a2e50b6386d6710000000000000000000000000938a82f93d5dab110dc6277fc236b5b082dc10f00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a926b63210510000000000000000000000000000000000000000000000000000a118e2ebf2bb00000000000000000000000037a8f295612602f2774d331e562be9e61b83a327",
"quoteId": "7b924043-f2d8-4f2e-abaa-9f65fbe5f890",
"requestId": "a02ca0ca-7855-4dd0-9330-8b818aaeb59f",
"orderHash": "0xb5b4e3be188f6eb9dbe7e1489595829184a9ebfb5389185ed7ba7c03142278c9",
"startTimeBufferSecs": 45,
"auctionPeriodSecs": 60,
"deadlineBufferSecs": 12,
"slippageTolerance": "0.5",
"permitData": {
"domain": {
"name": "Permit2",
"chainId": 1,
"verifyingContract": "0x000000000022d473030f116ddee9f6b43ac78ba3"
},
"types": {
"PermitWitnessTransferFrom": [
{
"name": "permitted",
"type": "TokenPermissions"
},
{
"name": "spender",
"type": "address"
},
{
"name": "nonce",
"type": "uint256"
},
{
"name": "deadline",
"type": "uint256"
},
{
"name": "witness",
"type": "ExclusiveDutchOrder"
}
],
"TokenPermissions": [
{
"name": "token",
"type": "address"
},
{
"name": "amount",
"type": "uint256"
}
],
"ExclusiveDutchOrder": [
{
"name": "info",
"type": "OrderInfo"
},
{
"name": "decayStartTime",
"type": "uint256"
},
{
"name": "decayEndTime",
"type": "uint256"
},
{
"name": "exclusiveFiller",
"type": "address"
},
{
"name": "exclusivityOverrideBps",
"type": "uint256"
},
{
"name": "inputToken",
"type": "address"
},
{
"name": "inputStartAmount",
"type": "uint256"
},
{
"name": "inputEndAmount",
"type": "uint256"
},
{
"name": "outputs",
"type": "DutchOutput[]"
}
],
"OrderInfo": [
{
"name": "reactor",
"type": "address"
},
{
"name": "swapper",
"type": "address"
},
{
"name": "nonce",
"type": "uint256"
},
{
"name": "deadline",
"type": "uint256"
},
{
"name": "additionalValidationContract",
"type": "address"
},
{
"name": "additionalValidationData",
"type": "bytes"
}
],
"DutchOutput": [
{
"name": "token",
"type": "address"
},
{
"name": "startAmount",
"type": "uint256"
},
{
"name": "endAmount",
"type": "uint256"
},
{
"name": "recipient",
"type": "address"
}
]
},
"values": {
"permitted": {
"token": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
"amount": {
"type": "BigNumber",
"hex": "0x0bebc200"
}
},
"spender": "0x6000da47483062A0D734Ba3dc7576Ce6A0B645C4",
"nonce": {
"type": "BigNumber",
"hex": "0x046832aa305880d33daa871e5041a0cd4853599a9ead518917239e2067650401"
},
"deadline": 1697481666,
"witness": {
"info": {
"reactor": "0x6000da47483062A0D734Ba3dc7576Ce6A0B645C4",
"swapper": "0x0938a82F93D5DAB110Dc6277FC236b5b082DC10F",
"nonce": {
"type": "BigNumber",
"hex": "0x046832aa305880d33daa871e5041a0cd4853599a9ead518917239e2067650401"
},
"deadline": 1697481666,
"additionalValidationContract": "0x0000000000000000000000000000000000000000",
"additionalValidationData": "0x"
},
"decayStartTime": 1697481594,
"decayEndTime": 1697481654,
"exclusiveFiller": "0xaAFb85ad4a412dd8adC49611496a7695A22f4aeb",
"exclusivityOverrideBps": {
"type": "BigNumber",
"hex": "0x64"
},
"inputToken": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
"inputStartAmount": {
"type": "BigNumber",
"hex": "0x0bebc200"
},
"inputEndAmount": {
"type": "BigNumber",
"hex": "0x0bebc200"
},
"outputs": [
{
"token": "0x0000000000000000000000000000000000000000",
"startAmount": {
"type": "BigNumber",
"hex": "0x01b7d653c183183f"
},
"endAmount": {
"type": "BigNumber",
"hex": "0x01a2e50b6386d671"
},
"recipient": "0x0938a82F93D5DAB110Dc6277FC236b5b082DC10F"
},
{
"token": "0x0000000000000000000000000000000000000000",
"startAmount": {
"type": "BigNumber",
"hex": "0xa926b6321051"
},
"endAmount": {
"type": "BigNumber",
"hex": "0xa118e2ebf2bb"
},
"recipient": "0x37a8f295612602f2774d331e562be9e61B83a327"
}
]
}
}
},
"portionBips": 15,
"portionAmount": "185983730585681",
"portionRecipient": "0x37a8f295612602f2774d331e562be9e61B83a327"
},
"requestId": "a02ca0ca-7855-4dd0-9330-8b818aaeb59f",
"allQuotes": [
{
"routing": "DUTCH_LIMIT",
"quote": {
"orderInfo": {
"chainId": 1,
"permit2Address": "0x000000000022d473030f116ddee9f6b43ac78ba3",
"reactor": "0x6000da47483062A0D734Ba3dc7576Ce6A0B645C4",
"swapper": "0x0938a82F93D5DAB110Dc6277FC236b5b082DC10F",
"nonce": "1993353164669688581970088190602701610528397285201889446578254799128576197633",
"deadline": 1697481666,
"additionalValidationContract": "0x0000000000000000000000000000000000000000",
"additionalValidationData": "0x",
"decayStartTime": 1697481594,
"decayEndTime": 1697481654,
"exclusiveFiller": "0xaAFb85ad4a412dd8adC49611496a7695A22f4aeb",
"exclusivityOverrideBps": "100",
"input": {
"token": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
"startAmount": "200000000",
"endAmount": "200000000"
},
"outputs": [
{
"token": "0x0000000000000000000000000000000000000000",
"startAmount": "123803169993201727",
"endAmount": "117908377342236273",
"recipient": "0x0938a82F93D5DAB110Dc6277FC236b5b082DC10F"
},
{
"token": "0x0000000000000000000000000000000000000000",
"startAmount": "185983730585681",
"endAmount": "177128258400955",
"recipient": "0x37a8f295612602f2774d331e562be9e61B83a327"
}
]
},
"encodedOrder": "0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000012000000000000000000000000000000000000000000000000000000000652d837a00000000000000000000000000000000000000000000000000000000652d83b6000000000000000000000000aafb85ad4a412dd8adc49611496a7695a22f4aeb0000000000000000000000000000000000000000000000000000000000000064000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000000000000000000000000000000000000bebc200000000000000000000000000000000000000000000000000000000000bebc20000000000000000000000000000000000000000000000000000000000000002000000000000000000000000006000da47483062a0d734ba3dc7576ce6a0b645c40000000000000000000000000938a82f93d5dab110dc6277fc236b5b082dc10f046832aa305880d33daa871e5041a0cd4853599a9ead518917239e206765040100000000000000000000000000000000000000000000000000000000652d83c2000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001b7d653c183183f00000000000000000000000000000000000000000000000001a2e50b6386d6710000000000000000000000000938a82f93d5dab110dc6277fc236b5b082dc10f00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a926b63210510000000000000000000000000000000000000000000000000000a118e2ebf2bb00000000000000000000000037a8f295612602f2774d331e562be9e61b83a327",
"quoteId": "7b924043-f2d8-4f2e-abaa-9f65fbe5f890",
"requestId": "a02ca0ca-7855-4dd0-9330-8b818aaeb59f",
"orderHash": "0xb5b4e3be188f6eb9dbe7e1489595829184a9ebfb5389185ed7ba7c03142278c9",
"startTimeBufferSecs": 45,
"auctionPeriodSecs": 60,
"deadlineBufferSecs": 12,
"slippageTolerance": "0.5",
"permitData": {
"domain": {
"name": "Permit2",
"chainId": 1,
"verifyingContract": "0x000000000022d473030f116ddee9f6b43ac78ba3"
},
"types": {
"PermitWitnessTransferFrom": [
{
"name": "permitted",
"type": "TokenPermissions"
},
{
"name": "spender",
"type": "address"
},
{
"name": "nonce",
"type": "uint256"
},
{
"name": "deadline",
"type": "uint256"
},
{
"name": "witness",
"type": "ExclusiveDutchOrder"
}
],
"TokenPermissions": [
{
"name": "token",
"type": "address"
},
{
"name": "amount",
"type": "uint256"
}
],
"ExclusiveDutchOrder": [
{
"name": "info",
"type": "OrderInfo"
},
{
"name": "decayStartTime",
"type": "uint256"
},
{
"name": "decayEndTime",
"type": "uint256"
},
{
"name": "exclusiveFiller",
"type": "address"
},
{
"name": "exclusivityOverrideBps",
"type": "uint256"
},
{
"name": "inputToken",
"type": "address"
},
{
"name": "inputStartAmount",
"type": "uint256"
},
{
"name": "inputEndAmount",
"type": "uint256"
},
{
"name": "outputs",
"type": "DutchOutput[]"
}
],
"OrderInfo": [
{
"name": "reactor",
"type": "address"
},
{
"name": "swapper",
"type": "address"
},
{
"name": "nonce",
"type": "uint256"
},
{
"name": "deadline",
"type": "uint256"
},
{
"name": "additionalValidationContract",
"type": "address"
},
{
"name": "additionalValidationData",
"type": "bytes"
}
],
"DutchOutput": [
{
"name": "token",
"type": "address"
},
{
"name": "startAmount",
"type": "uint256"
},
{
"name": "endAmount",
"type": "uint256"
},
{
"name": "recipient",
"type": "address"
}
]
},
"values": {
"permitted": {
"token": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
"amount": {
"type": "BigNumber",
"hex": "0x0bebc200"
}
},
"spender": "0x6000da47483062A0D734Ba3dc7576Ce6A0B645C4",
"nonce": {
"type": "BigNumber",
"hex": "0x046832aa305880d33daa871e5041a0cd4853599a9ead518917239e2067650401"
},
"deadline": 1697481666,
"witness": {
"info": {
"reactor": "0x6000da47483062A0D734Ba3dc7576Ce6A0B645C4",
"swapper": "0x0938a82F93D5DAB110Dc6277FC236b5b082DC10F",
"nonce": {
"type": "BigNumber",
"hex": "0x046832aa305880d33daa871e5041a0cd4853599a9ead518917239e2067650401"
},
"deadline": 1697481666,
"additionalValidationContract": "0x0000000000000000000000000000000000000000",
"additionalValidationData": "0x"
},
"decayStartTime": 1697481594,
"decayEndTime": 1697481654,
"exclusiveFiller": "0xaAFb85ad4a412dd8adC49611496a7695A22f4aeb",
"exclusivityOverrideBps": {
"type": "BigNumber",
"hex": "0x64"
},
"inputToken": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
"inputStartAmount": {
"type": "BigNumber",
"hex": "0x0bebc200"
},
"inputEndAmount": {
"type": "BigNumber",
"hex": "0x0bebc200"
},
"outputs": [
{
"token": "0x0000000000000000000000000000000000000000",
"startAmount": {
"type": "BigNumber",
"hex": "0x01b7d653c183183f"
},
"endAmount": {
"type": "BigNumber",
"hex": "0x01a2e50b6386d671"
},
"recipient": "0x0938a82F93D5DAB110Dc6277FC236b5b082DC10F"
},
{
"token": "0x0000000000000000000000000000000000000000",
"startAmount": {
"type": "BigNumber",
"hex": "0xa926b6321051"
},
"endAmount": {
"type": "BigNumber",
"hex": "0xa118e2ebf2bb"
},
"recipient": "0x37a8f295612602f2774d331e562be9e61B83a327"
}
]
}
}
},
"portionBips": 15,
"portionAmount": "185983730585681",
"portionRecipient": "0x37a8f295612602f2774d331e562be9e61B83a327"
}
},
{
"routing": "CLASSIC",
"quote": {
"methodParameters": {
"calldata": "0x3593564c000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000652d85d0000000000000000000000000000000000000000000000000000000000000000308060c0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000300000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000180000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000bebc20000000000000000000000000000000000000000000000000001bdf1285753b47400000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000002000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000000000000000000000000000000000000000000060000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc200000000000000000000000037a8f295612602f2774d331e562be9e61b83a327000000000000000000000000000000000000000000000000000000000000000f00000000000000000000000000000000000000000000000000000000000000400000000000000000000000000938a82f93d5dab110dc6277fc236b5b082dc10f00000000000000000000000000000000000000000000000001bd45ea74e458eb",
"value": "0x00",
"to": "0x3fC91A3afd70395Cd496C647d5a6CC9D4B2b7FAD"
},
"blockNumber": "18364784",
"amount": "200000000",
"amountDecimals": "200",
"quote": "126149127803342909",
"quoteDecimals": "0.126149127803342909",
"quoteGasAdjusted": "122888348391508943",
"quoteGasAdjustedDecimals": "0.122888348391508943",
"quoteGasAndPortionAdjusted": "122699124699803928",
"quoteGasAndPortionAdjustedDecimals": "0.122699124699803928",
"gasUseEstimateQuote": "3260779411833966",
"gasUseEstimateQuoteDecimals": "0.003260779411833966",
"gasUseEstimate": "240911",
"gasUseEstimateUSD": "5.153332510477604328",
"simulationStatus": "SUCCESS",
"simulationError": false,
"gasPriceWei": "13535203506",
"route": [
[
{
"type": "v2-pool",
"address": "0xB4e16d0168e52d35CaCD2c6185b44281Ec28C9Dc",
"tokenIn": {
"chainId": 1,
"decimals": "6",
"address": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
"symbol": "USDC"
},
"tokenOut": {
"chainId": 1,
"decimals": "18",
"address": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
"symbol": "WETH"
},
"reserve0": {
"token": {
"chainId": 1,
"decimals": "6",
"address": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
"symbol": "USDC"
},
"quotient": "27487668611269"
},
"reserve1": {
"token": {
"chainId": 1,
"decimals": "18",
"address": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
"symbol": "WETH"
},
"quotient": "17390022942803382004255"
},
"amountIn": "200000000",
"amountOut": "125959904111637894"
}
]
],
"routeString": "[V2] 100.00% = USDC -- [0xB4e16d0168e52d35CaCD2c6185b44281Ec28C9Dc] --> WETH",
"quoteId": "f46cf31c-251e-470c-bd57-13209015694e",
"portionBips": 15,
"portionRecipient": "0x37a8f295612602f2774d331e562be9e61B83a327",
"portionAmount": "189223691705014",
"portionAmountDecimals": "0.000189223691705014",
"requestId": "a02ca0ca-7855-4dd0-9330-8b818aaeb59f",
"tradeType": "EXACT_INPUT",
"slippage": 0.5
}
}
]
}

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

@@ -5,7 +5,7 @@
"incremental": true,
"isolatedModules": false,
"noImplicitAny": false,
"target": "ES5",
"target": "ES6",
"tsBuildInfoFile": "../node_modules/.cache/tsbuildinfo/cypress", // avoid clobbering the build tsbuildinfo
"types": ["cypress", "node"],
},

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,6 +13,7 @@
"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",
@@ -20,7 +21,7 @@
"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",
@@ -188,9 +191,10 @@
"@sentry/react": "^7.45.0",
"@sentry/tracing": "^7.45.0",
"@sentry/types": "^7.45.0",
"@types/react-helmet": "^6.1.7",
"@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",
@@ -200,8 +204,8 @@
"@uniswap/sdk-core": "^4.0.3",
"@uniswap/smart-order-router": "^3.15.0",
"@uniswap/token-lists": "^1.0.0-beta.33",
"@uniswap/uniswapx-sdk": "^1.3.0",
"@uniswap/universal-router-sdk": "^1.5.6",
"@uniswap/uniswapx-sdk": "^1.4.1",
"@uniswap/universal-router-sdk": "^1.5.8",
"@uniswap/v2-core": "^1.0.1",
"@uniswap/v2-periphery": "^1.1.0-beta.0",
"@uniswap/v2-sdk": "^3.2.0",
@@ -219,16 +223,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 +256,8 @@
"ms": "^2.1.3",
"multicodec": "^3.0.1",
"multihashes": "^4.0.2",
"nock": "^13.3.3",
"node-fetch": "^3.3.2",
"node-vibrant": "^3.2.1-alpha.1",
"numbro": "^2.3.6",
"polished": "^3.3.2",
@@ -262,6 +268,7 @@
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-feather": "^2.0.8",
"react-helmet": "^6.1.0",
"react-infinite-scroll-component": "^6.1.0",
"react-is": "^17.0.2",
"react-markdown": "^4.3.1",
@@ -291,6 +298,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 />

10
public/robots.txt Normal file
View File

@@ -0,0 +1,10 @@
# *
User-agent: *
Disallow: /static/js/
Allow: /
# Host
Host: https://app.uniswap.org
# Sitemaps
Sitemap: https://app.uniswap.org/sitemap.xml

1975
public/sitemap.xml Normal file

File diff suppressed because it is too large Load Diff

108
scripts/generate-sitemap.js Normal file
View File

@@ -0,0 +1,108 @@
/* eslint-env node */
const fs = require('fs')
const { parseStringPromise, Builder } = require('xml2js')
const weekMs = 7 * 24 * 60 * 60 * 1000
const nowISO = new Date().toISOString()
const getTopTokensQuery = (chain) => `
query {
topTokens(pageSize: 100, page: 1, chain: ${chain}, orderBy: VOLUME) {
address
}
}
`
const chains = ['ETHEREUM', 'ARBITRUM', 'OPTIMISM', 'POLYGON', 'BASE', 'BNB', 'CELO']
const nftTopCollectionsQuery = `
query {
topCollections(first: 100, duration: MAX) {
edges {
node {
nftContracts {
address
}
}
}
}
}
`
fs.readFile('./public/sitemap.xml', 'utf8', async (err, data) => {
const sitemapURLs = {}
try {
const sitemap = await parseStringPromise(data)
if (sitemap.urlset.url) {
sitemap.urlset.url.forEach((url) => {
const lastMod = new Date(url.lastmod).getTime()
if (lastMod < Date.now() - weekMs) {
url.lastmod = nowISO
}
sitemapURLs[url.loc] = true
})
}
for (const chainName of chains) {
const tokensResponse = await fetch('https://api.uniswap.org/v1/graphql', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Origin: 'https://app.uniswap.org',
},
body: JSON.stringify({ query: getTopTokensQuery(chainName) }),
})
const tokensJSON = await tokensResponse.json()
const tokenAddresses = tokensJSON.data.topTokens.map((token) => token.address.toLowerCase())
tokenAddresses.forEach((address) => {
const tokenURL = `https://app.uniswap.org/tokens/${chainName.toLowerCase()}/${address}`
if (!(tokenURL in sitemapURLs)) {
sitemap.urlset.url.push({
loc: [tokenURL],
lastmod: [nowISO],
priority: [0.8],
})
}
})
}
const nftResponse = await fetch('https://api.uniswap.org/v1/graphql', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Origin: 'https://app.uniswap.org',
},
body: JSON.stringify({ query: nftTopCollectionsQuery }),
})
const nftJSON = await nftResponse.json()
const collectionAddresses = nftJSON.data.topCollections.edges.map((edge) => edge.node.nftContracts[0].address)
collectionAddresses.forEach((address) => {
const collectionURL = `https://app.uniswap.org/nfts/collection/${address}`
if (!(collectionURL in sitemapURLs)) {
sitemap.urlset.url.push({
loc: [collectionURL],
lastmod: [nowISO],
priority: [0.7],
})
}
})
const builder = new Builder()
const xml = builder.buildObject(sitemap)
const path = './public/sitemap.xml'
fs.writeFile(path, xml, (error) => {
if (error) throw error
const stats = fs.statSync(path)
const fileSizeBytes = stats.size
const fileSizeMegabytes = fileSizeBytes / (1024 * 1024)
if (fileSizeMegabytes > 50) {
throw new Error('Generated sitemap file size exceeds 50MB')
}
console.log('Sitemap updated')
})
} catch (e) {
console.error(e)
}
})

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

View File

@@ -1,20 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 26.0.1, 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 2496 2496" style="enable-background:new 0 0 2496 2496;" xml:space="preserve">
<g>
<path style="fill-rule:evenodd;clip-rule:evenodd;fill:#F0B90B;" d="M1248,0c689.3,0,1248,558.7,1248,1248s-558.7,1248-1248,1248
S0,1937.3,0,1248S558.7,0,1248,0L1248,0z"/>
<path style="fill:#FFFFFF;" d="M685.9,1248l0.9,330l280.4,165v193.2l-444.5-260.7v-524L685.9,1248L685.9,1248z M685.9,918v192.3
l-163.3-96.6V821.4l163.3-96.6l164.1,96.6L685.9,918L685.9,918z M1084.3,821.4l163.3-96.6l164.1,96.6L1247.6,918L1084.3,821.4
L1084.3,821.4z"/>
<path style="fill:#FFFFFF;" d="M803.9,1509.6v-193.2l163.3,96.6v192.3L803.9,1509.6L803.9,1509.6z M1084.3,1812.2l163.3,96.6
l164.1-96.6v192.3l-164.1,96.6l-163.3-96.6V1812.2L1084.3,1812.2z M1645.9,821.4l163.3-96.6l164.1,96.6v192.3l-164.1,96.6V918
L1645.9,821.4L1645.9,821.4L1645.9,821.4z M1809.2,1578l0.9-330l163.3-96.6v524l-444.5,260.7v-193.2L1809.2,1578L1809.2,1578
L1809.2,1578z"/>
<polygon style="fill:#FFFFFF;" points="1692.1,1509.6 1528.8,1605.3 1528.8,1413 1692.1,1316.4 1692.1,1509.6 "/>
<path style="fill:#FFFFFF;" d="M1692.1,986.4l0.9,193.2l-281.2,165v330.8l-163.3,95.7l-163.3-95.7v-330.8l-281.2-165V986.4
L968,889.8l279.5,165.8l281.2-165.8l164.1,96.6H1692.1L1692.1,986.4z M803.9,656.5l443.7-261.6l444.5,261.6l-163.3,96.6
l-281.2-165.8L967.2,753.1L803.9,656.5L803.9,656.5z"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -1,9 +0,0 @@
<svg width="14" height="16" viewBox="0 0 14 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0.53125 5.04465V10.9554C0.53125 11.3328 0.742546 11.6817 1.08477 11.8698L6.44755 14.8258C6.78977 15.0139 7.21107 15.0139 7.55329 14.8258L12.9161 11.8698C13.2583 11.6817 13.4696 11.3328 13.4696 10.9554V5.04465C13.4696 4.66726 13.2583 4.31833 12.9161 4.13026L7.55329 1.17426C7.21107 0.986184 6.78977 0.986184 6.44755 1.17426L1.08347 4.13026C0.74125 4.31833 0.53125 4.66726 0.53125 5.04465Z" fill="#213147"/>
<path d="M8.17051 9.14643L7.40569 11.1484C7.38495 11.2041 7.38495 11.2648 7.40569 11.3204L8.72143 14.7652L10.2433 13.9263L8.4168 9.14643C8.37532 9.03631 8.21199 9.03631 8.17051 9.14643Z" fill="#12AAFF"/>
<path d="M9.70391 5.77961C9.66243 5.66949 9.4991 5.66949 9.45762 5.77961L8.6928 7.78162C8.67206 7.83731 8.67206 7.89793 8.6928 7.95361L10.8485 13.5934L12.3704 12.7545L9.70391 5.77961Z" fill="#12AAFF"/>
<path d="M7 1.39574C7.03759 1.39574 7.07519 1.40564 7.10889 1.42296L12.9124 4.62147C12.9798 4.65859 13.0213 4.72789 13.0213 4.80089V11.1967C13.0213 11.2709 12.9798 11.339 12.9124 11.3761L7.10889 14.5746C7.07648 14.5932 7.03759 14.6018 7 14.6018C6.96241 14.6018 6.92482 14.5919 6.89111 14.5746L1.08759 11.3786C1.02019 11.3415 0.978704 11.2722 0.978704 11.1992V4.80213C0.978704 4.72789 1.02019 4.65983 1.08759 4.62271L6.89111 1.4242C6.92482 1.40564 6.96241 1.39574 7 1.39574ZM7 0.461548C6.79389 0.461548 6.58648 0.512279 6.40111 0.614978L0.598889 3.81226C0.228148 4.01642 0 4.3938 0 4.80213V11.1979C0 11.6062 0.228148 11.9836 0.598889 12.1878L6.40241 15.3863C6.58778 15.4878 6.79389 15.5397 7.0013 15.5397C7.20741 15.5397 7.41482 15.489 7.60019 15.3863L13.4037 12.1878C13.7744 11.9836 14.0026 11.6062 14.0026 11.1979V4.80213C14.0026 4.3938 13.7744 4.01642 13.4037 3.81226L7.59889 0.614978C7.41352 0.512279 7.20611 0.461548 7 0.461548Z" fill="#9DCCED"/>
<path d="M3.16162 13.6008L3.6957 12.2051L4.77033 13.0576L3.7657 13.9336L3.16162 13.6008Z" fill="#213147"/>
<path d="M6.51113 4.3443H5.03983C4.92965 4.3443 4.83113 4.40988 4.79354 4.50887L1.63965 12.7619L3.1615 13.6008L6.63428 4.51258C6.66669 4.43091 6.60317 4.3443 6.51113 4.3443Z" fill="white"/>
<path d="M9.08579 4.3443H7.6145C7.50431 4.3443 7.40579 4.40988 7.3682 4.50887L3.76709 13.9324L5.28894 14.7713L9.20894 4.51258C9.24005 4.43091 9.17653 4.3443 9.08579 4.3443Z" fill="white"/>
</svg>

Before

Width:  |  Height:  |  Size: 2.3 KiB

View File

@@ -1,11 +0,0 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_298_130193)">
<path d="M12.9346 2.74121H3.05566V11.7259H12.9346V2.74121Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M15.9998 0C15.9998 4.41538 15.9998 15.9947 15.9998 15.9947C11.5844 15.9947 0.00590861 15.9947 0.00590861 15.9947L0.00488281 5.43594e-05C4.42027 5.43594e-05 15.9998 0 15.9998 0ZM5.73493 11.1815H4.18339C3.85736 11.1815 3.69632 11.1815 3.59813 11.1187C3.49207 11.0499 3.42726 10.936 3.4194 10.8103C3.41351 10.6945 3.49404 10.553 3.65508 10.2702L7.48603 3.51765C7.64905 3.2309 7.73153 3.08753 7.83562 3.03451C7.94756 2.97755 8.08112 2.97755 8.19307 3.03451C8.29716 3.08753 8.37965 3.2309 8.54265 3.51765L9.33022 4.89243L9.33423 4.89945C9.51029 5.20707 9.59958 5.36307 9.63856 5.52679C9.68176 5.70552 9.68176 5.89406 9.63856 6.07278C9.59928 6.23775 9.5109 6.39488 9.33218 6.70715L7.31987 10.2643L7.31466 10.2734C7.13744 10.5836 7.04762 10.7408 6.92315 10.8594C6.78763 10.9891 6.62462 11.0833 6.44589 11.1364C6.28288 11.1815 6.10024 11.1815 5.73493 11.1815ZM9.65309 11.1815H11.8763C12.2043 11.1815 12.3693 11.1815 12.4675 11.1168C12.5735 11.048 12.6403 10.9321 12.6463 10.8065C12.6519 10.6944 12.5731 10.5584 12.4188 10.2921C12.4134 10.283 12.4081 10.2738 12.4027 10.2644L11.2891 8.35932L11.2764 8.33787C11.1199 8.07325 11.0409 7.93962 10.9395 7.88797C10.8276 7.831 10.6959 7.831 10.584 7.88797C10.4819 7.94099 10.3994 8.08044 10.2364 8.36128L9.12674 10.2663L9.12294 10.2729C8.9605 10.5533 8.87932 10.6934 8.88518 10.8084C8.89303 10.9341 8.95784 11.0499 9.06389 11.1187C9.16014 11.1815 9.32511 11.1815 9.65309 11.1815Z" fill="#E84142"/>
</g>
<defs>
<clipPath id="clip0_298_130193">
<rect width="16" height="16" fill="white"/>
</clipPath>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 1.8 KiB

View File

@@ -1,11 +0,0 @@
<svg width="28" height="28" viewBox="0 0 28 28" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0 14C0 6.26801 6.26801 0 14 0V0C21.732 0 28 6.26801 28 14V14C28 21.732 21.732 28 14 28V28C6.26801 28 0 21.732 0 14V14Z" fill="#0052FF"/>
<g clip-path="url(#clip0_13924_33076)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M23.3332 14.0003C23.3332 19.155 19.1472 23.3337 13.9836 23.3337C9.08459 23.3337 5.06565 19.5724 4.6665 14.7849H17.0245V13.2158H4.6665C5.06565 8.42825 9.08459 4.66699 13.9836 4.66699C19.1472 4.66699 23.3332 8.84566 23.3332 14.0003Z" fill="white"/>
</g>
<defs>
<clipPath id="clip0_13924_33076">
<rect width="18.6667" height="18.6667" fill="white" transform="translate(4.66675 4.66699)"/>
</clipPath>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 745 B

View File

@@ -1,11 +0,0 @@
<svg width="28" height="28" viewBox="0 0 28 28" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="28" height="28" rx="8" fill="#0052FF"/>
<g clip-path="url(#clip0_13921_13252)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M23.3332 14.0003C23.3332 19.155 19.1472 23.3337 13.9836 23.3337C9.08459 23.3337 5.06565 19.5724 4.6665 14.7849H17.0245V13.2158H4.6665C5.06565 8.42825 9.08459 4.66699 13.9836 4.66699C19.1472 4.66699 23.3332 8.84566 23.3332 14.0003Z" fill="white"/>
</g>
<defs>
<clipPath id="clip0_13921_13252">
<rect width="18.6667" height="18.6667" fill="white" transform="translate(4.66675 4.66699)"/>
</clipPath>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 651 B

View File

@@ -1,21 +0,0 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<rect x="0" y="0" width="16" height="16" rx="3" fill="#F0B90B"/>
<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 2496 2496" style="enable-background:new 0 0 2496 2496;" xml:space="preserve">
<g>
<path style="fill-rule:evenodd;clip-rule:evenodd;fill:#F0B90B;" d="M1248,0c689.3,0,1248,558.7,1248,1248s-558.7,1248-1248,1248
S0,1937.3,0,1248S558.7,0,1248,0L1248,0z"/>
<path style="fill:#FFFFFF;" d="M685.9,1248l0.9,330l280.4,165v193.2l-444.5-260.7v-524L685.9,1248L685.9,1248z M685.9,918v192.3
l-163.3-96.6V821.4l163.3-96.6l164.1,96.6L685.9,918L685.9,918z M1084.3,821.4l163.3-96.6l164.1,96.6L1247.6,918L1084.3,821.4
L1084.3,821.4z"/>
<path style="fill:#FFFFFF;" d="M803.9,1509.6v-193.2l163.3,96.6v192.3L803.9,1509.6L803.9,1509.6z M1084.3,1812.2l163.3,96.6
l164.1-96.6v192.3l-164.1,96.6l-163.3-96.6V1812.2L1084.3,1812.2z M1645.9,821.4l163.3-96.6l164.1,96.6v192.3l-164.1,96.6V918
L1645.9,821.4L1645.9,821.4L1645.9,821.4z M1809.2,1578l0.9-330l163.3-96.6v524l-444.5,260.7v-193.2L1809.2,1578L1809.2,1578
L1809.2,1578z"/>
<polygon style="fill:#FFFFFF;" points="1692.1,1509.6 1528.8,1605.3 1528.8,1413 1692.1,1316.4 1692.1,1509.6 "/>
<path style="fill:#FFFFFF;" d="M1692.1,986.4l0.9,193.2l-281.2,165v330.8l-163.3,95.7l-163.3-95.7v-330.8l-281.2-165V986.4
L968,889.8l279.5,165.8l281.2-165.8l164.1,96.6H1692.1L1692.1,986.4z M803.9,656.5l443.7-261.6l444.5,261.6l-163.3,96.6
l-281.2-165.8L967.2,753.1L803.9,656.5L803.9,656.5z"/>
</g>
</svg>
</svg>

Before

Width:  |  Height:  |  Size: 1.6 KiB

View File

@@ -1,5 +0,0 @@
<svg width="250" height="250" viewBox="0 0 250 250" fill="none" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<rect width="250" height="250" rx="40" fill="#FCFF52"/>
<path style="fill:black;" d="M188.9,60.7H60.7v128.2h128.2v-44.8h-21.3c-7.3,16.3-23.8,27.7-42.7,27.7c-26,0-47.1-21.3-47.1-47.1c0-25.9,21.1-47,47.1-47
c19.3,0,35.8,11.7,43.1,28.4h20.9V60.7z"/>
</svg>

Before

Width:  |  Height:  |  Size: 398 B

View File

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

Before

Width:  |  Height:  |  Size: 5.4 KiB

View File

@@ -0,0 +1,3 @@
<svg width="17" height="16" viewBox="0 0 17 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M11.6376 8.86202C11.8982 9.12269 11.8982 9.54407 11.6376 9.80473L8.97089 12.4714C8.84089 12.6014 8.6702 12.6667 8.49954 12.6667C8.32887 12.6667 8.15818 12.6014 8.02818 12.4714L5.36152 9.80473C5.10085 9.54407 5.10085 9.12269 5.36152 8.86202C5.62218 8.60136 6.04356 8.60136 6.30422 8.86202L8.49954 11.0573L10.6948 8.86202C10.9555 8.60136 11.3769 8.60136 11.6376 8.86202ZM6.30422 7.13807L8.49954 4.94275L10.6948 7.13807C10.8248 7.26807 10.9955 7.33338 11.1662 7.33338C11.3369 7.33338 11.5076 7.26807 11.6376 7.13807C11.8982 6.8774 11.8982 6.45602 11.6376 6.19536L8.97089 3.52869C8.71022 3.26802 8.28885 3.26802 8.02818 3.52869L5.36152 6.19536C5.10085 6.45602 5.10085 6.8774 5.36152 7.13807C5.62218 7.39873 6.04356 7.39873 6.30422 7.13807Z" fill="#5E5E5E"/>
</svg>

After

Width:  |  Height:  |  Size: 866 B

View File

@@ -0,0 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M11.1376 11.5287C11.3982 11.7894 11.3982 12.2107 11.1376 12.4714C11.0076 12.6014 10.8369 12.6667 10.6662 12.6667C10.4955 12.6667 10.3248 12.6014 10.1948 12.4714L7.99954 10.2761L5.80422 12.4714C5.54356 12.7321 5.12218 12.7321 4.86152 12.4714C4.60085 12.2107 4.60085 11.7894 4.86152 11.5287L7.52818 8.86202C7.78885 8.60136 8.21022 8.60136 8.47089 8.86202L11.1376 11.5287ZM7.52818 7.13807C7.65818 7.26807 7.82887 7.33338 7.99954 7.33338C8.1702 7.33338 8.34089 7.26807 8.47089 7.13807L11.1376 4.4714C11.3982 4.21073 11.3982 3.78936 11.1376 3.52869C10.8769 3.26802 10.4555 3.26802 10.1948 3.52869L7.99954 5.724L5.80422 3.52869C5.54356 3.26802 5.12218 3.26802 4.86152 3.52869C4.60085 3.78936 4.60085 4.21073 4.86152 4.4714L7.52818 7.13807Z" fill="#5E5E5E"/>
</svg>

After

Width:  |  Height:  |  Size: 864 B

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

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 32 KiB

View File

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

Before

Width:  |  Height:  |  Size: 2.1 KiB

View File

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

Before

Width:  |  Height:  |  Size: 1.1 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 26 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

@@ -6,13 +6,14 @@ import { TraceEvent } from 'analytics'
import { useToggleAccountDrawer } from 'components/AccountDrawer'
import Row from 'components/Row'
import { MouseoverTooltip } from 'components/Tooltip'
import { BIPS_BASE } from 'constants/misc'
import { useFilterPossiblyMaliciousPositions } from 'hooks/useFilterPossiblyMaliciousPositions'
import { useSwitchChain } from 'hooks/useSwitchChain'
import { EmptyWalletModule } from 'nft/components/profile/view/EmptyWalletContent'
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'
@@ -162,7 +163,7 @@ function PositionListItem({ positionInfo }: { positionInfo: PositionInfo }) {
</ThemedText.SubHeader>
</Row>
}
descriptor={<ThemedText.BodySmall>{`${pool.fee / 10000}%`}</ThemedText.BodySmall>}
descriptor={<ThemedText.BodySmall>{`${pool.fee / BIPS_BASE}%`}</ThemedText.BodySmall>}
right={
<>
<MouseoverTooltip

View File

@@ -1,5 +1,5 @@
import { ChainId } from '@uniswap/sdk-core'
import { BRIDGED_USDC_ARBITRUM, DAI, DAI_ARBITRUM_ONE, USDC_MAINNET } from 'constants/tokens'
import { DAI, DAI_ARBITRUM_ONE, USDC_ARBITRUM, USDC_MAINNET } from 'constants/tokens'
import { render } from 'test-utils/render'
import { PortfolioLogo } from './PortfolioLogo'
@@ -12,7 +12,7 @@ describe('PortfolioLogo', () => {
it('renders with L2 icon', () => {
const { container } = render(
<PortfolioLogo chainId={ChainId.ARBITRUM_ONE} currencies={[DAI_ARBITRUM_ONE, BRIDGED_USDC_ARBITRUM]} />
<PortfolioLogo chainId={ChainId.ARBITRUM_ONE} currencies={[DAI_ARBITRUM_ONE, USDC_ARBITRUM]} />
)
expect(container).toMatchSnapshot()
})

View File

@@ -2,8 +2,8 @@ import { ChainId, Currency } from '@uniswap/sdk-core'
import blankTokenUrl from 'assets/svg/blank_token.svg'
import { ReactComponent as UnknownStatus } from 'assets/svg/contract-interaction.svg'
import { MissingImageLogo } from 'components/Logo/AssetLogo'
import { ChainLogo, getDefaultBorderRadius } from 'components/Logo/ChainLogo'
import { Unicon } from 'components/Unicon'
import { getChainInfo } from 'constants/chainInfo'
import useTokenLogoSource from 'hooks/useAssetLogoSource'
import useENSAvatar from 'hooks/useENSAvatar'
import React from 'react'
@@ -48,25 +48,14 @@ const ENSAvatarImg = styled.img`
width: 40px;
`
const StyledChainLogo = styled.img`
height: 14px;
width: 14px;
`
const SquareChainLogo = styled.img`
height: 100%;
width: 100%;
`
const CircleLogoImage = styled.img<{ size: string }>`
width: ${({ size }) => size};
height: ${({ size }) => size};
border-radius: 50%;
`
const L2LogoContainer = styled.div<{ hasSquareLogo?: boolean }>`
background-color: ${({ theme, hasSquareLogo }) => (hasSquareLogo ? theme.surface2 : theme.neutral1)};
border-radius: 2px;
const L2LogoContainer = styled.div`
border-radius: ${getDefaultBorderRadius(16)}px;
height: 16px;
left: 60%;
position: absolute;
@@ -152,27 +141,21 @@ interface PortfolioLogoProps {
function SquareL2Logo({ chainId }: { chainId: ChainId }) {
if (chainId === ChainId.MAINNET) return null
const { squareLogoUrl, logoUrl } = getChainInfo(chainId)
const chainLogo = squareLogoUrl ?? logoUrl
return (
<L2LogoContainer hasSquareLogo={!!squareLogoUrl}>
{squareLogoUrl ? (
<SquareChainLogo src={chainLogo} alt="chainLogo" />
) : (
<StyledChainLogo src={chainLogo} alt="chainLogo" />
)}
<L2LogoContainer>
<ChainLogo chainId={chainId} />
</L2LogoContainer>
)
}
// TODO(WEB-2983)
/**
* Renders an image by prioritizing a list of sources, and then eventually a fallback contract icon
*/
export function PortfolioLogo(props: PortfolioLogoProps) {
return (
<StyledLogoParentContainer>
<StyledLogoParentContainer style={props.style}>
{getLogo(props)}
<SquareL2Logo chainId={props.chainId} />
</StyledLogoParentContainer>

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

@@ -37,11 +37,6 @@ exports[`PortfolioLogo renders with L2 icon 1`] = `
left: 0;
}
.c4 {
height: 14px;
width: 14px;
}
.c2 {
width: 40px;
height: 40px;
@@ -49,8 +44,7 @@ exports[`PortfolioLogo renders with L2 icon 1`] = `
}
.c3 {
background-color: #222222;
border-radius: 2px;
border-radius: 4px;
height: 16px;
left: 60%;
position: absolute;
@@ -84,17 +78,41 @@ exports[`PortfolioLogo renders with L2 icon 1`] = `
/>
<img
class="c2"
src="https://raw.githubusercontent.com/Uniswap/assets/master/blockchains/arbitrum/assets/0xFF970A61A04b1cA14834A43f5dE4533eBDDB5CC8/logo.png"
src="https://raw.githubusercontent.com/Uniswap/assets/master/blockchains/arbitrum/assets/0xaf88d065e77c8cC2239327C5EDb3A432268e5831/logo.png"
/>
</div>
<div
class="c3"
>
<img
alt="chainLogo"
class="c4"
src="arbitrum_logo.svg"
/>
<svg
aria-labelledby="titleID"
height="16"
width="16"
>
<title
id="titleID"
>
Arbitrum logo
</title>
<rect
fill="#F9F9F9"
height="16"
rx="4"
width="16"
/>
<rect
fill="#00A3FF33"
height="16"
rx="4"
width="16"
/>
<svg
height="16"
width="16"
>
arbitrum.svg
</svg>
</svg>
</div>
</div>
</div>

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%;

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