Compare commits

..

161 Commits

Author SHA1 Message Date
Jordan Frankfurt
d23b6e5da6 fix: remove app advert on mobile safari (#6630)
Co-authored-by: Jordan Frankfurt <jordan@CORN-Jordan-949.frankfurt>
2023-05-25 10:36:57 -05:00
Zach Pomerantz
44c355c7f0 build: caching i18n extractor (#6619)
* fix: do not attempt to cache i18n:extract

* fix: i18n extraction

* docs: improve comments
2023-05-24 14:23:40 -07:00
Vignesh Mohankumar
e4a9764a12 fix: increase useBestTrade debounce time (#6631)
* fix: increase useBestTrade debounce time

* reduce

* increase

* 350

* fix
2023-05-24 14:44:18 -04:00
Mike Grabowski
303fa15240 feat: show affordance in swap UI when we can't fetch usd quote (#6622)
* initial commit:

* add todo to linear
2023-05-24 22:32:53 +04:00
eddie
d180aef306 fix: no-undefined-or in object field types (#6640) 2023-05-24 11:31:40 -07:00
Mike Grabowski
c07c401189 feat: add animation to Settings menu (#6617)
* feat: add price impact back

* chore: update tes tname

* chore: update snapshot for price impact

* fix

* fix

* update snapshot after rebase

* update snapshot

* chore: finish

* chore: remove snapshot

* feat: add test matcher

* cleanup

* chore: add animation test

* add comment

* update comment
2023-05-24 22:02:59 +04:00
Zach Pomerantz
65d91eb363 build: use repository slack secret (#6639) 2023-05-24 13:15:28 -04:00
Jordan Frankfurt
bd4042aa16 chore: update pr template (#6634)
Co-authored-by: Jordan Frankfurt <jordan@CORN-Jordan-949.frankfurt>
2023-05-23 14:53:07 -07:00
github-actions[bot]
1dcafd2f2d chore(i18n): new Crowdin translations (#6215)
* chore(i18n): synchronize translations from crowdin [skip ci]

* chore: trigger actions

---------

Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
Co-authored-by: Zach Pomerantz <zzmp@uniswap.org>
2023-05-23 14:39:58 -07:00
Jordan Frankfurt
66fcdb4465 feat: improve yarn prepare scripts (#6609)
* feat: improve yarn prepare scripts

* reset yarn.lock to main

* pr feedback

* Update scripts/prepare.js

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

* Update scripts/prepare.js

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

* Update scripts/prepare.js

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

* Update scripts/prepare.js

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

* Update scripts/prepare.js

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

* Update scripts/prepare.js

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

* Update scripts/prepare.js

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

* pr feedback

* switch to using concurrently

* yarn dedupe

---------

Co-authored-by: Jordan Frankfurt <jordan@CORN-Jordan-949.frankfurt>
Co-authored-by: Jordan Frankfurt <jordan@corn-jordan-949.lan>
Co-authored-by: Zach Pomerantz <zzmp@uniswap.org>
2023-05-23 16:07:05 -05:00
Jack Short
e398e8b950 fix: allow unsupported chain in pwat (#6629)
chore: allow unsupported chain in pwat
2023-05-23 13:36:28 -04:00
Tina
6424fdfbcd fix: Simplify event logging for SWAP_QUOTE_RECEIVED (#6628)
* simplify event logging

* remove unused function parameter
2023-05-23 11:06:18 -04:00
eddie
95814e3271 fix: prevent race condition for swap state (#6624) 2023-05-22 15:57:17 -07:00
Jack Short
caa2524e27 feat: [DetailsV2] instant buy (#6599)
* initial impl

* removing isopen change

* stopping refetching

* shared button

* pending animiation

* updating shared

* updating snapshots

* adding disabled state

* isLoading in hook

* pulling out ternary

* removing fragment

* separate file for offer button

* fixing price diff check

* remove unnecessary export

* changing name to useBuyAssetCallback
2023-05-22 18:28:48 -04:00
Zach Pomerantz
d28a4b34cd fix: do not attempt to cache i18n:extract (#6616) 2023-05-22 14:03:11 -07:00
cartcrom
f3a80c6272 feat: special case arb search (#6584)
* feat: special case arb search

* fix: check both current and existing token
2023-05-22 12:40:46 -04:00
Zach Pomerantz
b89ee36448 test(e2e): attempt to de-flake (#6611)
* test(e2e): improve memory mgmt

* test(e2e): record flakes

* test(e2e): simplify tests in attempt to de-flake

* test(e2e): more simplification

* test(e2e): disable transaction popup checks

* test(e2e): better wrap assertions

* test(e2e): always assert both inputs
2023-05-22 09:02:54 -07:00
Vignesh Mohankumar
fbc55db937 chore: remove chunkResponseStatus tag (#6586)
* chore: remove chunkResponseStatus tag

* lint
2023-05-21 17:25:58 -04:00
Jordan Frankfurt
835c62acfa fix: use ephemeral props for styled component (#6607)
* fix: use ephemeral props for styled component

* add
2023-05-20 16:55:37 -05:00
Zach Pomerantz
8fe7c7a0a7 build: notify from notify/test (#6597)
* build: notify from notify/test

* debug

* debug2

* revert debugs
2023-05-19 12:24:11 -07:00
Tina
41113e6e41 fix: Use client side router only for price fetching (#6604)
use client side router only for price fetching
2023-05-19 13:35:29 -04:00
Mike Grabowski
58b25d29a9 feat: expand settings by default when custom values are set (#6603)
feat: expand by default
2023-05-19 21:35:02 +04:00
Zach Pomerantz
a2db3e2719 test(e2e): configure Cypress to post PR status comments (#6591) 2023-05-19 06:32:42 -07:00
Mike Grabowski
b62f9066a7 fix: add price impact back (#6581)
* feat: add price impact back

* chore: update tes tname

* chore: update snapshot for price impact

* fix

* fix

* update snapshot after rebase

* update snapshot
2023-05-19 09:24:06 +04:00
Zach Pomerantz
258f22e037 build: continue-on-error for slack notifications (#6600) 2023-05-18 15:30:43 -07:00
Zach Pomerantz
38b306a80f build: pin github-tag-action (#6598) 2023-05-18 15:30:24 -07:00
Zach Pomerantz
9050f09bfe build: notify from notify/releases (#6596) 2023-05-18 15:30:03 -07:00
Zach Pomerantz
77d46c361a test(e2e): de-flake wrap (#6589)
* test(e2e): mv swap to dir

* test(e2e): split swap/wrap/errors

* test(e2e): de-flake wrap
2023-05-18 13:47:15 -07:00
Charles Bachmeier
4fb48bdd1f fix: only request 1 listing on NFTDetails page (#6602)
only request 1 listing

Co-authored-by: Charles Bachmeier <charlie@genie.xyz>
2023-05-18 13:35:01 -07:00
cartcrom
cf2b6bf568 fix: portfolio balances after switching accounts (#6527)
* fix: handle portfolio staleness upon account change

* fix: only consider current account for tx updates

* test: add e2e test for account switching

* fix: remove unnused data-testid

* fix: added todo comment

* refactor: move test into existing folder

* fix: add account to dependency array

* todo: tx reducer

* test: update cypress config to pass in env variables

* fix: undo unintended change

* fix: use process.env

* fix: use regex instead of env
2023-05-18 16:32:35 -04:00
eddie
03095f4e48 feat: add feature flag for URA (#6593) 2023-05-18 10:14:18 -07:00
lavalamp
b2966f8d29 ci: Fix YAML spacing (#6592)
Fix YAML spacing
2023-05-18 09:43:02 -07:00
Charles Bachmeier
ef6d1f20ed feat: [DetailsV2] Show data page header when nft scrolled out of view (#6585)
* show data page header when nft scrolled out of view

* add new snapshot test

* useRef for observer

* add comment

---------

Co-authored-by: Charles Bachmeier <charlie@genie.xyz>
2023-05-18 09:41:04 -07:00
lavalamp
10b156ff2b ci: Final CI fixes pass (#6556)
* Final CI fixes pass

* Change cut to awk

* Remove workflow_dispatch

* Start, success, failure messges

* Deploy from main and add comment

* Update .github/workflows/2-deploy-to-staging.yml

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

* Update jobs to have correct comments, simplification in prod

---------

Co-authored-by: Zach Pomerantz <zzmp@uniswap.org>
2023-05-17 14:29:06 -06:00
Zach Pomerantz
146c5f29cf feat: only pre-cache the document (#6580)
* test(e2e): de-flake service-worker

* feat: rm stale cache storage

* fix: put not del

* fix: staging and test

* test: include staging

* fix: log

* test: rm console.log

* fix: unregister before

* test: deflake by restoring state afterwards
2023-05-17 12:10:28 -07:00
Zach Pomerantz
66a3475bf6 test(e2e): split swap tests (#6587)
* test(e2e): mv swap to dir

* test(e2e): split swap/wrap/errors
2023-05-17 09:43:52 -07:00
Zach Pomerantz
f6c393b016 test(e2e): de-flake activity-history (#6583) 2023-05-17 09:43:26 -07:00
cartcrom
15f8d34320 fix: update nonce deduplication logic (#6588)
* fix: update nonce-deduplication logic
* lint
2023-05-16 21:28:19 -04:00
eddie
504e09d3dc feat: new review design (#6451)
* test: swap flow cypress tests

* fix: use default parameter

* feat: use Swap Component on TDP

* feat: auto nav for TDP tokens

* chore: merge

* chore: merge

* chore: merge

* chore: merge

* fix: remove extra inputCurrency URL parsing logic

* fix: undo last change

* fix: pass expected chain id to swap component

* fix: search for default tokens on unconnected networks if needed

* test: e2e test for l2 token

* fix: delete irrelevant tests

* fix: address comments

* fix: lint error

* test: update TDP e2e tests

* fix: use pageChainId for filter

* fix: rename chainId

* fix: typecheck

* fix: chainId bug

* fix: chainId required fixes

* fix: bad merge in e2e test

* fix: remove unused test util

* fix: remove unnecessary variable

* fix: token defaults

* fix: address comments

* fix: address comments and fix tests

* fix: e2e test formatting, remove Maybe<>

* fix: remove unused variable

* fix: use feature flag for swap component on TDP

* fix: back button

* feat: copy review screen UI from widgetg

* fix: modal padding

* feat: add final detail row

* fix: remove widget comment

* fix: update unit tests

* fix: code style consistency

* fix: remove padding from AutoColumn

* fix: update snapshots

* fix: use semantic gaps

* fix: more px and gaps

* fix: design feedbacks

* fix: button radius in summary modal

* fix: design nits

* feat: update design of summary modal

* fix: font weight and vertical spacing

* fix: update snapshots

* fix: css nits

* fix: modal flicker when refetching trade

* fix: comments

* fix: code style improvements

* feat: require trade to be defined

* fix: remove extra props from ThemedTexts

* fix: one more trans

* fix: remove unused export

* feat: remove undefined checks and other fixes

* fix: update test

* fix: add missing dollar sign

* fix: remove null check and update test

* fix: remove max width from detail row value

* fix: remove isOpen prop

* fix: isopen
2023-05-16 15:15:30 -07:00
Vignesh Mohankumar
1f755e8b0d feat: add retry logic for dynamic imports (#6512)
* feat: add retry logic for lazy import

* try again

* add tests

* refactor: moves retry helper to subfolder

* missing-files

* fix

* doc comment

* tsdoc

* fake timers

* fix

* add eslint rule

* try again?

* try again?

* only dynamic

* try again

* try again

* IT WORKS

* add retry

* fix

* add test

* warn -> error

* lint

* lint

* lint

* add back cache

* rm test

* try again

* real timers but really short intervals

* try returning the promise?

* try returning the promise?

* try this package

* retry

* Update src/utils/retry.ts

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

* Update rules/enforce-retry-on-import.js

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

* Update rules/enforce-retry-on-import.js

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

* eslint_rules

* test fixes

* name

* fix

---------

Co-authored-by: Zach Pomerantz <zzmp@uniswap.org>
2023-05-16 16:53:22 -04:00
Vignesh Mohankumar
f45a7f921b fix: handle switchChain failure in swap (#6507)
* fix: handle switchChain failure in swap

* comment

* fix
2023-05-16 16:47:13 -04:00
Vignesh Mohankumar
29db61ff90 fix: filter error caused by missing meta tags (#6546)
* fix: filter error caused by missing meta tags

* fix
2023-05-16 16:40:25 -04:00
Tina
8431ad9161 chore: Refactor swap request flow (#6499)
* Refactor swap quote flow with widget logic

* remove console logging

* add ignore path for serialization check and pass in native currencies for client side routing

* apply stashed changes

* revert node version change

* remove TODO comment because maybe no longer relevant

* update unit tests

* wip: add snapshot test

* add snapshot test for gas estimate badge

* address PR comments: rename variables, fix client side router initialization

* update Trade type

* add TODO comment about isExactInput util

* change | undefined convention to ?

* PR comments

* update type

* remove client side initialization logic and replace with TODO

* use routing-api for price fetching trades too

* remove QuoteType.Initialized
2023-05-16 16:33:46 -04:00
Vignesh Mohankumar
fd1aded517 fix: remove trailing slash from request url (#6542)
* fix: remove trailing slash from request url

* moves cast

* {}
2023-05-16 12:36:24 -04:00
Vignesh Mohankumar
27ad7cbd41 test: move all tests to beforeSend (#6513)
* beforeSend tests

* fix

* refactor: filterKnownErrors -> shouldRejectError (#6547)

* refactor: filterKnownErrors -> shouldRejectError

* no unknown

* comments
2023-05-16 12:36:17 -04:00
Joshua DeCristi
01e5de436a fix: "Minimum output" should be "Maximum output" when trade is Exact Output (#6565)
* fix: minimum output should be showing maximum input when trade type is exact output

* fix: test for minimum output should be showing maximum input when trade type is exact output

---------

Co-authored-by: Josh DeCristi <joshdecristi@Joshs-MacBook-Pro.local>
2023-05-16 12:07:39 -04:00
Charles Bachmeier
fd5aa1b51e fix: use padding component (#6579)
* fix: rm unused const

* use component

* update snapshot

---------

Co-authored-by: Zach Pomerantz <zzmp@uniswap.org>
Co-authored-by: Charles Bachmeier <charlie@genie.xyz>
2023-05-16 08:56:51 -07:00
Mike Grabowski
a6e1a7e6d9 feat: add slippage warning to MenuButton (#6548)
* feat: initial commit

* chore: add unit tests

* chore: move menubutton to sep. component

* chore: simplify styles and add real focused state

* chore: fix tests + some other tweaks

* chore: rename

* test: add snapshot tests

* tweaks
2023-05-16 11:41:14 +04:00
Jack Short
629fe2c144 feat: [DetailsV2] trait bubbles (#6552) 2023-05-15 19:17:45 -04:00
Vignesh Mohankumar
d73763ce75 refactor: imports shared polyfills in setupTests (#6571) 2023-05-15 17:11:28 -04:00
Zach Pomerantz
fe6df38997 build: upgrade to webpack5 with polyfilled Buffer (#6568)
* fix: Revert "fix: Revert "build: upgrade to webpack 5 (#6459)" (#6566)"

This reverts commit 5e591455b3.

* build: polyfill Buffer

* docs: fix comment negation
2023-05-15 14:07:05 -07:00
eddie
719ee0f5b5 fix: loosen permit2 expiration tolerance in e2e tests (#6573) 2023-05-15 10:58:05 -07:00
Charles Bachmeier
75bdf9a8d4 feat: [DetailsV2] Add left padding to trait rows and headers (#6534)
* feat: [DetailsV2] Add left padding to trait rows

* update snapshot

---------

Co-authored-by: Charles Bachmeier <charlie@genie.xyz>
2023-05-15 10:40:38 -07:00
Tina
efbe3994bb fix: catch RouterPreference.AUTO case for routing-api usage (#6572)
catch AUTO case for routing-api
2023-05-15 13:38:59 -04:00
Vignesh Mohankumar
93fe8e4349 fix: polyfill ResizeObserver (#6553)
* polyfill

* lint

* polyfill test

* dedupe
2023-05-15 12:39:31 -04:00
Vignesh Mohankumar
6062f615a0 build: change automated release to 16:00 UTC (#6567) 2023-05-15 12:25:49 -04:00
Charles Bachmeier
42e3af7b5c feat: [DetailsV2] Offer and Listing Tables (#6515)
* added home icon, basic content container with scroll behaviour

* add more struct

* add timeUntil util, add main structure of generic component, basic mock data

* propagate asset

* actual fake data

* working scroll

* proper alignment

* 1155 quantity

* small window sizes

* more action buttons

* cleanup

* update snapshot

* add tests

* add new test files

* add outline and hide usd price for certain screen sizes

* use sell order data

* update tests

* fetch multiple listings

* better price width on select screens

* mobile icon for approve

* bottom padding on mobile

* update snapshot

* use test objs in tests

* update query

* add border between rows

* update page padding

* breakpoint overlap

* simplified sellOrder check

* external link

* upstream button and better mobile padding

* add file and update tests

---------

Co-authored-by: Charles Bachmeier <charlie@genie.xyz>
2023-05-15 09:14:09 -07:00
Vignesh Mohankumar
57274a800d fix: don't console.error for WalletConnect modal close (#6559) 2023-05-15 12:13:34 -04:00
Tina
5e591455b3 fix: Revert "build: upgrade to webpack 5 (#6459)" (#6566)
Revert "build: upgrade to webpack 5 (#6459)"

This reverts commit ec547ab100.
2023-05-15 09:48:27 -04:00
Zach Pomerantz
ec547ab100 build: upgrade to webpack 5 (#6459)
* fix: import default from json

* fix: fallback to path-browserify

* build: upgrade to webpack5

* test: update size-tests to reflect single entry

* test: increase service-worker timeout

* docs: improve comments

* build: rm MiniCssExtract workaround

* docs: even better comments

* test: back out longer test

* build: vendor chunk

* test: increase sw timeout

* fix: justified splitChunks config

* better explanation

* fix: longer timeout

* fix: caching

* merge and rm duplication

* build
2023-05-12 12:55:01 -07:00
Charles Bachmeier
9de76c69ae feat: [DetailsV2] Data Page Header (#6549)
* hide header on mobile

* add buy and offer buttons, thumbnail, text

* handle no sell orders and add tests

* rehide on mobile

* breakpoint optimizations

* design feedback

---------

Co-authored-by: Charles Bachmeier <charlie@genie.xyz>
2023-05-12 14:48:26 -04:00
Charles Bachmeier
85d1b90197 feat: [DetailsV2] Mobile support for traits (#6535)
Co-authored-by: Charles Bachmeier <charlie@genie.xyz>
2023-05-12 13:39:31 -04:00
lavalamp
38af86e1bb ci: More CI pipeline fixes (#6550)
Move git config
2023-05-12 10:03:45 -07:00
Zach Pomerantz
11a8df2a3e build: report test failures via Slack (#6539)
* build: report test failures
2023-05-12 09:19:20 -07:00
Vignesh Mohankumar
3726b6bb47 fix: remove top-level Fragment (#6540) 2023-05-12 11:19:52 -04:00
Vignesh Mohankumar
bfde34c774 refactor: moves retry helper to subfolder (#6531) 2023-05-12 11:08:14 -04:00
Tina
bd8113d018 chore: Goodbye widget :( (#6543)
* goodbye widget :(

* remove unused function

* modify trace tests

* fix lint

* is github down?
2023-05-12 11:05:22 -04:00
lavalamp
14e3ef044e ci: CI pipeline fixes for merge issues (#6529)
* CI fixes

* update text content

* Change PR to force push

* releases environment for prod deploy

* add runs-on

* Rename third step

* Update .github/workflows/1-main-to-staging.yml

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

* Update .github/workflows/1-main-to-staging.yml

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

* nits

---------

Co-authored-by: Zach Pomerantz <zzmp@uniswap.org>
2023-05-12 08:30:30 -04:00
Vignesh Mohankumar
4fc4bdcd55 fix: propagates user rejection errors (#6533) 2023-05-11 19:40:02 -04:00
Zach Pomerantz
3733570a89 build: update workflow owners (#6537) 2023-05-11 14:24:52 -07:00
Tina
7a042a5199 chore: Replace widget skeleton with internal swap skeleton (#6524)
* replace widget skeleton with internal version

* remove unuecessary div wrappers

* add snapshot test

* forgot to commit this file earlier

* remove exports

* revert(e2e): waitForAnimations

* convert values to px, refactor a bit to use flex gap instead of padding

* remove unnecessary props

* update snapshot test

---------

Co-authored-by: Zach Pomerantz <zzmp@uniswap.org>
2023-05-11 16:58:35 -04:00
Jordan Frankfurt
6d5e17a6e7 fix: deduplicate remote and local tx lists (#6438)
* fix: deduplicate remote and local tx lists

* add nonce to local transactions

* removes local transactions that have nonces that are duplicates of remote transactions

* add e2e test

* lint fix

* fix types in test

* use supported chain id for reducer tests

* use getReceipt to remove outdated transactions via existing polling setup

* pr nits from cmcewen

* fix lint

* fix test
2023-05-11 13:16:18 -05:00
Jack Short
8301c5892c chore: removing token type check from routing (#6536) 2023-05-11 13:37:28 -04:00
Vignesh Mohankumar
59b757dda0 fix: disables CMC list temporarily (#6532)
* fix: disables CMC list temporarily

* -
2023-05-10 20:32:43 -04:00
Jack Short
92a6ec67b3 feat: [DetailsV2] different media types on landing page (#6492)
* initial layout assuming media type exists

* fixing embeds

* media type function

* updated snapshot test

* initial shadow impl

* better shadows

* audio player controls

* updating tests

* content not available handlers

* errors on all media types

* removing fullscreen from iframe

* adding snapshot tests

* responding to comments

* text align center

* updating tests
2023-05-10 13:10:25 -04:00
lavalamp
1d6a1e90d7 ci: GH action for deploying to production (#6482)
* GH action for deploying to production

* Remove dependency on tests

* Update .github/workflows/4-deploy-to-prod.yml

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

* Update .github/workflows/4-deploy-to-prod.yml

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

* Add tag as action step rather than separate target

* Comments

* Update .github/workflows/4-deploy-to-prod.yml

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

* Specify branches

* Update run name

---------

Co-authored-by: Zach Pomerantz <zzmp@uniswap.org>
2023-05-10 12:59:59 -04:00
Jack Short
01aa3291b3 chore: bubbling up pwat spender from route query (#6481)
* chore: bubbling up pwat spender from route query

* refactoring

* adding loading state

* moving to loading allowance

* responding to comments

* linting
2023-05-10 12:45:58 -04:00
Zach Pomerantz
5539ebedf7 build: update stage 2 name (#6526) 2023-05-10 09:38:31 -07:00
Vignesh Mohankumar
e6adddbf55 refactor: return onDemandEntries instead of mediaURLs (#6514)
* refactor: return onDemandEntries instead of mediaURLs

* lint

* fix test
2023-05-10 11:46:28 -04:00
Mike Grabowski
0050b1e165 feat: new routing diagram (#6510)
* chore: initial commit

* chore: add todo to refactor and simplify if conditional in the future

* chore: update layout

* chore: ui tweaks

* chore: add todo

* chore: change todo

* chore: update UI

* chore: tmp

* feat: rename router preference

* chore: update type

* fix error

* fix one more issue

* finish UI work

* chore: remove unecessary components

* chore: update non-snapshot unit tests

* chore: fix lint

* chore: fix ts

* chore: one more time

* fix

* chore: update snapshots

* chore: add jira tickets

* chore: fix mobile popovers

* chore: add analytics event

* chore: fix padding and send event

* chore: fix loading state

* oops

* chore: address review

* chore: comment

* chore
2023-05-10 19:06:12 +04:00
Vignesh Mohankumar
5bf33ab004 revert(e2e): waitForAnimations (#6525)
Co-authored-by: Zach Pomerantz <zzmp@uniswap.org>
2023-05-10 10:53:53 -04:00
lavalamp
a4cfeecd8c ci: Workflow for pushing to releases/staging (#6319)
* Initial draft of releases/staging force push

* Rename

* Update PAT, test on PR target

* Remove pull request target, add environment

* Update .github/workflows/push-staging.yml

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

* Update .github/workflows/push-staging.yml

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

* Update .github/workflows/push-staging.yml

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

* comments

* Update step name

---------

Co-authored-by: Zach Pomerantz <zzmp@uniswap.org>
2023-05-10 08:29:31 -04:00
lavalamp
76cbfdd0b9 ci: GH action for promoting staging -> prod (#6366)
* Add action for promoting to prod

* Update .github/workflows/pr-staging-to-prod.yml

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

* Update .github/workflows/pr-staging-to-prod.yml

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

* Update .github/workflows/pr-staging-to-prod.yml

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

* Update .github/workflows/pr-staging-to-prod.yml

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

* Update .github/workflows/pr-staging-to-prod.yml

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

* Comments

* Comments

* Remove commit message

* Fix service account reference, add CODEOWNERS

* Update .github/workflows/pr-staging-to-prod.yml

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

* Nits

* Fix newline

---------

Co-authored-by: Zach Pomerantz <zzmp@uniswap.org>
2023-05-10 08:29:20 -04:00
lavalamp
0db9e51e41 ci: Staging deploy GH action definition (#6455)
* Staging deploy GH action definition

* Remove workflow_dispatch

* Update src/utils/env.ts

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

* Restrict sentry reporting to staging environment

* Rename and change environment

---------

Co-authored-by: Zach Pomerantz <zzmp@uniswap.org>
2023-05-10 08:28:59 -04:00
Vignesh Mohankumar
82e7925a17 fix: properly filter AbortErrors (#6519)
* fix: properly filter AbortErrors

* lol
2023-05-09 19:04:41 -04:00
Zach Pomerantz
2150347ba2 build: cache generated files across builds (#6495)
* build: cache generated files across builds

* docs: lingui pkg-up comment

* docs: explain clean extraction
2023-05-09 15:49:53 -07:00
Vignesh Mohankumar
2f80646ddd fix: add chunkResponseStatus tag (#6509)
* fix: add chunkResponseStatus tag

* add-tests

* fix tests

* tests

* description

* comments

* comment

* move

* comment

* lint
2023-05-09 17:44:58 -04:00
matteen
55eea6a724 chore(deps): bump @uniswap/token-lists (#6520)
* chore(deps): bump @uniswap/token-lists

* run yarn deduplicate
2023-05-09 17:34:05 -04:00
matteen
709a70652f feat: add tokenlist validation (#6504)
* feat: add tokenlist validation

* use alternative for spread operator

* maintain tokenlists version and use original ajv version

* bump ajv

* Revert "bump ajv"

This reverts commit b9d2dd61c6.

* rename vars in validator

* update gitignore

* nit fixes

* test

* add ^ back

* remove ^

* removed and readded ajv

* try require.resolve

* Revert "try require.resolve"

This reverts commit 62f58bcb7f.

* bump eslint-config

* yarn lock merge conflict

* bring back spread operator

* remove redundant lint ignore
2023-05-09 16:34:15 -04:00
Jack Short
5a7a041f12 feat: [DetailsV2] background for nft details (#6496)
* feat: background for nft details

* fixing top spacing

* adding new states for zindex
2023-05-09 16:08:28 -04:00
cartcrom
b60d98fc17 fix: loading spinner console bug (#6518)
fix: update svg path
2023-05-09 14:59:26 -04:00
Zach Pomerantz
38d9ab67eb test(e2e): token details and test cleanup (#6516)
* test(e2e): split e2e commands/setup

* test(e2e): consolidate tests

* test(e2e): simplify

* test(e2e): mv token details tests to appropriate spec

* test(e2e): rm obsolete constant

* test(e2e): comments

* test(e2e): do not wait for animations
2023-05-09 10:33:55 -07:00
Jordan Frankfurt
5e6ef1575b test: skip flakey e2e test (#6517) 2023-05-09 11:07:13 -05:00
Vignesh Mohankumar
4a015e9d0d fix: ensure event.request is defined in trace (#6498)
* fix: ensure event.request is defined in trace

* add check for empty event
2023-05-09 11:44:07 -04:00
Mike Grabowski
c383a0a0a2 feat: new Settings menu for Swap (#6480)
* feat: initial commit

* remove extra check

* chore: divider fix

* feat: switch from boolean to routerPreference enum

* chore: align name

* chore: fix two errors

* chore: clean up

* chore: entire radio button clickable

* chore: remove unused toggle component

* Revert "chore: remove unused toggle component"

This reverts commit 42858a02b5.

* feat: rewrite slippage

* feat: Slippage

* chore: tbd tomorrow

* Update src/state/user/reducer.ts

Co-authored-by: Tina <59578595+tinaszheng@users.noreply.github.com>

* feat: replace auto with Slippage enum

* chore: add todo for deadline

* chore: cleanup

* feat: improve autoslippage

* chore: replace price with auto

* chore: fix lint

* test: add coverage for Expand

* chore: fix tests

* chore: review feedback part 1

* chore: rework warning

* chore: add jira tickets

* feat: add tests for useUserSlippageTolerance

* chore: add some more

* chore: one more

* add tests for slippage

* chore: add unit tests for transactionsettings

* remove

* revet changes to improve coverage

* chore: update to figma caption

* chore

* chore

* chore: update wording

* Update src/components/Expand/index.tsx

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

* fix: issue with new value, update confusing migration comment

* chore: remove opacity animation temporarily

* chore: update snapshot test

* chore: fix e2e + update comment

* chore: fix tests

* chore: fix tests

---------

Co-authored-by: Tina <59578595+tinaszheng@users.noreply.github.com>
Co-authored-by: Zach Pomerantz <zzmp@uniswap.org>
2023-05-09 10:33:25 -04:00
Vignesh Mohankumar
d6e92804ad fix: filter AbortError from Sentry (#6497) 2023-05-08 18:25:00 -04:00
Zach Pomerantz
f0d8f8b23b test(e2e): mock uniswap token lists from ipfs (#6511)
* test(e2e): mock uniswap token lists from ipfs

* test(e2e): correctly intercept
2023-05-08 15:04:02 -07:00
Charles Bachmeier
ff080aa957 fix: update OS listing fee (#6505)
* update OS listing fee

* remove redundancy

---------

Co-authored-by: Charles Bachmeier <charlie@genie.xyz>
2023-05-08 10:28:01 -07:00
cartcrom
04d9ff7d71 feat: update useAllTokensMultichain usage (#6493)
* feat: updates useAllTokensMultichain to return userAddedTokens

* fix: use correct types in tests

* chore: add documentation for future removal of TokenAddressMap

* fix: use doc comments

* test: add unit test for useAllTokensMultichain

* fix: check userAddedTokens for undefined
2023-05-05 18:15:27 -04:00
cartcrom
406893d99a test(e2e): permit2 approval (#6488)
* build: upgrade cypress-hardhat

* test: reset hardhat between tests

* test: enable auto-mining

* build: upgrade cypress-hardhat again

* test: permit2 e2e tests

* test: user approval rejection

* refactor: reuse common permit util vars to reduce # of lines

* fix: var casing

* refactor: polish

* refactor: clean up comments and function names

* refactor: remove approval util and update scoping

* fix: further nits
2023-05-05 17:52:22 -04:00
Vignesh Mohankumar
4630720956 fix: handle addEventListener being unsupported on Safari <14 (#6426)
* fix: handle addEventListener being unsupported on Safari

* conditionally call

* Revert "conditionally call"

This reverts commit b6b7d2f017.

* comment

* fix

* callback

* add tests

* lint

* casting

* good pr comments

* fix
2023-05-05 13:36:20 -04:00
Jordan Frankfurt
e73e1540ad fix: errors are responsible for their own grammar (#6490) 2023-05-05 11:38:00 -05:00
Vignesh Mohankumar
3a82642fe6 fix: update service worker to precache css/js files (#6486)
* fix: update service worker to precache css/js files

* add test

* move to utils

* working tests

* fixes

* lint

* change names

* lint

* change return type

* fix

* fix

* changes

* lint

* rename cache

* lint

* fix comments and remove string[] case

* string |

* Update src/serviceWorker/utils.ts

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

---------
2023-05-04 14:29:13 -04:00
Zach Pomerantz
2a50d6a17e test(e2e): reset hardhat between tests (#6487) 2023-05-04 08:31:53 -07:00
eddie
c8a8149127 fix: add amplitude page context to swap flow events (#6489) 2023-05-04 11:20:12 -04:00
Vignesh Mohankumar
38cde648cf fix: add a catch on collect static call (#6491)
* fix: add a catch on collect static call

* comment
2023-05-04 11:07:01 -04:00
Charles Bachmeier
33c099119d chore: refactor Bag to use scroll hook (#6483)
* chore: refactor Bag to use scroll hook

* remove outdated comment

---------

Co-authored-by: Charles Bachmeier <charlie@genie.xyz>
2023-05-03 14:19:27 -07:00
Jordan Frankfurt
40247ff7e0 test(e2e): slippage failure (#6464)
* initial draft

* remove logs

* assertions improvement

* add more comments

* add explicit call to disable automine

* test out a very long timeout

* Revert "test out a very long timeout"

This reverts commit 0fc2666d6f.

* improve test reliability

* remove mock list response

* remove hardhat reset and clean up tests

* simplify assertion

* add zzmp's nits
2023-05-03 16:03:43 -05:00
eddie
30d1de8e84 fix: moonpay modal height (#6439) 2023-05-03 14:05:50 -04:00
Jack Short
0e5328bee9 feat: [DetailsV2] initial details landing page view (#6453)
* feat: initial view of details landing page

* gap

* dynamic font sizes

* added test

* responding to comments

* updating snapshot

* linting
2023-05-03 11:08:48 -04:00
Jordan Frankfurt
d1995bc5a6 test(e2e): deadline passed swap error state (#6444)
* chore: ignore hardhat cache files

* test: add forking hardhat config

* test: install cypress-hardhat

* build: add cypress-hardhat

* fix: lint

* build: add hardhat

* build: add @sentry/types

* fix: better origin

* test: update cypress VisitOptions to include hardhat

* fix: default to connected wallet user state

* test: add a hardhat provider

* build: update imports

* chore: comments

* fix: massage eth_sendTransaction

* feat: example swap test (#6415)

* initial commit

* add intercept for amplitude

* fix: destructure result

* click swap submission

* fix: eth_sendTransaction via bridge

* test works

* finish chain interaction test

---------

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

* swap should render an error when a transaction fails due to a passed deadline

* use mining utils to manage transaction confirmation

* update to use new hardhat syntax and make comments more clear

* test a very long timeout in CI

* Revert "test a very long timeout in CI"

This reverts commit 141c28e15c.

* fiddle with automine settings

* pr feedback

* cleanup

* bump cypress-hardhat

* use setAutomine

* use setAutomine

* remove .reset

---------

Co-authored-by: Zach Pomerantz <zzmp@uniswap.org>
2023-05-02 23:52:07 -05:00
Jordan Frankfurt
4959836c2a test(e2e): (un/)wrap transactions (#6474)
* chore: e2e test (un/)wrap transactions

* wrap button should not be disabled (wait for enabled state)

* use automine

* pr feedback

* configure automine

* make tests stop relying on reset
2023-05-02 23:51:17 -05:00
Charles Bachmeier
ef4a80852d chore: update to listing with seaport 1.5 (#6485)
update to listing with seaport 1.5

Co-authored-by: Charles Bachmeier <charlie@genie.xyz>
2023-05-02 18:46:12 -07:00
Vignesh Mohankumar
c2a972eb75 fix: filter chrome-extension errors (#6475)
* fix: filter chrome-extension errors in Sentry

* check stack
2023-05-02 15:43:37 -04:00
eddie
47a2768d89 chore: remove unused feature flags (#6484) 2023-05-02 15:28:12 -04:00
eddie
ca60caf6b0 feat: use Swap Component on TDP (#6332)
* test: swap flow cypress tests

* fix: use default parameter

* feat: use Swap Component on TDP

* feat: auto nav for TDP tokens

* chore: merge

* chore: merge

* chore: merge

* chore: merge

* fix: remove extra inputCurrency URL parsing logic

* fix: undo last change

* fix: pass expected chain id to swap component

* fix: search for default tokens on unconnected networks if needed

* test: e2e test for l2 token

* fix: delete irrelevant tests

* fix: address comments

* fix: lint error

* test: update TDP e2e tests

* fix: use pageChainId for filter

* fix: rename chainId

* fix: typecheck

* fix: chainId bug

* fix: chainId required fixes

* fix: bad merge in e2e test

* fix: remove unused test util

* fix: remove unnecessary variable

* fix: token defaults

* fix: address comments

* fix: address comments and fix tests

* fix: e2e test formatting, remove Maybe<>

* fix: remove unused variable

* fix: use feature flag for swap component on TDP

* fix: back button
2023-05-02 14:39:44 -04:00
Zach Pomerantz
252acef199 build: cap chunk size to 5MB (#6479)
* build: cap chunk size to 5MB

* test: rm size-tests
2023-05-02 08:46:32 -07:00
Zach Pomerantz
00ecb933ac test(lint): separate multiline comments (#6476) 2023-05-02 08:26:07 -07:00
Vignesh Mohankumar
607d0d443e fix: filter errors with OneKey in stack (#6477)
* fix: filter errors with OneKey in stack

* check stack
2023-05-01 15:32:05 -04:00
Charles Bachmeier
ff0209a78f feat: [DetailsV2] add link and hover state to trait component (#6472)
* hide trait container when asset has no traits

* add traits header row

* trait value rows and scroll behaviour

* row with placeholder values

* add random filler values and proper scrollbar styles

* working rarity graph

* bar border radius

* move rarity graph to its own file

* always show scrim

* working scrim and move traitrow to its own file

* cleanup

* remove padding

* move scrollbar right

* add snapshot tests

* add comment about randomly generated rarities

* cleanup

* only pass traits

* justify

* not important

* cleanup scrim styles

* remove comment

* add scroll state hook

* lint

* update test

* object over map

* remove spaces

* justify content

* add ticket

* add comments

* update snapshot

* add link and hover state to trait component

* correct padding

* respond to comments

* add component and use css for vis

---------

Co-authored-by: Charles Bachmeier <charlie@genie.xyz>
2023-05-01 11:52:49 -07:00
Zach Pomerantz
924e83139b test: keep test output clean (#6469) 2023-05-01 10:58:32 -07:00
Jack Short
4d5cc8267e chore: removing pwat feature flag (#6467)
* chore: removing pwat flag

* removing it from enum

* updating test

* trade is not the same as usingPwat
2023-05-01 11:57:56 -04:00
Charles Bachmeier
6bc7cfc996 feat: [DetailsV2] Add Trait component content (#6460)
* hide trait container when asset has no traits

* add traits header row

* trait value rows and scroll behaviour

* row with placeholder values

* add random filler values and proper scrollbar styles

* working rarity graph

* bar border radius

* move rarity graph to its own file

* always show scrim

* working scrim and move traitrow to its own file

* cleanup

* remove padding

* move scrollbar right

* add snapshot tests

* add comment about randomly generated rarities

* cleanup

* only pass traits

* justify

* not important

* cleanup scrim styles

* remove comment

* add scroll state hook

* lint

* update test

* object over map

* remove spaces

* justify content

* add ticket

* add comments

* update snapshot

* correct padding

---------

Co-authored-by: Charles Bachmeier <charlie@genie.xyz>
2023-05-01 11:43:55 -04:00
Vignesh Mohankumar
97312bb174 fix: match OneKey more generically (#6461)
* fix: match OneKey more generically

* flip

* Update src/tracing/errors.test.ts

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

* Update src/tracing/errors.test.ts

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

* Update src/tracing/errors.test.ts

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

* backslashes

* fix

---------
2023-05-01 11:02:26 -04:00
cartcrom
d0a10fcf8d refactor: activation hook w/ global state (#6413)
* feat: moved tryActivation to global hook/state

* test: activation hook

* fix: merge conflicts

* fix: update file path for render utils in activate.test.ts

* fix: add await for connector deactivation

* fix: pr comment fixes

* fix: update tests

* refactor: use stronger activation state type

* refactor: use global state instead of props in ConnectionErrorView

* fix: re-add uri availability check

* fix: lint

* fix: nits

* fix: css regression

* fix: update test enum usage

* fix: use native disabled attribute

* test: add snapshot tests for different Option states

* fix: zach's PR comments

* test: update snapshots/unit tests

* style: pending boolean names

* fix: updated test import

* docs: added comment explaining analytics difference in wallet connection

* test: assert console.debug calls and fix act() issues

* test: drawer close

* test: import specific drawer fn instead of whole module
2023-04-28 19:14:30 -04:00
Jordan Frankfurt
7a1a476e45 fix: spoof origin and referer (#6468)
* fix: spoof origin and referer

* comments, chaining, and an accurate replication of amplitude response bodies
2023-04-28 17:29:16 -05:00
Vignesh Mohankumar
b3bfc1003a build: upgrade sentry-release action (#6463) 2023-04-28 11:55:28 -04:00
Zach Pomerantz
3b1ef8033b test: upgrade cypress-hardhat (#6462) 2023-04-28 08:46:42 -07:00
Zach Pomerantz
803485b96a build: update default test settings (#6441)
build: coverage on CI by default
2023-04-27 14:34:37 -07:00
Jack Short
6df5d3a701 chore: moving nft test fixtures to their own file (#6456)
* chore: moving nft test fixtures to their own file

* refactoring structure

* renaming file
2023-04-27 17:10:06 -04:00
lynn
2d4eafc6b3 test: unsupported currency footer unit test (#6360)
* tests are complete

* update comment

* remove console log, and test close icon

* init

* lets try something else to avoid timeout

* try splitting up last test for timeout

* undo, it wasnt working

* revert order test

* add comment for sanity check

* test another way

* try userEvent

* increase timeout

* move timeout

* init

* use test constants

* use constants

* change address

* more comprehensive tests

* merge constants and use mocked

* remove comments

* remove dual import

* Update src/components/swap/UnsupportedCurrencyFooter.test.tsx

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

* respond comments

* update tests

* remove console log

* fixes

* add act

---------

Co-authored-by: Zach Pomerantz <zzmp@uniswap.org>
2023-04-27 16:54:05 -04:00
lynn
9b52fea58a test: swap details dropdown unit test (#6349)
* init for swap details dropdown test

* more tests

* complete tests, ready for review

* add to dev deps

* init

* merge main

* init

* use test constants

* use constants

* change address

* more comprehensive tests

* merge with constants

* move noop

* add eslint rule

* return null in noop

* merge

* update snapshot

* constant name

* snapshot change

* lint

* undo eslint change

* Update src/components/swap/SwapDetailsDropdown.test.tsx

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

* Update src/components/swap/SwapDetailsDropdown.test.tsx

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

* Update src/components/swap/SwapDetailsDropdown.test.tsx

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

* respond comments

* update snapshot

* merge main

* user event instead

* add act

* import fix

---------

Co-authored-by: Zach Pomerantz <zzmp@uniswap.org>
2023-04-27 16:53:45 -04:00
Jack Short
b92b286626 chore: updating listing marketplace icons (#6458)
* chore: updating listing marketplace icons

* removing svgs from public

* responding to comments
2023-04-27 16:51:53 -04:00
lynn
a8268728d3 test: advanced swap details component unit test (#6363)
* init

* init

* init

* use test constants

* use constants

* change address

* more comprehensive tests

* move noop

* add eslint rule

* return null in noop

* merge

* checkpoint

* fixes

* add tooltip test

* remove unused file

* merge swap modal header

* add third test

* add test for syncing and loading case

* respond to comments

* more descriptive comments

* update snapshot

* update

* change to act
2023-04-27 16:36:36 -04:00
lynn
ab6debbf46 test: swap modal footer unit test (#6353)
* swap footer modal test

* remove empty test

* init

* merge main and jordan comment

* init

* use test constants

* use constants

* change address

* more comprehensive tests

* merge constants

* fix errors

* remove dual import noop

* update snapshot

* Update src/components/swap/SwapModalFooter.test.tsx

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

* update snapshot

---------

Co-authored-by: Zach Pomerantz <zzmp@uniswap.org>
2023-04-27 15:07:49 -04:00
matteen
4c664645c6 feat: add L2 CG tokenlists (#6292)
* feat: add L2 CG tokenlists

* remove extra line
2023-04-27 12:50:05 -04:00
Jordan Frankfurt
4416a84fd7 fix: disable bnb network option on uniwallet (#6452)
* fix: disable bnb network option on uniwallet

* use supported chain set
2023-04-27 09:16:28 -05:00
lavalamp
eaaff81a67 ci: Emit to slack on merge to releases/* branch (#6318)
* Initial draft of Slack notification on releases/* merges

Test

* Add back proper Slack message content

* Feedback revisions

* Update .github/workflows/notify-slack-on-release-merge.yml

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

* Update .github/workflows/notify-slack-on-release-merge.yml

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

* Update .github/workflows/notify-slack-on-release-merge.yml

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

* Comments

* revert to github env

* Add PR target back

* Revert multi-line

* write to context, add multi-line back

* Try other reference to context

* one other way of settings output...

* Pull from output

* Remove env

* Remove PR target

---------

Co-authored-by: Zach Pomerantz <zzmp@uniswap.org>
2023-04-26 20:32:42 -06:00
Zach Pomerantz
f7b8f945f2 build: allow parallel cypress re-run (#6449)
* build: allow parallel cypress re-run

* build: add a step
2023-04-26 17:02:53 -07:00
Jordan Frankfurt
115c65500b feat: mining mode switch to disable auto-mine (#6450)
* feat: mining mode switch to update so we can make assertions about pending transaction state

* update test to use explicit mining

* Update hardhat.config.js

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

---------

Co-authored-by: Zach Pomerantz <zzmp@uniswap.org>
2023-04-26 15:16:51 -05:00
Jordan Frankfurt
3d8d29fd18 feat: e2e testing - Handle wallet rejection (#6442)
* chore: ignore hardhat cache files

* test: add forking hardhat config

* test: install cypress-hardhat

* build: add cypress-hardhat

* fix: lint

* build: add hardhat

* build: add @sentry/types

* fix: better origin

* test: update cypress VisitOptions to include hardhat

* fix: default to connected wallet user state

* test: add a hardhat provider

* build: update imports

* chore: comments

* fix: massage eth_sendTransaction

* feat: example swap test (#6415)

* initial commit

* add intercept for amplitude

* fix: destructure result

* click swap submission

* fix: eth_sendTransaction via bridge

* test works

* finish chain interaction test

---------

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

* stub sending a transaction to simulate wallet rejection of a transaction

finish test

s

* Update cypress/e2e/swap.test.ts

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

* lint

* fix network connection bug

---------

Co-authored-by: Zach Pomerantz <zzmp@uniswap.org>
2023-04-26 13:02:51 -05:00
Charles Bachmeier
c178da626f chore: add more ip infringing collections to the blocklist (#6446)
* updated blocked collections and don't allow for address search of blocked collections

* revert to array
2023-04-26 10:31:49 -07:00
Zach Pomerantz
e3c589ae41 fix: wallet connection analytics (#6296)
* test: web3

* fix: wallet connection analytics

* test: wallet connection analytics

* build: upgrade web3-react@^8.2

* test: web3

* chore: better merge

* fix: unmock

* fix: use usePrevious

* fix: naming

* fix: deps
2023-04-26 10:27:04 -07:00
Vignesh Mohankumar
6d60aca437 fix: filter OneKey related errors (#6396)
* fix: filter OneKey related errors

* fix

* .app
2023-04-26 11:57:13 -04:00
Charles Bachmeier
c77885c4c0 fix: remove patches (#6448)
remove patches
2023-04-25 16:20:46 -07:00
Zach Pomerantz
01684977b3 test: add a 'hardhat' mock window.ethereum (#6395) 2023-04-25 14:24:57 -07:00
Vignesh Mohankumar
025a84de93 build: analyzes only mapped (#6437)
build: analyze only mapped
2023-04-25 16:59:10 -04:00
Jack Short
0025997175 chore: updating purchasable markets from activity (#6443)
* updating purchasable markets from activity

* renaming

* adding trans + refactor

* snapshot test

* removing snapshot test and only matchin test

* deleting snap
2023-04-25 16:17:36 -04:00
Vignesh Mohankumar
acf97e042c fix: filter WebAssembly compile errors (#6445)
* test

* fix: filter WebAssembly compilition errors

* Update src/tracing/errors.ts

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

---------
2023-04-25 15:35:14 -04:00
Jack Short
771618b1c9 chore: updating marketplace icons (#6424)
* chore: updating marketplace icons

* removing console

* eslint

* uni logo

* nft20

* weird svg id bug

* adding gradient id to other marketplaces

* purchasable markets

* test size inc

* removing the nxyz endpoint

* adding back nxyz endpoint for testing

* graphql enum is purchasable markets

* removing nxyz

* self nit

* changing width and height to size - removing gradientId from props - updating gradient id

* removing purchasable markets

* linting

* adding tests to ensure marketplace

* better description

* needs to check for null and not undefined
2023-04-25 15:20:30 -04:00
Zach Pomerantz
87144de9c8 test: transform vanilla-extract for jest (#6425)
* fix: transform vanilla extract for jest

* lint: vanilla transform

* build: deduplicate

* test: improve transformIgnorePatterns

* fix: broken snapshot

* nits

* Update craco.config.cjs

* build: lockfile

* build: lockfile

* Update vanilla.transform.cjs
2023-04-25 12:06:58 -07:00
Charles Bachmeier
63d0290a50 feat: [DetailsV2] more shared component content (#6420)
* add page break and placeholder files

* container layout

* description tabs

* add table tabs

* mobile wrap

* remove todo

* adaptive padding

* add extensible tabbed component

* add new file

* add snapshot test for empty data page

* update snapshot

* add new snapshot

* undo unrelated snapshot change

* draft: details v2 containers

* Revert "draft: details v2 containers"

This reverts commit 7c6580c974.

* uniform padding and single tab styling

* update snapshot

* update default tab behaviour

* add trait bubble

* working chevrons

* shared bubble tab title

* add placeholder nums for offers and listings

* remove styles and update snapshot

* remove chevron and update snapshot

* use test asset for snapshot

* max width

* convert to map with keys

* remove bubbletext helper

* remove extra brace

* use styled components and memo

* update trait component height

* update snapshot

* Add count field to Tab

* add comment

* update snapshot

* move styled component to shared folder

* update snapshot

---------

Co-authored-by: Charles Bachmeier <charlie@genie.xyz>
Co-authored-by: Jack Short <john.short.tj@gmail.com>
2023-04-25 09:40:43 -07:00
Zach Pomerantz
963121f19b test: prevent async debounce updates (#6427)
* test: prevent async debounce updates

* lint

* fix: actually fix

* fix: rm unused act

* fix: synchronously update popper

* lint

* way better
2023-04-24 16:26:28 -07:00
Zach Pomerantz
58417412f1 build: tighten tsconfig (to include build cache, exclude watch folders) (#6375)
* build: extend cypress tsconfig from build tsconfig

* build: exclude node_modules from watch to improve typechecking perf

* fix: even tighter tsconfigs

* fix: even tighter

* fix: tailor cypress tsconfig to testing

* fix: split them up again

* fix: pr comments
2023-04-24 16:00:04 -07:00
Charles Bachmeier
cd3cff0202 chore: remove graphql feature flag and old query methods (#6384)
* remove graphql feature flag and old query methods

* remove unused fn

* remove redundant names, simplify import

* explicit var naming

* column

* add comment

* update flag check

* undo

* visit /swap in uni search test

* empty array base

---------

Co-authored-by: Charles Bachmeier <charlie@genie.xyz>
2023-04-24 14:03:24 -07:00
Zach Pomerantz
90546fd803 fix: rm non-serializable data (#6430) 2023-04-24 12:59:23 -07:00
Zach Pomerantz
bee558fa1a test: squelch console.debug (#6429) 2023-04-24 12:59:12 -07:00
Zach Pomerantz
87514742f1 test: shard GH action unit tests (#6428)
* test: shard GH action unit tests

* fix: use percent
2023-04-24 12:59:05 -07:00
eddie
75b0320bbe feat: use codecov flags (#6418) 2023-04-24 12:44:41 -07:00
Zach Pomerantz
4966ee56dd fix: mock useWeb3React (#6422)
* fix: mock useWeb3React

* fix: pr nits

* fix: do not import for perf

* fix: update snaps

* fix: import type

* Revert "fix: import type"

This reverts commit d7da5a04d1.
2023-04-24 12:43:55 -07:00
Tina
545fdd2cbe feat: Update Subhead text to match Figma spec (#6307)
* update font weight and line height to match figma

* remove unnecessary fontWeight and lineHeight props

* revert textSecondary change

* revert SubheadSmall change

* remove lineHeight from Subheader for Success screen

* add Trans wrapper to Sweep text

* reduce gap between name and address to 2px

* update snapshot tests for classnames change

* fix failing snapshot

---------

Co-authored-by: Lynn Yu <lynn.yu@uniswap.org>
2023-04-24 15:06:15 -04:00
492 changed files with 58391 additions and 31061 deletions

View File

@@ -2,9 +2,21 @@
require('@uniswap/eslint-config/load')
const rulesDirPlugin = require('eslint-plugin-rulesdir')
rulesDirPlugin.RULES_DIR = 'eslint_rules'
module.exports = {
extends: '@uniswap/eslint-config/react',
extends: ['@uniswap/eslint-config/react'],
plugins: ['rulesdir'],
overrides: [
{
files: ['**/*'],
rules: {
'multiline-comment-style': ['error', 'separate-lines'],
'rulesdir/enforce-retry-on-import': 'error',
'rulesdir/no-undefined-or': 'error',
},
},
{
// Configuration/typings typically export objects/definitions that are used outside of the transpiled package
// (eg not captured by the tsconfig). Because it's typical and not exceptional, this is turned off entirely.

2
.github/CODEOWNERS vendored
View File

@@ -1 +1 @@
@uniswap/web-reviewers
@uniswap/web-admins

48
.github/actions/report/action.yml vendored Normal file
View File

@@ -0,0 +1,48 @@
name: Report
description: Report test failures via Slack
inputs:
name:
description: The name of the failing test
required: true
SLACK_WEBHOOK_URL:
description: The webhook URL to send the report to
required: true
runs:
using: composite
steps:
- uses: slackapi/slack-github-action@007b2c3c751a190b6f0f040e47ed024deaa72844
with:
payload: |
{
"text": "${{ inputs.name }} failing on `${{ github.ref_name }}`",
"blocks": [
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "*${{ inputs.name }} failing on `${{ github.ref_name }}`:* <https://github.com/${{ github.repository}}/actions/runs/${{ github.run_id }}|view failing action>"
}
},
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "_This is blocking pull requests and branch promotions._\n_Please prioritize fixing the build._"
}
}
]
}
env:
SLACK_WEBHOOK_URL: ${{ inputs.SLACK_WEBHOOK_URL }}
SLACK_WEBHOOK_TYPE: INCOMING_WEBHOOK
# The !oncall bot requires its own message:
- uses: slackapi/slack-github-action@007b2c3c751a190b6f0f040e47ed024deaa72844
with:
payload: |
{
"text": "!oncall web"
}
env:
SLACK_WEBHOOK_URL: ${{ inputs.SLACK_WEBHOOK_URL }}
SLACK_WEBHOOK_TYPE: INCOMING_WEBHOOK

View File

@@ -10,11 +10,12 @@ runs:
with:
node-version: 14
registry-url: https://registry.npmjs.org
cache: 'yarn'
# node_modules/.cache is intentionally omitted, as this is used for build tool caches.
- uses: actions/cache@v3
id: install-cache
with:
# node_modules/.cache is intentionally omitted, as this is used for build tool caches.
path: |
node_modules
!node_modules/.cache
@@ -22,3 +23,53 @@ runs:
- if: steps.install-cache.outputs.cache-hit != 'true'
run: yarn install --frozen-lockfile --ignore-scripts
shell: bash
# Validators compile quickly, so caching can be omitted.
- run: yarn ajv
shell: bash
# Contracts are compiled from source. If source hasn't changed, the contracts do not need to be re-compiled.
- uses: actions/cache@v3
id: contracts-cache
with:
path: |
src/abis/types
src/types/v3
key: ${{ runner.os }}-contracts-${{ hashFiles('src/abis/**/*.json', 'node_modules/@uniswap/**/artifacts/contracts/**/*.json') }}
- if: steps.contracts-cache.outputs.cache-hit != 'true'
run: yarn contracts
shell: bash
# GraphQL is generated from schema. The schema is always fetched, but if unchanged, graphql does not need to be re-generated.
- run: yarn graphql:fetch
shell: bash
- uses: actions/cache@v3
id: graphql-cache
with:
path: src/graphql/**/__generated__
key: ${{ runner.os }}-graphql-${{ hashFiles('src/graphql/**/schema.graphql') }}
- if: steps.graphql-cache.outputs.cache-hit != 'true'
run: yarn graphql:generate
shell: bash
# Messages are extracted from source.
# A record of source file content hashes and catalogs is maintained in node_modules/.cache/lingui.
# Messages are always extracted, but extraction may short-circuit from the custom extractor's cache.
- uses: actions/cache@v3
id: i18n-extract-cache
with:
path: node_modules/.cache
key: ${{ runner.os }}-i18n-extract-${{ github.run_id }}
restore-keys: ${{ runner.os }}-i18n-extract-
- run: yarn i18n:extract
shell: bash
# Translations are compiled from messages. If messages haven't changed, the translations do not need to be re-compiled.
- uses: actions/cache@v3
id: i18n-compile-cache
with:
path: src/locales/*.js
key: ${{ runner.os }}-i18n-compile-${{ hashFiles('src/locales/*.po') }}
- if: steps.i18n-compile-cache.outputs.cache-hit !='true'
run: yarn i18n:compile
shell: bash

View File

@@ -8,6 +8,5 @@ updates:
allow:
- dependency-name: '@uniswap/default-token-list'
- dependency-name: '@uniswap/token-lists'
- dependency-name: '@uniswap/widgets'
reviewers:
- 'Uniswap/dependabot-reviewers'

View File

@@ -6,7 +6,7 @@
<!-- Delete inapplicable lines: -->
_JIRA ticket:_
_Linear ticket:_
_Slack thread:_
_Relevant docs:_
@@ -14,9 +14,16 @@ _Relevant docs:_
<!-- Delete this section if your change does not affect UI. -->
## Screen capture
| Before | After (Desktop) | After (Mobile) |
| ------------ |---------------- | -------------- |
| paste_before | past_after | paste_after |
### Before
| Mobile | Desktop |
| ------------ | ------------ |
| paste_before | paste_before |
### After
| Mobile | Desktop |
| ------------ | ----------- |
| paste_after | paste_after |
## Test plan

32
.github/workflows/1-main-to-staging.yml vendored Normal file
View File

@@ -0,0 +1,32 @@
name: 1 | Push main -> staging
# This CI job is responsible for pushing the current contents of the `main` branch to the
# `releases/staging` branch, which will in turn kick off a deploy to the staging environment.
on:
workflow_dispatch:
# https://stackoverflow.com/questions/57921401/push-to-origin-from-github-action
jobs:
push-staging:
name: 'Push to staging branch'
runs-on: ubuntu-latest
environment:
name: push/staging
steps:
- uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab
with:
token: ${{ secrets.RELEASE_SERVICE_ACCESS_TOKEN }}
ref: main
- name: Git config
run: |
git config user.name "UL Service Account"
git config user.email "hello-happy-puppy@users.noreply.github.com"
- name: Add CODEOWNERS file
run: |
echo "@uniswap/web-admins" > CODEOWNERS
git add CODEOWNERS
git commit -m "ci: add global CODEOWNERS"
- name: Git push
run: |
git push origin main:releases/staging --force

View File

@@ -0,0 +1,67 @@
name: 2 | Deploy staging
on:
push:
branches:
- 'releases/staging'
jobs:
deploy-to-staging:
runs-on: ubuntu-latest
environment:
name: deploy/staging
steps:
- name: Send Slack message that deploy is starting
uses: slackapi/slack-github-action@007b2c3c751a190b6f0f040e47ed024deaa72844
continue-on-error: true
with:
payload: |
{
"text": "Staging deploy started for branch: ${{ github.ref_name }}"
}
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
SLACK_WEBHOOK_TYPE: INCOMING_WEBHOOK
- uses: actions/checkout@v3
- uses: ./.github/actions/setup
- run: yarn prepare
- run: yarn build
env:
REACT_APP_STAGING: 1
- name: Setup node@16 (required by Cloudflare Pages)
uses: actions/setup-node@v3
with:
node-version: 16
- name: Update Cloudflare Pages deployment
id: pages-deployment
uses: cloudflare/pages-action@364c7ca09a4b57837c5967871d64a2c31adb8c0d
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
projectName: interface-staging
directory: build
githubToken: ${{ secrets.GITHUB_TOKEN }}
# Cloudflare uses `main` as the default production branch, so we push using the `main` branch so that it can be aliased by a custom domain.
branch: main
- name: Send Slack message about deployment outcome
uses: slackapi/slack-github-action@007b2c3c751a190b6f0f040e47ed024deaa72844
continue-on-error: true
if: always()
with:
payload: |
{
"text": "Staging deploy **${{ steps.pages-deployment.outcome }}** for: ${{ github.ref_name }}"
}
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
SLACK_WEBHOOK_TYPE: INCOMING_WEBHOOK
- name: Upload source maps to Sentry
uses: getsentry/action-release@bd5f874fcda966ba48139b0140fb3ec0cb3aabdd
continue-on-error: true
env:
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
SENTRY_ORG: ${{ secrets.SENTRY_ORG }}
SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }}
with:
environment: staging
sourcemaps: './build/static/js'
url_prefix: '~/static/js'

27
.github/workflows/3-staging-to-prod.yml vendored Normal file
View File

@@ -0,0 +1,27 @@
name: 3 | Push staging -> prod
# This CI job is responsible for force pushing the content of releases/staging to releases/prod. It
# is restricted to web-reviewers through virtue of the GitHub environment protection rules for the
# prod environment.
on:
workflow_dispatch:
jobs:
push-prod:
name: 'Push to prod branch'
runs-on: ubuntu-latest
environment:
name: push/prod
steps:
- uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab
with:
token: ${{ secrets.RELEASE_SERVICE_ACCESS_TOKEN }}
ref: releases/staging
- name: Git config
run: |
git config user.name "UL Service Account"
git config user.email "hello-happy-puppy@users.noreply.github.com"
- name: Git push
run: |
git push origin releases/staging:releases/prod --force

116
.github/workflows/4-deploy-to-prod.yml vendored Normal file
View File

@@ -0,0 +1,116 @@
name: 4 | Deploy prod
on:
push:
branches:
- 'releases/prod'
jobs:
deploy-to-prod:
runs-on: ubuntu-latest
environment:
name: deploy/prod
steps:
- name: Send Slack message that build is starting
uses: slackapi/slack-github-action@007b2c3c751a190b6f0f040e47ed024deaa72844
continue-on-error: true
with:
payload: |
{
"text": "Production deploy started for branch: ${{ github.ref_name }}"
}
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
SLACK_WEBHOOK_TYPE: INCOMING_WEBHOOK
- uses: actions/checkout@v3
- uses: ./.github/actions/setup
- run: yarn prepare
- run: yarn build
- name: Bump and tag
id: github-tag-action
uses: mathieudutour/github-tag-action@d745f2e74aaf1ee82e747b181f7a0967978abee0
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
release_branches: releases/prod
default_bump: patch
- name: Pin to IPFS
id: pinata
uses: anantaramdas/ipfs-pinata-deploy-action@39bbda1ce1fe24c69c6f57861b8038278d53688d
with:
pin-name: Uniswap ${{ steps.github-tag-action.outputs.new_tag }}
path: './build'
pinata-api-key: ${{ secrets.PINATA_API_KEY }}
pinata-secret-api-key: ${{ secrets.PINATA_API_SECRET_KEY }}
- name: Convert CIDv0 to CIDv1
id: convert-cidv0
uses: uniswap/convert-cidv0-cidv1@v1.0.0
with:
cidv0: ${{ steps.pinata.outputs.hash }}
- name: Release
uses: actions/create-release@v1.1.0
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: ${{ steps.github-tag-action.outputs.new_tag }}
release_name: Release ${{ steps.github-tag-action.outputs.new_tag }}
body: |
IPFS hash of the deployment:
- CIDv0: `${{ steps.pinata.outputs.hash }}`
- CIDv1: `${{ steps.convert-cidv0.outputs.cidv1 }}`
The latest release is always mirrored at [app.uniswap.org](https://app.uniswap.org).
You can also access the Uniswap Interface from an IPFS gateway.
**BEWARE**: The Uniswap interface uses [`localStorage`](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage) to remember your settings, such as which tokens you have imported.
**You should always use an IPFS gateway that enforces origin separation**, or our hosted deployment of the latest release at [app.uniswap.org](https://app.uniswap.org).
Your Uniswap settings are never remembered across different URLs.
IPFS gateways:
- https://${{ steps.convert-cidv0.outputs.cidv1 }}.ipfs.dweb.link/
- https://${{ steps.convert-cidv0.outputs.cidv1 }}.ipfs.cf-ipfs.com/
- [ipfs://${{ steps.pinata.outputs.hash }}/](ipfs://${{ steps.pinata.outputs.hash }}/)
${{ steps.github-tag-action.outputs.changelog }}
- name: Setup node@16 (required by Cloudflare Pages)
uses: actions/setup-node@v3
with:
node-version: 16
- name: Update Cloudflare Pages deployment
uses: cloudflare/pages-action@364c7ca09a4b57837c5967871d64a2c31adb8c0d
id: pages-deployment
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
projectName: ${{ secrets.CLOUDFLARE_PROJECT_NAME }}
directory: build
githubToken: ${{ secrets.GITHUB_TOKEN }}
# Cloudflare uses `main` as the default production branch, so we push using the `main` branch so that it can be aliased by a custom domain.
branch: main
- name: Send Slack message about deployment outcome
uses: slackapi/slack-github-action@007b2c3c751a190b6f0f040e47ed024deaa72844
continue-on-error: true
if: always()
with:
payload: |
{
"text": "Production deploy **${{ steps.pages-deployment.outcome }}** for: ${{ github.ref_name }}"
}
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
SLACK_WEBHOOK_TYPE: INCOMING_WEBHOOK
- name: Upload source maps to Sentry
uses: getsentry/action-release@4744f6a65149f441c5f396d5b0877307c0db52c7
continue-on-error: true
env:
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
SENTRY_ORG: ${{ secrets.SENTRY_ORG }}
SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }}
with:
environment: production
sourcemaps: './build/static/js'
url_prefix: '~/static/js'

View File

@@ -0,0 +1,94 @@
name: Slack notifications for releases/* merges
# This CI job will push notifications to Slack whenever code is merged into any releases/* branch
#
# The steps of the command line kung-fu shown below are as follows:
# First we take the JSON-formatted Github context
# echo $GITHUB_CONTEXT \
# Then we parse out the specific fields we want for our messages using jq and format it into tab-separated values
# | jq '.event.commits[] | [.url, .id[0:7], .author.username, .timestamp, .message] | @tsv' \
# We need to do some cleaning on this output - specifically removing quotes and replacing newlines with something easier to split
# | sed 's/"//g' | sed 's/\\t/;/g' | sed 's/\\n/;/g' | sed 's/\\//g' \
# We then use awk to format the TSV into a Slack message
# | awk -F';' '{print "• <"$1"|"$2"> (<https://github.com/"$3"|"$3">, "$4") - "$5}' \
# We need to deal with some escaping issues with newlines so that we don't break the Slack message format
# | sed 's/$/\\n/g' | tr -d '\n' \
# Finally we have to truncate the message to 3,000 characters max, otherwise Slack will reject it
# | awk '{print substr($0,0,3000);}' \
# Then shove the bytes into a file to store them in their exact format
# > /tmp/parsed_github_context
on:
push:
branches:
- 'releases/*'
jobs:
notify-slack:
name: 'Emit Slack notification(s)'
runs-on: ubuntu-latest
environment:
name: notify/releases
steps:
- name: Parse event to slug
id: parse-slug
env:
GITHUB_CONTEXT: ${{ toJson(github) }}
# Formats the contents of the GitHub event into slugs: one line per commit, formatted for Slack.
# Explanation for each line is in the comments above.
run: |
echo $GITHUB_CONTEXT \
| jq '.event.commits[] | [.url, .id[0:7], .author.username, .timestamp, .message] | @tsv' \
| sed 's/"//g' | sed 's/\\t/;/g' | sed 's/\\n/;/g' | sed 's/\\//g' \
| awk -F';' '{print "• <"$1"|"$2"> (<https://github.com/"$3"|"$3">, "$4") - "$5}' \
| sed 's/$/\\n/g' | tr -d '\n' \
| awk '{print substr($0,0,3000);}' \
> /tmp/parsed_github_context
echo "SLACK_COMMITS=$(cat /tmp/parsed_github_context)" >> "$GITHUB_OUTPUT"
- name: Send custom JSON data to Slack workflow
id: slack
uses: slackapi/slack-github-action@007b2c3c751a190b6f0f040e47ed024deaa72844
with:
payload: |
{
"text": "GitHub Action build result: ${{ job.status }}\n${{ github.event.pull_request.html_url || github.event.head_commit.url }}",
"blocks": [
{
"type": "divider"
},
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "*Code merged to <https://github.com/Uniswap/interface/tree/${{ github.ref }}|${{ github.ref_name }}> branch:*\n"
}
},
{
"type": "divider"
},
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "*Actor*: <https://github.com/${{ github.triggering_actor }}/|${{ github.triggering_actor }}>\n*Force pushed*: ${{ github.event.forced || false }}\n"
}
},
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "${{ steps.parse-slug.outputs.SLACK_COMMITS || 'New branch created' }}"
}
},
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "<${{ github.event.compare}}|View Diff>"
}
}
]
}
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
SLACK_WEBHOOK_TYPE: INCOMING_WEBHOOK

View File

@@ -1,7 +1,7 @@
name: Release
on:
schedule:
- cron: '0 12 * * 1-4' # every day 12:00 UTC Monday-Thursday
- cron: '0 16 * * 1-4' # every day 16:00 UTC Monday-Thursday
# manual trigger
workflow_dispatch:
@@ -47,7 +47,6 @@ jobs:
steps:
- uses: actions/checkout@v3
- uses: ./.github/actions/setup
- run: yarn prepare
- run: yarn build
- name: Pin to IPFS
@@ -114,7 +113,7 @@ jobs:
githubToken: ${{ secrets.GITHUB_TOKEN }}
- name: Upload source maps to Sentry
uses: getsentry/action-release@bd5f874fcda966ba48139b0140fb3ec0cb3aabdd
uses: getsentry/action-release@4744f6a65149f441c5f396d5b0877307c0db52c7
continue-on-error: true
env:
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}

View File

@@ -2,6 +2,7 @@ name: Test
# Many build steps have their own caches, so each job has its own cache to improve subsequent build times.
# Build tools are configured to cache cache to node_modules/.cache, so this is cached independently of node_modules.
# Caches are saved every run (by keying on github.run_id), and the most recent available cache is loaded.
# See https://jongleberry.medium.com/speed-up-your-ci-and-dx-with-node-modules-cache-ac8df82b7bb0.
on:
@@ -25,6 +26,11 @@ jobs:
key: ${{ runner.os }}-eslint-${{ hashFiles('**/yarn.lock') }}-${{ github.run_id }}
restore-keys: ${{ runner.os }}-eslint-${{ hashFiles('**/yarn.lock') }}-
- run: yarn lint
- if: failure() && github.ref_name == 'main'
uses: ./.github/actions/report
with:
name: Lint
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_TEST_REPORTER_WEBHOOK }}
typecheck:
runs-on: ubuntu-latest
@@ -37,8 +43,12 @@ jobs:
path: node_modules/.cache
key: ${{ runner.os }}-tsc-${{ hashFiles('**/yarn.lock') }}-${{ github.run_id }}
restore-keys: ${{ runner.os }}-tsc-${{ hashFiles('**/yarn.lock') }}-
- run: yarn prepare
- run: yarn typecheck
- if: failure() && github.ref_name == 'main'
uses: ./.github/actions/report
with:
name: Typecheck
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_TEST_REPORTER_WEBHOOK }}
deps-tests:
runs-on: ubuntu-latest
@@ -46,6 +56,11 @@ jobs:
- uses: actions/checkout@v3
- uses: ./.github/actions/setup
- run: yarn yarn-deduplicate --strategy=highest --list --fail
- if: failure() && github.ref_name == 'main'
uses: ./.github/actions/report
with:
name: Dependency checks
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_TEST_REPORTER_WEBHOOK }}
unit-tests:
runs-on: ubuntu-latest
@@ -58,32 +73,18 @@ jobs:
path: node_modules/.cache
key: ${{ runner.os }}-jest-${{ hashFiles('**/yarn.lock') }}-${{ github.run_id }}
restore-keys: ${{ runner.os }}-jest-${{ hashFiles('**/yarn.lock') }}-
- run: yarn prepare
- run: yarn test --silent
- run: yarn test --coverage --maxWorkers=100%
- uses: codecov/codecov-action@v3
with:
token: ${{ secrets.CODECOV_TOKEN }}
fail_ci_if_error: false
verbose: true
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: ./.github/actions/setup
- uses: actions/cache@v3
id: build-cache
flags: unit-tests
- if: failure() && github.ref_name == 'main'
uses: ./.github/actions/report
with:
path: node_modules/.cache
key: ${{ runner.os }}-build-${{ hashFiles('**/yarn.lock') }}-${{ github.run_id }}
restore-keys: ${{ runner.os }}-build-${{ hashFiles('**/yarn.lock') }}-
- run: yarn prepare
- run: yarn build
- uses: actions/upload-artifact@v3
with:
name: build
path: build
if-no-files-found: error
name: Unit tests
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_TEST_REPORTER_WEBHOOK }}
build-e2e:
runs-on: ubuntu-latest
@@ -96,7 +97,6 @@ jobs:
path: node_modules/.cache
key: ${{ runner.os }}-build-e2e-${{ hashFiles('**/yarn.lock') }}-${{ github.run_id }}
restore-keys: ${{ runner.os }}-build-e2e-${{ hashFiles('**/yarn.lock') }}-
- run: yarn prepare
- run: yarn build:e2e
env:
NODE_OPTIONS: "--max_old_space_size=4096"
@@ -106,21 +106,14 @@ jobs:
path: build
if-no-files-found: error
size-tests:
needs: [build]
# Allows for parallel re-runs of cypress tests without re-building.
cypress-rerun:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: ./.github/actions/setup
- uses: actions/download-artifact@v3
with:
name: build
path: build
- run: yarn test:size
- run: exit 0
cypress-test-matrix:
needs: [build-e2e]
needs: [build-e2e, cypress-rerun]
runs-on: ubuntu-latest
container: cypress/browsers:node-18.14.1-chrome-111.0.5563.64-1-ff-111.0-edge-111.0.1661.43-1
strategy:
@@ -134,7 +127,7 @@ jobs:
id: cypress-cache
with:
path: /root/.cache/Cypress
key: ${{ runner.os }}-cypress
key: ${{ runner.os }}-cypress-${{ hashFiles('**/node_modules/cypress/package.json') }}
- run: |
yarn cypress install
yarn cypress info
@@ -147,24 +140,42 @@ jobs:
- uses: cypress-io/github-action@v4
with:
install: false
record: true
parallel: true
start: yarn serve
wait-on: 'http://localhost:3000'
browser: chrome
record: true
parallel: true
group: e2e
env:
CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
COMMIT_INFO_BRANCH: ${{ github.event.pull_request.head.ref || github.ref_name }}
COMMIT_INFO_MESSAGE: ${{ github.event.pull_request.title || github.event.head_commit.message }}
COMMIT_INFO_AUTHOR: ${{ github.event.sender.login || github.event.head_commit.author.login }}
# Cypress requires an email for filtering by author, but GitHub does not expose one.
# GitHub's public profile email can be deterministically produced from user id/login.
COMMIT_INFO_EMAIL: ${{ github.event.sender.id || github.event.head_commit.author.id }}+${{ github.event.sender.login || github.event.head_commit.author.login }}@users.noreply.github.com
COMMIT_INFO_SHA: ${{ github.event.pull_request.head.sha || github.event.head_commit.sha }}
COMMIT_INFO_TIMESTAMP: ${{ github.event.pull_request.updated_at || github.event.head_commit.timestamp }}
CYPRESS_PULL_REQUEST_ID: ${{ github.event.pull_request.number }}
CYPRESS_PULL_REQUEST_URL: ${{ github.event.pull_request.html_url }}
- uses: codecov/codecov-action@v3
with:
token: ${{ secrets.CODECOV_TOKEN }}
fail_ci_if_error: false
verbose: true
flags: e2e-tests
- if: failure() && github.ref_name == 'main'
uses: ./.github/actions/report
with:
name: Cypress tests
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_TEST_REPORTER_WEBHOOK }}
# Included as a single job to check for cypress-test-matrix success, as a matrix cannot be checked.
cypress-tests:
if: ${{ always() }}
if: always()
needs: [cypress-test-matrix]
runs-on: ubuntu-latest
steps:

6
.gitignore vendored
View File

@@ -8,8 +8,10 @@
/src/locales/**/en-US.po
/src/locales/**/pseudo.po
# generated graphql types
/src/graphql/**/__generated__
# generated files
/src/**/__generated__
# schema
schema.graphql
# dependencies

View File

@@ -9,13 +9,17 @@ ignore:
- "**/constants/**/*"
- "constants/**/*"
coverage:
status:
project:
default:
flag_management:
default_rules:
statuses:
- type: project
target: auto
threshold: 1%
if_ci_failed: error
patch:
default:
target: 80%
- type: patch
target: 80%
individual_flags:
- name: e2e-tests
statuses:
- type: patch
target: 0%

View File

@@ -1,9 +1,11 @@
/* eslint-env node */
const { VanillaExtractPlugin } = require('@vanilla-extract/webpack-plugin')
const CaseSensitivePathsPlugin = require('case-sensitive-paths-webpack-plugin')
const { execSync } = require('child_process')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const CaseSensitivePathsPlugin = require('case-sensitive-paths-webpack-plugin')
const { DefinePlugin, IgnorePlugin } = require('webpack')
const path = require('path')
const ModuleScopePlugin = require('react-dev-utils/ModuleScopePlugin')
const { DefinePlugin, IgnorePlugin, ProvidePlugin } = require('webpack')
const commitHash = execSync('git rev-parse HEAD').toString().trim()
const isProduction = process.env.NODE_ENV === 'production'
@@ -12,6 +14,11 @@ const isProduction = process.env.NODE_ENV === 'production'
// Omit them from production builds, as they slow down the feedback loop.
const shouldLintOrTypeCheck = !isProduction
function getCacheDirectory(cacheName) {
// Include the trailing slash to denote that this is a directory.
return `${path.join(__dirname, 'node_modules/.cache/', cacheName)}/`
}
module.exports = {
babel: {
plugins: [
@@ -44,8 +51,13 @@ module.exports = {
pluginOptions(eslintConfig) {
return Object.assign(eslintConfig, {
cache: true,
cacheLocation: 'node_modules/.cache/eslint/',
cacheLocation: getCacheDirectory('eslint'),
ignorePath: '.gitignore',
// Use our own eslint/plugins/config, as overrides interfere with caching.
// This ensures that `yarn start` and `yarn lint` share one cache.
eslintPath: require.resolve('eslint'),
resolvePluginsRelativeTo: null,
baseConfig: null,
})
},
},
@@ -55,7 +67,13 @@ module.exports = {
jest: {
configure(jestConfig) {
return Object.assign(jestConfig, {
cacheDirectory: 'node_modules/.cache/jest',
cacheDirectory: getCacheDirectory('jest'),
transform: Object.assign(jestConfig.transform, {
// Transform vanilla-extract using its own transformer.
// See https://sandroroth.com/blog/vanilla-extract-cra#jest-transform.
'\\.css\\.ts$': '@vanilla-extract/jest-transform',
}),
// Use @uniswap/conedison's build directly, as jest does not support its exports.
transformIgnorePatterns: ['@uniswap/conedison/format', '@uniswap/conedison/provider'],
moduleNameMapper: {
'@uniswap/conedison/format': '@uniswap/conedison/dist/format',
@@ -65,8 +83,19 @@ module.exports = {
},
},
webpack: {
plugins: [new VanillaExtractPlugin({ identifiers: 'short' })],
plugins: [
// Webpack 5 does not polyfill node globals, so we do so for those necessary:
new ProvidePlugin({
// - react-markdown requires process.cwd
process: 'process/browser',
}),
// vanilla-extract has poor performance on M1 machines with 'debug' identifiers, so we use 'short' instead.
// See https://vanilla-extract.style/documentation/integrations/webpack/#identifiers for docs.
// See https://github.com/vanilla-extract-css/vanilla-extract/issues/771#issuecomment-1249524366.
new VanillaExtractPlugin({ identifiers: 'short' }),
],
configure: (webpackConfig) => {
// Configure webpack plugins:
webpackConfig.plugins = webpackConfig.plugins
.map((plugin) => {
// Extend process.env with dynamic values (eg commit hash).
@@ -83,10 +112,16 @@ module.exports = {
plugin.options.ignoreOrder = true
}
// Disable TypeScript's config overwrite, as it interferes with incremental build caching.
// This ensures that `yarn start` and `yarn typecheck` share one cache.
if (plugin.constructor.name == 'ForkTsCheckerWebpackPlugin') {
delete plugin.options.typescript.configOverwrite
}
return plugin
})
.filter((plugin) => {
// Case sensitive paths are enforced by TypeScript.
// Case sensitive paths are already enforced by TypeScript.
// See https://www.typescriptlang.org/tsconfig#forceConsistentCasingInFileNames.
if (plugin instanceof CaseSensitivePathsPlugin) return false
@@ -96,10 +131,66 @@ module.exports = {
return true
})
// We're currently on Webpack 4.x which doesn't support the `exports` field in package.json.
// Instead, we need to manually map the import path to the correct exports path (eg dist or build folder).
// See https://github.com/webpack/webpack/issues/9509.
webpackConfig.resolve.alias['@uniswap/conedison'] = '@uniswap/conedison/dist'
// Configure webpack resolution:
webpackConfig.resolve = Object.assign(webpackConfig.resolve, {
plugins: webpackConfig.resolve.plugins.map((plugin) => {
// Allow vanilla-extract in production builds.
// This is necessary because create-react-app guards against external imports.
// See https://sandroroth.com/blog/vanilla-extract-cra#production-build.
if (plugin instanceof ModuleScopePlugin) {
plugin.allowedPaths.push(path.join(__dirname, 'node_modules/@vanilla-extract/webpack-plugin'))
}
return plugin
}),
// Webpack 5 does not resolve node modules, so we do so for those necessary:
fallback: {
// - react-markdown requires path
path: require.resolve('path-browserify'),
},
})
// Configure webpack transpilation (create-react-app specifies transpilation rules in a oneOf):
webpackConfig.module.rules[1].oneOf = webpackConfig.module.rules[1].oneOf.map((rule) => {
// The fallback rule (eg for dependencies).
if (rule.loader && rule.loader.match(/babel-loader/) && !rule.include) {
// Allow not-fully-specified modules so that legacy packages are still able to build.
rule.resolve = { fullySpecified: false }
// The class properties transform is required for @uniswap/analytics to build.
rule.options.plugins.push('@babel/plugin-proposal-class-properties')
}
return rule
})
// Configure webpack optimization:
webpackConfig.optimization = Object.assign(
webpackConfig.optimization,
isProduction
? {
splitChunks: {
// Cap the chunk size to 5MB.
// react-scripts suggests a chunk size under 1MB after gzip, but we can only measure maxSize before gzip.
// react-scripts also caps cacheable chunks at 5MB, which gzips to below 1MB, so we cap chunk size there.
// See https://github.com/facebook/create-react-app/blob/d960b9e/packages/react-scripts/config/webpack.config.js#L713-L716.
maxSize: 5 * 1024 * 1024,
// Optimize over all chunks, instead of async chunks (the default), so that initial chunks are also optimized.
chunks: 'all',
},
}
: {}
)
// Configure webpack caching:
webpackConfig.cache = Object.assign(webpackConfig.cache, {
cacheDirectory: getCacheDirectory('webpack'),
})
// Ignore failed source mappings to avoid spamming the console.
// 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.
webpackConfig.ignoreWarnings = [/Failed to parse source map/]
return webpackConfig
},

View File

@@ -1,17 +1,29 @@
import codeCoverageTask from '@cypress/code-coverage/task'
import { defineConfig } from 'cypress'
import { setupHardhatEvents } from 'cypress-hardhat'
import { unlinkSync } from 'fs'
export default defineConfig({
projectId: 'yp82ef',
videoUploadOnPasses: false,
defaultCommandTimeout: 24000, // 2x average block time
chromeWebSecurity: false,
experimentalMemoryManagement: true, // better memory management, see https://github.com/cypress-io/cypress/pull/25462
retries: { runMode: 2 },
e2e: {
async setupNodeEvents(on, config) {
await setupHardhatEvents(on, config)
codeCoverageTask(on, config)
// Delete recorded videos for specs that passed without flakes.
on('after:spec', async (spec, results) => {
if (results && results.video) {
// If there were no failures (including flakes), delete the recorded video.
if (!results.tests?.some((test) => test.attempts.some((attempt) => attempt?.state === 'failed'))) {
unlinkSync(results.video)
}
}
})
return {
...config,
// Only enable Chrome.

View File

@@ -1,8 +0,0 @@
import { defineConfig } from 'cypress'
export default defineConfig({
projectId: 'yp82ef',
e2e: {
specPattern: 'cypress/release.ts',
},
})

View File

@@ -0,0 +1,28 @@
import { getTestSelector } from '../utils'
describe('Buy Crypto Modal', () => {
it('should open and close', () => {
cy.visit('/')
// Open the fiat onramp modal
cy.get(getTestSelector('buy-fiat-button')).click()
cy.get(getTestSelector('fiat-onramp-modal')).should('be.visible')
// Click on a location that should be outside the modal, which should close it
cy.get('body').click(0, 100)
cy.get(getTestSelector('fiat-onramp-modal')).should('not.exist')
})
it('should open and close, mobile viewport', () => {
cy.viewport('iphone-6')
cy.visit('/')
// Open the fiat onramp modal
cy.get(getTestSelector('buy-fiat-button')).click()
cy.get(getTestSelector('fiat-onramp-modal')).should('be.visible')
// Click on a location that should be outside the modal, which should close it
cy.get('body').click(10, 10)
cy.get(getTestSelector('fiat-onramp-modal')).should('not.exist')
})
})

View File

@@ -1,25 +1,27 @@
import { getTestSelector } from '../utils'
import { CONNECTED_WALLET_USER_STATE } from '../utils/user-state'
describe('Landing Page', () => {
it('shows landing page when no selectedWallet', () => {
cy.visit('/', { noWallet: true })
it('shows landing page when no user state exists', () => {
cy.visit('/', { userState: {} })
cy.get(getTestSelector('landing-page'))
cy.screenshot()
})
it('redirects to swap page when selectedWallet is INJECTED', () => {
cy.visit('/', { selectedWallet: 'INJECTED' })
it('redirects to swap page when a user has already connected a wallet', () => {
cy.visit('/', { userState: CONNECTED_WALLET_USER_STATE })
cy.get('#swap-page')
cy.url().should('include', '/swap')
cy.screenshot()
})
it('shows landing page when selectedWallet is INJECTED and ?intro=true is in query', () => {
cy.visit('/?intro=true', { selectedWallet: 'INJECTED' })
it('shows landing page when a user has already connected a wallet but ?intro=true is in query', () => {
cy.visit('/?intro=true', { userState: CONNECTED_WALLET_USER_STATE })
cy.get(getTestSelector('landing-page'))
})
it('shows landing page when the unicorn icon in nav is selected', () => {
cy.visit('/swap')
cy.get(getTestSelector('uniswap-logo')).click()
cy.get(getTestSelector('landing-page'))
})

View File

@@ -0,0 +1,58 @@
import { getTestSelector } from '../../utils'
describe('Mini Portfolio account drawer', () => {
beforeEach(() => {
cy.intercept(/api.uniswap.org\/v1\/graphql/, cy.spy().as('gqlSpy'))
cy.visit('/swap', { ethereum: 'hardhat' })
})
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')
// Balances should have been fetched once after hover
cy.get(getTestSelector('web3-status-connected')).trigger('mouseover')
cy.get('@gqlSpy').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')
// Balances should not be refetched upon opening drawer
cy.get(getTestSelector('web3-status-connected')).click()
cy.get('@gqlSpy').should('have.been.calledOnce')
// Balances should not be refetched upon closing & reopening drawer
cy.get(getTestSelector('close-account-drawer')).click()
cy.get(getTestSelector('web3-status-connected')).click()
cy.get('@gqlSpy').should('have.been.calledOnce')
})
it('refetches balances when account changes', () => {
cy.hardhat().then((hardhat) => {
const accountA = hardhat.wallets[0].address
const accountB = hardhat.wallets[1].address
// Opens the account drawer
cy.get(getTestSelector('web3-status-connected')).click()
// A shortened version of the first account's address should be shown
cy.contains(accountA.slice(0, 6)).should('exist')
// Stores the current portfolio balance to later compare to next account's balance
cy.get(getTestSelector('portfolio-total-balance'))
.invoke('text')
.then((originalBalance) => {
// TODO(INFRA-3) Replace window.ethereum access below with cypress-hardhat utility
// Simulates the wallet changing accounts via eip-1193 event
cy.window().then((win) => win.ethereum.emit('accountsChanged', [accountB]))
// The second account's address should now be shown
cy.contains(accountB.slice(0, 6)).should('exist')
// The second account's portfolio balance should differ from the original balance
cy.get(getTestSelector('portfolio-total-balance')).should('not.have.text', originalBalance)
})
})
})
})

View File

@@ -0,0 +1,116 @@
import { USDC_MAINNET } from '../../../src/constants/tokens'
import { getTestSelector } from '../../utils'
describe('mini-portfolio activity history', () => {
beforeEach(() => {
cy.hardhat()
.then((hardhat) => hardhat.wallet.getTransactionCount())
.then((nonce) => {
// Mock graphql response to include specific nonces.
cy.intercept(
{
method: 'POST',
url: 'https://beta.api.uniswap.org/v1/graphql',
},
{
body: {
data: {
portfolios: [
{
id: 'UG9ydGZvbGlvOjB4NUNlYUI3NGU0NDZkQmQzYkY2OUUyNzcyMDBGMTI5ZDJiQzdBMzdhMQ==',
assetActivities: [
{
id: 'QXNzZXRBY3Rpdml0eTpWSEpoYm5OaFkzUnBiMjQ2TUhnME5tUm1PVGs0T0RrNVl6UmtNR1kzWTJNNE9HRTVNVFEzTURBME9EWmtOVGhrTURnNFpqbG1NelkxTnpRM1l6WXdZek15WVRFNE4yWXlaRFEwWVdVNFh6QjRZV1EyWXpCa05XTmlOVEZsWWpjMU5qUTFaRGszT1RneE4yRTJZVEkxTmpreU1UbG1ZbVE1Wmw4d2VEQXpOR0UwTURjMk5EUTROV1kzWlRBNFkyRXhOak0yTm1VMU1ETTBPVEZoTm1GbU56ZzFNR1E9',
timestamp: 1681150079,
type: 'UNKNOWN',
chain: 'ETHEREUM',
transaction: {
id: 'VHJhbnNhY3Rpb246MHg0NmRmOTk4ODk5YzRkMGY3Y2M4OGE5MTQ3MDA0ODZkNThkMDg4ZjlmMzY1NzQ3YzYwYzMyYTE4N2YyZDQ0YWU4XzB4YWQ2YzBkNWNiNTFlYjc1NjQ1ZDk3OTgxN2E2YTI1NjkyMTlmYmQ5Zl8weDAzNGE0MDc2NDQ4NWY3ZTA4Y2ExNjM2NmU1MDM0OTFhNmFmNzg1MGQ=',
blockNumber: 17019453,
hash: '0x46df998899c4d0f7cc88a914700486d58d088f9f365747c60c32a187f2d44ae8',
status: 'CONFIRMED',
to: '0x034a40764485f7e08ca16366e503491a6af7850d',
from: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266',
nonce,
__typename: 'Transaction',
},
assetChanges: [],
__typename: 'AssetActivity',
},
{
id: 'QXNzZXRBY3Rpdml0eTpWSEpoYm5OaFkzUnBiMjQ2TUhneE16UXpaR1ppTlROaE9XRmpNR00yWW1aaVpqUTNNRFEyWWpObFkyRXhORGN3TUdZd00yWXhOMkV3WWpnM1pqWXpPRFpsWVRnNU16QTRNVFZtWmpoaFh6QjRZMkUzTXpOalkySm1OelZoTXpnME1ERXhPR1ZpT1RjNU9EVTJOemRpTkdRMk56TTBZemMwWmw4d2VERmlOVEUxTkdGaE5HSTRaakF5TjJJNVptUXhPVE0wTVRFek1tWmpPV1JoWlRFd1pqY3pOVGs9',
timestamp: 1681149995,
type: 'SEND',
chain: 'ETHEREUM',
transaction: {
id: 'VHJhbnNhY3Rpb246MHgxMzQzZGZiNTNhOWFjMGM2YmZiZjQ3MDQ2YjNlY2ExNDcwMGYwM2YxN2EwYjg3ZjYzODZlYTg5MzA4MTVmZjhhXzB4Y2E3MzNjY2JmNzVhMzg0MDExOGViOTc5ODU2NzdiNGQ2NzM0Yzc0Zl8weDFiNTE1NGFhNGI4ZjAyN2I5ZmQxOTM0MTEzMmZjOWRhZTEwZjczNTk=',
blockNumber: 17019446,
hash: '0x1343dfb53a9ac0c6bfbf47046b3eca14700f03f17a0b87f6386ea8930815ff8a',
status: 'CONFIRMED',
to: '0x1b5154aa4b8f027b9fd19341132fc9dae10f7359',
from: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266',
nonce: nonce + 1,
__typename: 'Transaction',
},
assetChanges: [
{
__typename: 'TokenTransfer',
id: 'VG9rZW5UcmFuc2ZlcjoweDVjZWFiNzRlNDQ2ZGJkM2JmNjllMjc3MjAwZjEyOWQyYmM3YTM3YTFfMHhiMWRjNDlmMDY1N2FkNTA1YjUzNzUyN2RkOWE1MDk0YTM2NTkzMWMxXzB4MTM0M2RmYjUzYTlhYzBjNmJmYmY0NzA0NmIzZWNhMTQ3MDBmMDNmMTdhMGI4N2Y2Mzg2ZWE4OTMwODE1ZmY4YQ==',
asset: {
id: 'VG9rZW46RVRIRVJFVU1fMHgxY2MyYjA3MGNhZjAxNmE3ZGRjMzA0N2Y5MzI3MmU4Yzc3YzlkZGU5',
name: 'USD Coin (USDC)',
symbol: 'USDC',
address: '0x1cc2b070caf016a7ddc3047f93272e8c77c9dde9',
decimals: 6,
chain: 'ETHEREUM',
standard: null,
project: {
id: 'VG9rZW5Qcm9qZWN0OkVUSEVSRVVNXzB4MWNjMmIwNzBjYWYwMTZhN2RkYzMwNDdmOTMyNzJlOGM3N2M5ZGRlOQ==',
isSpam: true,
logo: null,
__typename: 'TokenProject',
},
__typename: 'Token',
},
tokenStandard: 'ERC20',
quantity: '18011.212084',
sender: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266',
recipient: '0xb1dc49f0657ad505b537527dd9a5094a365931c1',
direction: 'OUT',
transactedValue: null,
},
],
__typename: 'AssetActivity',
},
],
__typename: 'Portfolio',
},
],
},
},
}
).as('graphql')
})
})
it('should deduplicate activity history by nonce', () => {
cy.visit(`/swap?inputCurrency=ETH&outputCurrency=${USDC_MAINNET.address}`, { ethereum: 'hardhat' }).hardhat({
automine: false,
})
// Input swap info.
cy.get('#swap-currency-input .token-amount-input').clear().type('1').should('have.value', '1')
cy.get('#swap-currency-output .token-amount-input').should('not.have.value', '')
cy.get('#swap-button').click()
cy.get('#confirm-swap-or-send').click()
cy.get(getTestSelector('dismiss-tx-confirmation')).click()
// Check activity history tab.
cy.get(getTestSelector('web3-status-connected')).click()
cy.get(getTestSelector('mini-portfolio-nav-activity')).click()
// Assert that the local pending transaction is replaced by a remote transaction with the same nonce.
cy.contains('Swapping').should('not.exist')
})
})

223
cypress/e2e/permit2.test.ts Normal file
View File

@@ -0,0 +1,223 @@
import { MaxUint160, MaxUint256 } from '@uniswap/permit2-sdk'
import { DAI, USDC_MAINNET } from '../../src/constants/tokens'
import { getTestSelector } from '../utils'
const APPROVE_BUTTON = '[data-testid="swap-approve-button"]'
/** Initiates a swap and confirms its success. */
function swaps() {
// The swap-button can be temporarily disabled following approval, & Cypress will retry clicking the disabled version.
// This ensures that we don't click until the button is enabled.
cy.get('#swap-button').should('not.have.attr', 'disabled')
// Completes the swap.
cy.get('#swap-button').click()
cy.get('#confirm-swap-or-send').click()
cy.get(getTestSelector('dismiss-tx-confirmation')).click()
// Verifies that there is a successful swap notification.
cy.contains('Swapped').should('exist')
}
// TODO(WEB-3299): Update tests to differentiate between permit2 vs token approval button text once UI is updated to indicate approval step.
describe('Permit2', () => {
// The same tokens & swap-amount combination is used for all permit2 tests.
const INPUT_TOKEN = DAI
const OUTPUT_TOKEN = USDC_MAINNET
const TEST_BALANCE_INCREMENT = 0.01
beforeEach(() => {
// Sets up a swap between INPUT_TOKEN and OUTPUT_TOKEN.
cy.visit(`/swap/?inputCurrency=${INPUT_TOKEN.address}&outputCurrency=${OUTPUT_TOKEN.address}`, {
ethereum: 'hardhat',
})
cy.get('#swap-currency-input .token-amount-input').clear().type(TEST_BALANCE_INCREMENT.toString())
})
/** Asserts permit2 has a max approval for spend of the input token on-chain. */
function expectTokenAllowanceForPermit2ToBeMax() {
// check token approval
return cy
.hardhat()
.then(({ approval, wallet }) => approval.getTokenAllowanceForPermit2({ owner: wallet, token: INPUT_TOKEN }))
.should('deep.equal', MaxUint256)
}
/** Asserts the universal router has a max permit2 approval for spend of the input token on-chain. */
function expectPermit2AllowanceForUniversalRouterToBeMax(approvalTime: number) {
return cy
.hardhat()
.then((hardhat) => hardhat.approval.getPermit2Allowance({ owner: hardhat.wallet, token: INPUT_TOKEN }))
.then((allowance) => {
cy.wrap(MaxUint160.eq(allowance.amount)).should('eq', true)
// Asserts that the on-chain expiration is in 30 days, within a tolerance of 40 seconds.
const expected = Math.floor((approvalTime + 2_592_000_000) / 1000)
cy.wrap(allowance.expiration).should('be.closeTo', expected, 40)
})
}
it('swaps when user has already approved token and permit2', () => {
cy.hardhat()
.then(({ approval, wallet }) => {
approval.setTokenAllowanceForPermit2({ owner: wallet, token: INPUT_TOKEN })
approval.setPermit2Allowance({ owner: wallet, token: INPUT_TOKEN })
})
.then(swaps)
})
it('swaps after completing full permit2 approval process', () => {
cy.get(APPROVE_BUTTON)
.click()
.then(() => {
const approvalTime = Date.now()
cy.get(APPROVE_BUTTON).should('have.text', 'Approval pending')
// There should be a successful Approved notification.
cy.contains('Approved').should('exist')
swaps()
expectTokenAllowanceForPermit2ToBeMax()
expectPermit2AllowanceForUniversalRouterToBeMax(approvalTime)
})
})
it('swaps after handling user rejection of approvals and signatures', () => {
const USER_REJECTION = { code: 4001 }
cy.hardhat().then((hardhat) => {
const tokenApprovalStub = cy.stub(hardhat.wallet, 'sendTransaction')
tokenApprovalStub.rejects(USER_REJECTION) // reject token approval
const permitApprovalStub = cy.stub(hardhat.provider, 'send')
permitApprovalStub.withArgs('eth_signTypedData_v4').rejects(USER_REJECTION) // reject permit approval
permitApprovalStub.callThrough() // allows non-eth_signTypedData_v4 send calls to return non-stubbed values
// Clicking the approve button should trigger a token approval that will be rejected by the user (tokenApprovalStub).
cy.get(APPROVE_BUTTON).click()
// The swap component should prompt approval again.
cy.get(APPROVE_BUTTON)
.should('have.text', `Approve use of ${INPUT_TOKEN.symbol}`)
.then(() => {
tokenApprovalStub.restore() // allow token approval
// The user is now allowing approval, but the permit2 signature will be rejected by the user (permitApprovalStub).
cy.get(APPROVE_BUTTON).click()
cy.get(APPROVE_BUTTON)
.should('have.text', `Approve use of ${INPUT_TOKEN.symbol}`)
.then(() => {
permitApprovalStub.restore() // allow permit approval
// The swap should now be able to proceed, as the permit2 signature will be accepted by the user.
cy.get(APPROVE_BUTTON)
.click()
.then(() => {
const approvalTime = Date.now()
cy.get(APPROVE_BUTTON).should('have.text', 'Approval pending')
// There should be a successful Approved notification.
cy.contains('Approved').should('exist')
swaps()
expectTokenAllowanceForPermit2ToBeMax()
expectPermit2AllowanceForUniversalRouterToBeMax(approvalTime)
})
})
})
})
})
it('swaps with existing token approval and missing permit approval', () => {
cy.hardhat()
.then(({ approval, wallet }) => approval.setTokenAllowanceForPermit2({ owner: wallet, token: INPUT_TOKEN }))
.then(() => {
cy.get(APPROVE_BUTTON)
.click()
.then(() => {
const approvalTime = Date.now()
swaps()
expectPermit2AllowanceForUniversalRouterToBeMax(approvalTime)
})
})
})
it('swaps with existing permit approval and missing token approval', () => {
cy.hardhat()
.then(({ approval, wallet }) => approval.setPermit2Allowance({ owner: wallet, token: INPUT_TOKEN }))
.then(() => {
cy.get(APPROVE_BUTTON).click()
cy.get(APPROVE_BUTTON).should('have.text', 'Approval pending')
// There should be a successful Approved notification.
cy.contains('Approved').should('exist')
swaps()
expectTokenAllowanceForPermit2ToBeMax()
})
})
it('prompts signature when existing permit approval is expired', () => {
const expiredAllowance = { expiration: Math.floor((Date.now() - 1) / 1000) }
cy.hardhat()
.then(({ approval, wallet }) => {
approval.setTokenAllowanceForPermit2({ owner: wallet, token: INPUT_TOKEN })
approval.setPermit2Allowance({ owner: wallet, token: INPUT_TOKEN }, expiredAllowance)
})
.then(() => {
cy.get(APPROVE_BUTTON)
.click()
.then(() => {
const approvalTime = Date.now()
swaps()
expectPermit2AllowanceForUniversalRouterToBeMax(approvalTime)
})
})
})
it('prompts signature when existing permit approval amount is too low', () => {
const smallAllowance = { amount: 1 }
cy.hardhat()
.then(({ approval, wallet }) => {
approval.setTokenAllowanceForPermit2({ owner: wallet, token: INPUT_TOKEN })
approval.setPermit2Allowance({ owner: wallet, token: INPUT_TOKEN }, smallAllowance)
})
.then(() => {
cy.get(APPROVE_BUTTON)
.click()
.then(() => {
const approvalTime = Date.now()
swaps()
expectPermit2AllowanceForUniversalRouterToBeMax(approvalTime)
})
})
})
it('prompts token approval when existing approval amount is too low', () => {
cy.hardhat()
.then(({ approval, wallet }) => {
approval.setPermit2Allowance({ owner: wallet, token: INPUT_TOKEN })
approval.setTokenAllowanceForPermit2({ owner: wallet, token: INPUT_TOKEN }, 1)
})
.then(() => {
cy.get(APPROVE_BUTTON).click()
// There should be a successful Approved notification.
cy.contains('Approved').should('exist')
swaps()
expectTokenAllowanceForPermit2ToBeMax()
})
})
})

View File

@@ -1,4 +1,4 @@
import assert = require('assert')
import assert from 'assert'
describe('Service Worker', () => {
before(() => {
@@ -9,7 +9,7 @@ describe('Service Worker', () => {
throw new Error(
'\n' +
'Service Worker tests must be run on a production-like build\n' +
'To test, build with `yarn build:e2e` and serve with `yarn serve`'
'To test, build with `yarn build` and serve with `yarn serve`'
)
}
})
@@ -20,65 +20,78 @@ describe('Service Worker', () => {
}
})
function unregister() {
return cy.log('unregister service worker').then(async () => {
const cacheKeys = await window.caches.keys()
const cacheKey = cacheKeys.find((key) => key.match(/precache/))
if (cacheKey) {
await window.caches.delete(cacheKey)
}
function unregisterServiceWorker() {
return cy.log('unregisters service worker').then(async () => {
const sw = await window.navigator.serviceWorker.getRegistration(Cypress.config().baseUrl ?? undefined)
await sw?.unregister()
})
}
before(unregister)
after(unregister)
before(unregisterServiceWorker)
after(unregisterServiceWorker)
beforeEach(() => {
cy.intercept({ hostname: 'www.google-analytics.com' }, (req) => {
const body = req.body.toString()
if (req.query['ep.event_category'] === 'Service Worker' || body.includes('Service%20Worker')) {
if (req.query['en'] === 'Not Installed' || body.includes('Not%20Installed')) {
req.alias = 'NotInstalled'
} else if (req.query['en'] === 'Cache Hit' || body.includes('Cache%20Hit')) {
req.alias = 'CacheHit'
} else if (req.query['en'] === 'Cache Miss' || body.includes('Cache%20Miss')) {
req.alias = 'CacheMiss'
}
cy.intercept('https://api.uniswap.org/v1/amplitude-proxy', (req) => {
const body = JSON.stringify(req.body)
const serviceWorkerStatus = body.match(/"service_worker":"(\w+)"/)?.[1]
if (serviceWorkerStatus) {
req.alias = `ServiceWorker:${serviceWorkerStatus}`
}
})
})
it('installs a ServiceWorker', () => {
it('installs a ServiceWorker and reports the uninstalled status to analytics', () => {
cy.visit('/', { serviceWorker: true })
.get('#swap-page')
.wait('@NotInstalled', { timeout: 20000 })
.window({ timeout: 20000 })
.and((win) => {
expect(win.navigator.serviceWorker.controller?.state).to.equal('activated')
})
cy.wait('@ServiceWorker:uninstalled')
cy.window().should(
'have.nested.property',
// The parent is checked instead of the AUT because it is on the same origin,
// and the AUT will not be considered "activated" until the parent is idle.
'parent.navigator.serviceWorker.controller.state',
'activated'
)
})
it('records a cache hit', () => {
cy.visit('/', { serviceWorker: true }).get('#swap-page').wait('@CacheHit', { timeout: 20000 })
})
it('records a cache miss', () => {
cy.then(async () => {
const cacheKeys = await window.caches.keys()
const cacheKey = cacheKeys.find((key) => key.match(/precache/))
assert(cacheKey)
const cache = await window.caches.open(cacheKey)
const keys = await cache.keys()
const key = keys.find((key) => key.url.match(/index/))
assert(key)
await cache.put(key, new Response())
describe('cache hit', () => {
it('reports the hit to analytics', () => {
cy.visit('/', { serviceWorker: true })
cy.wait('@ServiceWorker:hit')
})
})
describe('cache miss', () => {
let cache: Cache | undefined
let request: Request | undefined
let response: Response | undefined
before(() => {
// Mocks the index.html in the cache to force a cache miss.
cy.visit('/', { serviceWorker: true }).then(async () => {
const cacheKeys = await window.caches.keys()
const cacheKey = cacheKeys.find((key) => key.match(/precache/))
assert(cacheKey)
cache = await window.caches.open(cacheKey)
const keys = await cache.keys()
request = keys.find((key) => key.url.match(/index/))
assert(request)
response = await cache.match(request)
assert(response)
await cache.put(request, new Response())
})
})
after(() => {
// Restores the index.html in the cache so that re-runs behave as expected.
// This is necessary because the Service Worker will not re-populate the cache.
cy.then(async () => {
if (cache && request && response) {
await cache.put(request, response)
}
})
})
it('reports the miss to analytics', () => {
cy.visit('/', { serviceWorker: true })
cy.wait('@ServiceWorker:miss')
})
.visit('/', { serviceWorker: true })
.get('#swap-page')
.wait('@CacheMiss', { timeout: 20000 })
})
})

View File

@@ -1,108 +0,0 @@
import { FeatureFlag } from '../../src/featureFlags/flags/featureFlags'
import { getClassContainsSelector, getTestSelector } from '../utils'
const UNI_GOERLI = '0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984'
const WETH_GOERLI = '0xB4FBF271143F4FBf7B91A5ded31805e42b2208d6'
describe('swap widget integration tests', () => {
const verifyInputToken = (inputText: string) => {
cy.get(getClassContainsSelector('TokenButtonRow')).first().contains(inputText)
}
const verifyOutputToken = (outputText: string) => {
cy.get(getClassContainsSelector('TokenButtonRow')).last().contains(outputText)
}
const selectOutputAndSwitch = (outputText: string) => {
// open token selector...
cy.contains('Select token').click()
// select token...
cy.contains(outputText).click({ force: true })
cy.get('body')
.then(($body) => {
if ($body.find(getTestSelector('TokenSafetyWrapper')).length) {
return 'I understand'
}
return 'You pay' // Just click on a random element as a no-op
})
.then((selector) => {
cy.contains(selector).click()
})
// token selector should close...
cy.contains('Search name or paste address').should('not.exist')
cy.get(getClassContainsSelector('ReverseButton')).first().click()
}
describe('widget on swap page', () => {
beforeEach(() => {
cy.viewport(1200, 800)
})
it('should have the correct default input/output and token selection should work', () => {
cy.visit('/swap', { featureFlags: [FeatureFlag.swapWidget] }).then(() => {
cy.wait('@eth_blockNumber')
verifyInputToken('ETH')
verifyOutputToken('Select token')
selectOutputAndSwitch('WETH')
verifyInputToken('WETH')
verifyOutputToken('ETH')
})
})
it('should have the correct default input from URL params ', () => {
cy.visit(`/swap?inputCurrency=${WETH_GOERLI}`, {
featureFlags: [FeatureFlag.swapWidget],
}).then(() => {
cy.wait('@eth_blockNumber')
})
verifyInputToken('WETH')
verifyOutputToken('Select token')
selectOutputAndSwitch('Ether')
verifyInputToken('ETH')
verifyOutputToken('WETH')
})
it('should have the correct default output from URL params ', () => {
cy.visit(`/swap?outputCurrency=${WETH_GOERLI}`, {
featureFlags: [FeatureFlag.swapWidget],
}).then(() => {
cy.wait('@eth_blockNumber')
})
verifyInputToken('Select token')
verifyOutputToken('WETH')
cy.get(getClassContainsSelector('ReverseButton')).first().click()
verifyInputToken('WETH')
verifyOutputToken('Select token')
selectOutputAndSwitch('Ether')
verifyInputToken('ETH')
verifyOutputToken('WETH')
})
})
describe('widget on Token Detail Page', () => {
beforeEach(() => {
cy.viewport(1200, 800)
cy.visit(`/tokens/ethereum/${UNI_GOERLI}`, { featureFlags: [FeatureFlag.swapWidget] }).then(() => {
cy.wait('@eth_blockNumber')
})
})
it('should have the expected output for a tokens detail page', () => {
verifyOutputToken('UNI')
cy.contains('Connect to Ethereum').should('exist')
})
})
})

View File

@@ -1,145 +0,0 @@
import { WETH_GOERLI } from '../fixtures/constants'
import { getTestSelector } from '../utils'
describe('Swap', () => {
const verifyAmount = (field: 'input' | 'output', amountText: string | null) => {
if (amountText === null) {
cy.get(`#swap-currency-${field} .token-amount-input`).should('not.have.value')
} else {
cy.get(`#swap-currency-${field} .token-amount-input`).should('have.value', amountText)
}
}
const verifyToken = (field: 'input' | 'output', tokenSymbol: string | null) => {
if (tokenSymbol === null) {
cy.get(`#swap-currency-${field} .token-symbol-container`).should('contain.text', 'Select token')
} else {
cy.get(`#swap-currency-${field} .token-symbol-container`).should('contain.text', tokenSymbol)
}
}
const selectOutput = (tokenSymbol: string) => {
// open token selector...
cy.contains('Select token').click()
// select token...
cy.contains(tokenSymbol).click()
cy.get('body')
.then(($body) => {
if ($body.find(getTestSelector('TokenSafetyWrapper')).length) {
return 'I understand'
}
return 'no-op' // Don't click on anything, a no-op
})
.then((content) => {
if (content !== 'no-op') {
cy.contains(content).click()
}
})
// token selector should close...
cy.contains('Search name or paste address').should('not.exist')
}
before(() => {
cy.visit('/swap')
})
it('starts with ETH selected by default', () => {
verifyAmount('input', '')
verifyToken('input', 'ETH')
verifyAmount('output', null)
verifyToken('output', null)
})
it('can enter an amount into input', () => {
cy.get('#swap-currency-input .token-amount-input').clear().type('0.001').should('have.value', '0.001')
})
it('zero swap amount', () => {
cy.get('#swap-currency-input .token-amount-input').clear().type('0.0').should('have.value', '0.0')
})
it('invalid swap amount', () => {
cy.get('#swap-currency-input .token-amount-input').clear().type('\\').should('have.value', '')
})
it('can enter an amount into output', () => {
cy.get('#swap-currency-output .token-amount-input').clear().type('0.001').should('have.value', '0.001')
})
it('zero output amount', () => {
cy.get('#swap-currency-output .token-amount-input').clear().type('0.0').should('have.value', '0.0')
})
it('should have the correct default input/output and token selection should work', () => {
cy.visit('/swap')
verifyToken('input', 'ETH')
verifyToken('output', null)
selectOutput('WETH')
cy.get(getTestSelector('swap-currency-button')).first().click()
verifyToken('input', 'WETH')
verifyToken('output', 'ETH')
})
it('should have the correct default input from URL params ', () => {
cy.visit(`/swap?inputCurrency=${WETH_GOERLI}`)
verifyToken('input', 'WETH')
verifyToken('output', null)
selectOutput('Ether')
cy.get(getTestSelector('swap-currency-button')).first().click()
verifyToken('input', 'ETH')
verifyToken('output', 'WETH')
})
it('should have the correct default output from URL params ', () => {
cy.visit(`/swap?outputCurrency=${WETH_GOERLI}`)
verifyToken('input', null)
verifyToken('output', 'WETH')
cy.get(getTestSelector('swap-currency-button')).first().click()
verifyToken('input', 'WETH')
verifyToken('output', null)
selectOutput('Ether')
cy.get(getTestSelector('swap-currency-button')).first().click()
verifyToken('input', 'ETH')
verifyToken('output', 'WETH')
})
it('ETH to wETH is same value (wrapped swaps have no price impact)', () => {
cy.visit('/swap')
selectOutput('WETH')
cy.get('#swap-currency-input .token-amount-input').clear().type('0.01')
cy.get('#swap-currency-output .token-amount-input').should('have.value', '0.01')
})
it('Opens and closes the settings menu', () => {
cy.visit('/swap')
cy.contains('Settings').should('not.exist')
cy.get(getTestSelector('swap-settings-button')).click()
cy.contains('Slippage tolerance').should('exist')
cy.contains('Transaction deadline').should('exist')
cy.contains('Auto Router API').should('exist')
cy.contains('Expert Mode').should('exist')
cy.get(getTestSelector('swap-settings-button')).click()
cy.contains('Settings').should('not.exist')
})
it('inputs reset when navigating between pages', () => {
cy.get('#swap-currency-input .token-amount-input').clear().type('0.01')
cy.get('#swap-currency-output .token-amount-input').should('not.equal', '')
cy.visit('/pool')
cy.visit('/swap')
cy.get('#swap-currency-input .token-amount-input').should('have.value', '')
cy.get('#swap-currency-output .token-amount-input').should('not.equal', '')
})
})

View File

@@ -0,0 +1,114 @@
import { BigNumber } from '@ethersproject/bignumber'
import { SupportedChainId } from '@uniswap/sdk-core'
import { UNI, USDC_MAINNET } from '../../../src/constants/tokens'
import { getBalance, getTestSelector } from '../../utils'
const UNI_MAINNET = UNI[SupportedChainId.MAINNET]
describe('Swap errors', () => {
it('wallet rejection', () => {
cy.visit(`/swap?inputCurrency=ETH&outputCurrency=${USDC_MAINNET.address}`, { ethereum: 'hardhat' })
cy.hardhat().then((hardhat) => {
// Stub the wallet to reject any transaction.
cy.stub(hardhat.wallet, 'sendTransaction').log(false).rejects(new Error('user cancelled'))
// Attempt to swap.
cy.get('#swap-currency-output .token-amount-input').clear().type('1').should('have.value', '1')
cy.get('#swap-currency-input .token-amount-input').should('not.have.value', '')
cy.get('#swap-button').click()
cy.get('#confirm-swap-or-send').click()
cy.contains('Transaction rejected').should('exist')
cy.contains('Dismiss').click()
cy.contains('Transaction rejected').should('not.exist')
})
})
it('transaction past deadline', () => {
cy.visit(`/swap?inputCurrency=ETH&outputCurrency=${USDC_MAINNET.address}`, { ethereum: 'hardhat' })
cy.hardhat({ automine: false })
getBalance(USDC_MAINNET).then((initialBalance) => {
// Set deadline to minimum. (1 minute)
cy.get(getTestSelector('open-settings-dialog-button')).click()
cy.get(getTestSelector('transaction-deadline-settings')).click()
cy.get(getTestSelector('deadline-input')).clear().type('1') // 1 minute
// Click outside of modal to dismiss it.
cy.get('body').click('topRight')
cy.get(getTestSelector('deadline-input')).should('not.exist')
// Attempt to swap.
cy.get('#swap-currency-output .token-amount-input').clear().type('1').should('have.value', '1')
cy.get('#swap-currency-input .token-amount-input').should('not.have.value', '')
cy.get('#swap-button').click()
cy.get('#confirm-swap-or-send').click()
cy.get(getTestSelector('dismiss-tx-confirmation')).click()
// The pending transaction indicator should reflect the state.
cy.get(getTestSelector('web3-status-connected')).should('contain', '1 Pending')
cy.hardhat().then((hardhat) => hardhat.mine(1, /* 10 minutes */ 1000 * 60 * 10)) // mines past the deadline
cy.get(getTestSelector('web3-status-connected')).should('not.contain', 'Pending')
// TODO(WEB-2085): Fix this test - transaction popups are flakey.
// cy.get(getTestSelector('transaction-popup')).contains('Swap failed')
// Verify the balance is unchanged.
cy.get('#swap-currency-output [data-testid="balance-text"]').should('have.text', `Balance: ${initialBalance}`)
getBalance(USDC_MAINNET).should('eq', initialBalance)
})
})
it('slippage failure', () => {
cy.visit(`/swap?inputCurrency=ETH&outputCurrency=${UNI_MAINNET.address}`, { ethereum: 'hardhat' })
cy.hardhat({ automine: false })
getBalance(USDC_MAINNET).then((initialBalance) => {
// Gas estimation fails for this transaction (that would normally fail), so we stub it.
cy.hardhat().then((hardhat) => {
const send = cy.stub(hardhat.provider, 'send').log(false)
send.withArgs('eth_estimateGas').resolves(BigNumber.from(2_000_000))
send.callThrough()
})
// Set slippage to a very low value.
cy.get(getTestSelector('open-settings-dialog-button')).click()
cy.get(getTestSelector('max-slippage-settings')).click()
cy.get(getTestSelector('slippage-input')).clear().type('0.01')
// Click outside of modal to dismiss it.
cy.get('body').click('topRight')
cy.get(getTestSelector('slippage-input')).should('not.exist')
// Swap 2 times.
const AMOUNT_TO_SWAP = 200
cy.get('#swap-currency-input .token-amount-input')
.clear()
.type(AMOUNT_TO_SWAP.toString())
.should('have.value', AMOUNT_TO_SWAP.toString())
cy.get('#swap-currency-output .token-amount-input').should('not.have.value', '')
cy.get('#swap-button').click()
cy.get('#confirm-swap-or-send').click()
cy.get(getTestSelector('dismiss-tx-confirmation')).click()
cy.get('#swap-currency-input .token-amount-input')
.clear()
.type(AMOUNT_TO_SWAP.toString())
.should('have.value', AMOUNT_TO_SWAP.toString())
cy.get('#swap-currency-output .token-amount-input').should('not.have.value', '')
cy.get('#swap-button').click()
cy.get('#confirm-swap-or-send').click()
cy.get(getTestSelector('dismiss-tx-confirmation')).click()
// The pending transaction indicator should reflect the state.
cy.get(getTestSelector('web3-status-connected')).should('contain', '2 Pending')
cy.hardhat().then((hardhat) => hardhat.mine())
cy.get(getTestSelector('web3-status-connected')).should('not.contain', 'Pending')
// TODO(WEB-2085): Fix this test - transaction popups are flakey.
// cy.get(getTestSelector('transaction-popup')).contains('Swap failed')
// Assert that the transactions were unsuccessful by checking on-chain balance.
getBalance(UNI_MAINNET).should('equal', initialBalance)
})
})
})

View File

@@ -0,0 +1,14 @@
import { getTestSelector } from '../../utils'
describe('Swap settings', () => {
it('Opens and closes the settings menu', () => {
cy.visit('/swap')
cy.contains('Settings').should('not.exist')
cy.get(getTestSelector('open-settings-dialog-button')).click()
cy.contains('Max slippage').should('exist')
cy.contains('Transaction deadline').should('exist')
cy.contains('Auto Router API').should('exist')
cy.get(getTestSelector('open-settings-dialog-button')).click()
cy.contains('Settings').should('not.exist')
})
})

View File

@@ -0,0 +1,70 @@
import { SupportedChainId } from '@uniswap/sdk-core'
import { UNI, USDC_MAINNET } from '../../../src/constants/tokens'
import { getBalance, getTestSelector } from '../../utils'
const UNI_MAINNET = UNI[SupportedChainId.MAINNET]
describe('Swap', () => {
describe('Swap on main page', () => {
it('starts with ETH selected by default', () => {
cy.visit('/swap')
cy.get(`#swap-currency-input .token-amount-input`).should('have.value', '')
cy.get(`#swap-currency-input .token-symbol-container`).should('contain.text', 'ETH')
cy.get(`#swap-currency-output .token-amount-input`).should('not.have.value')
cy.get(`#swap-currency-output .token-symbol-container`).should('contain.text', 'Select token')
})
it('should default inputs from URL params ', () => {
cy.visit(`/swap?inputCurrency=${UNI_MAINNET.address}`)
cy.get(`#swap-currency-input .token-symbol-container`).should('contain.text', 'UNI')
cy.get(`#swap-currency-output .token-symbol-container`).should('contain.text', 'Select token')
cy.visit(`/swap?outputCurrency=${UNI_MAINNET.address}`)
cy.get(`#swap-currency-input .token-symbol-container`).should('contain.text', 'Select token')
cy.get(`#swap-currency-output .token-symbol-container`).should('contain.text', 'UNI')
cy.visit(`/swap?inputCurrency=ETH&outputCurrency=${UNI_MAINNET.address}`)
cy.get(`#swap-currency-input .token-symbol-container`).should('contain.text', 'ETH')
cy.get(`#swap-currency-output .token-symbol-container`).should('contain.text', 'UNI')
})
it('inputs reset when navigating between pages', () => {
cy.visit('/swap')
cy.get('#swap-currency-input .token-amount-input').should('have.value', '')
cy.get('#swap-currency-input .token-amount-input').type('0.01').should('have.value', '0.01')
cy.visit('/pool').visit('/swap')
cy.get('#swap-currency-input .token-amount-input').should('have.value', '')
})
it('swaps ETH for USDC', () => {
cy.visit('/swap', { ethereum: 'hardhat' })
cy.hardhat({ automine: false })
getBalance(USDC_MAINNET).then((initialBalance) => {
cy.get('#swap-currency-output .open-currency-select-button').click()
cy.get(getTestSelector('token-search-input')).clear().type(USDC_MAINNET.address)
cy.contains('USDC').click()
cy.get('#swap-currency-output .token-amount-input').clear().type('1').should('have.value', '1')
cy.get('#swap-currency-input .token-amount-input').should('not.have.value', '')
cy.get('#swap-button').click()
cy.get('#confirm-swap-or-send').click()
cy.get(getTestSelector('dismiss-tx-confirmation')).click()
// The pending transaction indicator should reflect the state.
cy.get(getTestSelector('web3-status-connected')).should('contain', '1 Pending')
cy.hardhat().then((hardhat) => hardhat.mine())
cy.get(getTestSelector('web3-status-connected')).should('not.contain', 'Pending')
// TODO(WEB-2085): Fix this test - transaction popups are flakey.
// cy.get(getTestSelector('transaction-popup')).contains('Swapped')
// Verify the balance is updated.
cy.get('#swap-currency-output [data-testid="balance-text"]').should(
'have.text',
`Balance: ${initialBalance + 1}`
)
getBalance(USDC_MAINNET).should('eq', initialBalance + 1)
})
})
})
})

View File

@@ -0,0 +1,86 @@
import { CurrencyAmount, SupportedChainId, WETH9 } from '@uniswap/sdk-core'
import { getBalance, getTestSelector } from '../../utils'
const WETH = WETH9[SupportedChainId.MAINNET]
describe('Swap wrap', () => {
beforeEach(() => {
cy.visit(`/swap?inputCurrency=ETH&outputCurrency=${WETH.address}`, { ethereum: 'hardhat' }).hardhat({
automine: false,
})
})
it('ETH to wETH is same value (wrapped swaps have no price impact)', () => {
cy.get('#swap-currency-input .token-amount-input').clear().type('0.01').should('have.value', '0.01')
cy.get('#swap-currency-output .token-amount-input').should('have.value', '0.01')
cy.get('#swap-currency-output .token-amount-input').clear().type('0.02').should('have.value', '0.02')
cy.get('#swap-currency-input .token-amount-input').should('have.value', '0.02')
})
it('should be able to wrap ETH', () => {
getBalance(WETH).then((initialWethBalance) => {
cy.contains('Enter ETH amount')
// Enter the amount to wrap.
cy.get('#swap-currency-output .token-amount-input').click().type('1').should('have.value', 1)
// This also ensures we don't click "Wrap" before the UI has caught up.
cy.get('#swap-currency-input .token-amount-input').should('have.value', 1)
// Click the wrap button.
cy.contains('Wrap').click()
// The pending transaction indicator should reflect the state.
cy.get(getTestSelector('web3-status-connected')).should('contain', '1 Pending')
cy.hardhat().then((hardhat) => hardhat.mine())
cy.get(getTestSelector('web3-status-connected')).should('not.contain', 'Pending')
// TODO(WEB-2085): Fix this test - transaction popups are flakey.
// cy.get(getTestSelector('transaction-popup')).contains('Wrapped')
// cy.get(getTestSelector('transaction-popup')).contains('1.00 ETH for 1.00 WETH')
// The UI balance should have increased.
cy.get('#swap-currency-output').should('contain', `Balance: ${initialWethBalance + 1}`)
// The user's WETH account balance should have increased
getBalance(WETH).should('equal', initialWethBalance + 1)
})
})
it('should be able to unwrap WETH', () => {
cy.hardhat().then(async (hardhat) => {
await hardhat.fund(hardhat.wallet, CurrencyAmount.fromRawAmount(WETH, 1e18))
await hardhat.mine()
})
getBalance(WETH).then((initialWethBalance) => {
// Swap input/output to unwrap WETH.
cy.get(getTestSelector('swap-currency-button')).click()
cy.contains('Enter WETH amount')
// Enter the amount to unwrap.
cy.get('#swap-currency-output .token-amount-input').click().type('1').should('have.value', 1)
// This also ensures we don't click "Wrap" before the UI has caught up.
cy.get('#swap-currency-input .token-amount-input').should('have.value', 1)
// Click the unwrap button.
cy.contains('Unwrap').click()
// The pending transaction indicator should reflect the state.
cy.get(getTestSelector('web3-status-connected')).should('contain', '1 Pending')
cy.hardhat().then((hardhat) => hardhat.mine())
cy.get(getTestSelector('web3-status-connected')).should('not.contain', 'Pending')
// TODO(WEB-2085): Fix this test - transaction popups are flakey.
// cy.get(getTestSelector('transaction-popup')).contains('Unwrapped')
// cy.get(getTestSelector('transaction-popup')).contains('1.00 WETH for 1.00 ETH')
// The UI balance should have increased.
cy.get('#swap-currency-input').should('contain', `Balance: ${initialWethBalance - 1}`)
// The user's WETH account balance should have increased
getBalance(WETH).should('equal', initialWethBalance - 1)
})
})
})

View File

@@ -1,10 +1,15 @@
import { getClassContainsSelector, getTestSelector } from '../utils'
import { SupportedChainId, WETH9 } from '@uniswap/sdk-core'
import { UNI } from '../../src/constants/tokens'
import { getTestSelector } from '../utils'
const UNI_MAINNET = UNI[SupportedChainId.MAINNET]
const UNI_ADDRESS = '0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984'
describe('Token details', () => {
before(() => {
cy.visit('/')
beforeEach(() => {
cy.viewport(1440, 900)
})
it('Uniswap token should have all information populated', () => {
@@ -40,9 +45,6 @@ describe('Token details', () => {
// Contract address should be displayed
cy.contains(UNI_ADDRESS).should('exist')
// Swap widget should have this token pre-selected as the “destination” token
cy.get(getTestSelector('token-select')).should('include.text', 'UNI')
})
it('token with warning and low trading volume should have all information populated', () => {
@@ -81,36 +83,75 @@ describe('Token details', () => {
// Contract address should be displayed
cy.contains('0xa71d0588EAf47f12B13cF8eC750430d21DF04974').should('exist')
// Swap widget should have this token pre-selected as the “destination” token
cy.get(getTestSelector('token-select')).should('include.text', 'QOM')
// Warning label should show if relevant ([spec](https://www.notion.so/3f7fce6f93694be08a94a6984d50298e))
cy.get('[data-cy="token-safety-message"]')
.should('include.text', 'Warning')
.and('include.text', "This token isn't traded on leading U.S. centralized exchanges")
})
describe('Swap on Token Detail Page', () => {
const verifyOutputToken = (outputText: string) => {
cy.get(getClassContainsSelector('TokenButtonRow')).last().contains(outputText)
}
describe('swapping', () => {
beforeEach(() => {
// On mobile widths, we just link back to /swap instead of rendering the swap component.
cy.viewport(1200, 800)
cy.visit(`/tokens/goerli/${UNI_ADDRESS}`).then(() => {
cy.visit(`/tokens/ethereum/${UNI_MAINNET.address}`, {
ethereum: 'hardhat',
}).then(() => {
cy.wait('@eth_blockNumber')
cy.scrollTo('top')
})
})
it('should have the expected output for a tokens detail page', () => {
verifyOutputToken('UNI')
cy.get(`#swap-currency-input .token-amount-input`).should('have.value', '')
cy.get(`#swap-currency-input .token-symbol-container`).should('contain.text', 'Select token')
cy.get(`#swap-currency-output .token-amount-input`).should('not.have.value')
cy.get(`#swap-currency-output .token-symbol-container`).should('contain.text', 'UNI')
})
it('should not share swap state with the main swap page', () => {
verifyOutputToken('UNI')
it('should automatically navigate to the new TDP', () => {
cy.get(`#swap-currency-output .open-currency-select-button`).click()
cy.contains('WETH').click()
cy.url().should('include', `${WETH9[1].address}`)
cy.url().should('not.include', `${UNI_MAINNET.address}`)
})
it.only('should not share swap state with the main swap page', () => {
cy.get(`#swap-currency-output .token-symbol-container`).should('contain.text', 'UNI')
cy.get(`#swap-currency-input .open-currency-select-button`).click()
cy.contains('WETH').click()
cy.visit('/swap')
cy.contains('UNI').should('not.exist')
cy.contains('WETH').should('not.exist')
})
it('can enter an amount into input', () => {
cy.get('#swap-currency-input .token-amount-input').clear().type('0.001').should('have.value', '0.001')
})
it('zero swap amount', () => {
cy.get('#swap-currency-input .token-amount-input').clear().type('0.0').should('have.value', '0.0')
})
it('invalid swap amount', () => {
cy.get('#swap-currency-input .token-amount-input').clear().type('\\').should('have.value', '')
})
it('can enter an amount into output', () => {
cy.get('#swap-currency-output .token-amount-input').clear().type('0.001').should('have.value', '0.001')
})
it('zero output amount', () => {
cy.get('#swap-currency-output .token-amount-input').clear().type('0.0').should('have.value', '0.0')
})
it('should show a L2 token even if the user is connected to a different network', () => {
cy.visit('/tokens', { ethereum: 'hardhat' })
cy.get(getTestSelector('tokens-network-filter-selected')).click()
cy.get(getTestSelector('tokens-network-filter-option-arbitrum')).click()
cy.get(getTestSelector('tokens-network-filter-selected')).should('contain', 'Arbitrum')
cy.get(getTestSelector('token-table-row-ARB')).click()
cy.get(`#swap-currency-output .token-symbol-container`).should('contain.text', 'ARB')
cy.contains('Connect to Arbitrum').should('exist')
})
})
})

View File

@@ -18,7 +18,7 @@ describe('Universal search bar', () => {
.and('contain.text', 'UNI')
.and('contain.text', '$')
.and('contain.text', '%')
cy.get('[data-cy="searchbar-token-row-UNI"]').click()
cy.get('[data-cy="searchbar-token-row-UNI"]').first().click()
cy.get('div').contains('Uniswap').should('exist')
// Stats should have: TVL, 24H Volume, 52W low, 52W high.

View File

@@ -1,96 +1,108 @@
import { getTestSelector } from '../utils'
function visit(darkMode: boolean) {
cy.visit('/swap', {
onBeforeLoad(win) {
cy.stub(win, 'matchMedia')
.withArgs('(prefers-color-scheme: dark)')
.returns({
matches: darkMode,
addEventListener() {
// do nothing
},
})
},
})
}
describe('Wallet Dropdown', () => {
before(() => {
cy.visit('/pools')
function itShouldChangeTheTheme() {
it('should change the theme', () => {
cy.get(getTestSelector('theme-lightmode')).click()
cy.get(getTestSelector('theme-lightmode')).should('not.have.css', 'background-color', 'rgba(0, 0, 0, 0)')
cy.get(getTestSelector('theme-darkmode')).should('have.css', 'background-color', 'rgba(0, 0, 0, 0)')
cy.get(getTestSelector('theme-auto')).should('have.css', 'background-color', 'rgba(0, 0, 0, 0)')
cy.get(getTestSelector('theme-darkmode')).click()
cy.get(getTestSelector('theme-lightmode')).should('have.css', 'background-color', 'rgba(0, 0, 0, 0)')
cy.get(getTestSelector('theme-darkmode')).should('not.have.css', 'background-color', 'rgba(0, 0, 0, 0)')
cy.get(getTestSelector('theme-auto')).should('have.css', 'background-color', 'rgba(0, 0, 0, 0)')
cy.get(getTestSelector('theme-auto')).click()
cy.get(getTestSelector('theme-lightmode')).should('have.css', 'background-color', 'rgba(0, 0, 0, 0)')
cy.get(getTestSelector('theme-darkmode')).should('have.css', 'background-color', 'rgba(0, 0, 0, 0)')
cy.get(getTestSelector('theme-auto')).should('not.have.css', 'background-color', 'rgba(0, 0, 0, 0)')
})
}
function itShouldChangeTheLanguage() {
it('should select a language', () => {
cy.get(getTestSelector('wallet-language-item')).contains('Deutsch').click({ force: true })
cy.get(getTestSelector('wallet-header')).should('contain', 'Sprache')
cy.get(getTestSelector('wallet-language-item')).contains('English').click({ force: true })
cy.get(getTestSelector('wallet-header')).should('contain', 'Language')
cy.get(getTestSelector('wallet-back')).click()
})
}
describe('connected', () => {
beforeEach(() => {
cy.visit('/')
cy.get(getTestSelector('web3-status-connected')).click()
cy.get(getTestSelector('wallet-settings')).click()
})
itShouldChangeTheTheme()
itShouldChangeTheLanguage()
})
it('should change the theme', () => {
cy.get(getTestSelector('web3-status-connected')).click()
cy.get(getTestSelector('wallet-settings')).click()
cy.get(getTestSelector('theme-lightmode')).click()
cy.get(getTestSelector('theme-lightmode')).should('not.have.css', 'background-color', 'rgba(0, 0, 0, 0)')
cy.get(getTestSelector('theme-darkmode')).should('have.css', 'background-color', 'rgba(0, 0, 0, 0)')
cy.get(getTestSelector('theme-auto')).should('have.css', 'background-color', 'rgba(0, 0, 0, 0)')
cy.get(getTestSelector('theme-darkmode')).click()
cy.get(getTestSelector('theme-lightmode')).should('have.css', 'background-color', 'rgba(0, 0, 0, 0)')
cy.get(getTestSelector('theme-darkmode')).should('not.have.css', 'background-color', 'rgba(0, 0, 0, 0)')
cy.get(getTestSelector('theme-auto')).should('have.css', 'background-color', 'rgba(0, 0, 0, 0)')
cy.get(getTestSelector('theme-auto')).click()
cy.get(getTestSelector('theme-lightmode')).should('have.css', 'background-color', 'rgba(0, 0, 0, 0)')
cy.get(getTestSelector('theme-darkmode')).should('have.css', 'background-color', 'rgba(0, 0, 0, 0)')
cy.get(getTestSelector('theme-auto')).should('not.have.css', 'background-color', 'rgba(0, 0, 0, 0)')
describe('disconnected', () => {
beforeEach(() => {
cy.visit('/')
cy.get(getTestSelector('web3-status-connected')).click()
cy.get(getTestSelector('wallet-disconnect')).click()
cy.get(getTestSelector('wallet-settings')).click()
})
itShouldChangeTheTheme()
itShouldChangeTheLanguage()
})
it('should select a language', () => {
cy.get(getTestSelector('wallet-language-item')).contains('Deutsch').click({ force: true })
cy.get(getTestSelector('wallet-header')).should('contain', 'Sprache')
cy.get(getTestSelector('wallet-language-item')).contains('English').click({ force: true })
cy.get(getTestSelector('wallet-header')).should('contain', 'Language')
cy.get(getTestSelector('wallet-back')).click()
describe('with color theme', () => {
function visitSwapWithColorTheme({ dark }: { dark: boolean }) {
cy.visit('/swap', {
onBeforeLoad(win) {
cy.stub(win, 'matchMedia')
.withArgs('(prefers-color-scheme: dark)')
.returns({
matches: dark,
addEventListener() {
/* noop */
},
removeEventListener() {
/* noop */
},
})
},
})
}
it('should properly use dark system theme when auto theme setting is selected', () => {
visitSwapWithColorTheme({ dark: true })
cy.get(getTestSelector('web3-status-connected')).click()
cy.get(getTestSelector('wallet-settings')).click()
cy.get(getTestSelector('theme-auto')).click()
cy.get(getTestSelector('wallet-header')).should('have.css', 'color', 'rgb(152, 161, 192)')
})
it('should properly use light system theme when auto theme setting is selected', () => {
visitSwapWithColorTheme({ dark: false })
cy.get(getTestSelector('web3-status-connected')).click()
cy.get(getTestSelector('wallet-settings')).click()
cy.get(getTestSelector('theme-auto')).click()
cy.get(getTestSelector('wallet-header')).should('have.css', 'color', 'rgb(119, 128, 160)')
})
})
it('should change the theme when not connected', () => {
cy.get(getTestSelector('wallet-disconnect')).click()
cy.get(getTestSelector('wallet-settings')).click()
cy.get(getTestSelector('theme-lightmode')).should('exist')
})
describe('mobile', () => {
beforeEach(() => {
cy.viewport('iphone-6').visit('/')
})
it('should select a language when not connected', () => {
cy.get(getTestSelector('wallet-language-item')).contains('Deutsch').click({ force: true })
cy.get(getTestSelector('wallet-header')).should('contain', 'Sprache')
cy.get(getTestSelector('wallet-language-item')).contains('English').click({ force: true })
cy.get(getTestSelector('wallet-header')).should('contain', 'Language')
cy.get(getTestSelector('wallet-back')).click()
})
it('should dismiss the wallet bottom sheet when clicking buy crypto', () => {
cy.get(getTestSelector('web3-status-connected')).click()
cy.get(getTestSelector('wallet-buy-crypto')).click()
cy.contains('Buy crypto').should('not.be.visible')
})
it('should properly use dark system theme when auto theme setting is selected', () => {
visit(true)
cy.get(getTestSelector('web3-status-connected')).click()
cy.get(getTestSelector('wallet-settings')).click()
cy.get(getTestSelector('theme-auto')).click()
cy.get(getTestSelector('wallet-header')).should('have.css', 'color', 'rgb(152, 161, 192)')
})
it('should properly use light system theme when auto theme setting is selected', () => {
visit(false)
cy.get(getTestSelector('web3-status-connected')).click()
cy.get(getTestSelector('wallet-settings')).click()
cy.get(getTestSelector('theme-auto')).click()
cy.get(getTestSelector('wallet-header')).should('have.css', 'color', 'rgb(119, 128, 160)')
})
it('should dismiss the wallet bottom sheet when clicking buy crypto', () => {
visit(false)
cy.viewport('iphone-6')
cy.get(getTestSelector('web3-status-connected')).click()
cy.get(getTestSelector('wallet-buy-crypto')).click()
cy.contains('Buy crypto').should('not.be.visible')
})
it('should use a bottom sheet and dismiss when on a mobile screen size', () => {
visit(true)
cy.viewport('iphone-6')
cy.get(getTestSelector('web3-status-connected')).click()
cy.root().click(15, 40)
cy.get(getTestSelector('wallet-settings')).should('not.be.visible')
it('should use a bottom sheet and dismiss when on a mobile screen size', () => {
cy.get(getTestSelector('web3-status-connected')).click()
cy.root().click(15, 40)
cy.get(getTestSelector('wallet-settings')).should('not.be.visible')
})
})
})

View File

@@ -1 +0,0 @@
export const WETH_GOERLI = '0xB4FBF271143F4FBf7B91A5ded31805e42b2208d6'

View File

@@ -1,21 +0,0 @@
const ONE_MINUTE = 60_000
describe(
'Release',
{
pageLoadTimeout: ONE_MINUTE,
retries: 30,
},
() => {
it('loads swap page', () => {
// TODO: We *must* wait in order to space out the retry attempts. Find a better way to do this.
// eslint-disable-next-line cypress/no-unnecessary-waiting
cy.wait(ONE_MINUTE)
.visit('/', {
retryOnStatusCodeFailure: true,
retryOnNetworkFailure: true,
})
.get('#swap-page')
})
}
)

View File

@@ -0,0 +1,80 @@
import 'cypress-hardhat/lib/browser'
import { Eip1193Bridge } from '@ethersproject/experimental/lib/eip1193-bridge'
import { FeatureFlag } from '../../src/featureFlags/flags/featureFlags'
import { UserState } from '../../src/state/user/reducer'
import { CONNECTED_WALLET_USER_STATE } from '../utils/user-state'
import { injected } from './ethereum'
declare global {
// eslint-disable-next-line @typescript-eslint/no-namespace
namespace Cypress {
interface ApplicationWindow {
ethereum: Eip1193Bridge
}
interface VisitOptions {
serviceWorker?: true
featureFlags?: Array<FeatureFlag>
/**
* The mock ethereum provider to inject into the page.
* @default 'goerli'
*/
// TODO(INFRA-175): Migrate all usage of 'goerli' to 'hardhat'.
ethereum?: 'goerli' | 'hardhat'
/**
* Initial user state.
* @default {@type import('../utils/user-state').CONNECTED_WALLET_USER_STATE}
*/
userState?: Partial<UserState>
}
}
}
// sets up the injected provider to be a mock ethereum provider with the given mnemonic/index
// eslint-disable-next-line no-undef
Cypress.Commands.overwrite(
'visit',
(original, url: string | Partial<Cypress.VisitOptions>, options?: Partial<Cypress.VisitOptions>) => {
if (typeof url !== 'string') throw new Error('Invalid arguments. The first argument to cy.visit must be the path.')
// Add a hash in the URL if it is not present (to use hash-based routing correctly with queryParams).
let hashUrl = url.startsWith('/') && url.length > 2 && !url.startsWith('/#') ? `/#${url}` : url
if (options?.ethereum === 'goerli') hashUrl += `${url.includes('?') ? '&' : '?'}chain=goerli`
return cy
.intercept('/service-worker.js', options?.serviceWorker ? undefined : { statusCode: 404 })
.provider()
.then((provider) =>
original({
...options,
url: hashUrl,
onBeforeLoad(win) {
options?.onBeforeLoad?.(win)
// We want to test from a clean state, so we clear the local storage (which clears redux).
win.localStorage.clear()
// Set initial user state.
win.localStorage.setItem(
'redux_localstorage_simple_user', // storage key for the user reducer using 'redux-localstorage-simple'
JSON.stringify(options?.userState ?? CONNECTED_WALLET_USER_STATE)
)
// Set feature flags, if configured.
if (options?.featureFlags) {
const featureFlags = options.featureFlags.reduce((flags, flag) => ({ ...flags, [flag]: 'enabled' }), {})
win.localStorage.setItem('featureFlags', JSON.stringify(featureFlags))
}
// Inject the mock ethereum provider.
if (options?.ethereum === 'hardhat') {
win.ethereum = provider
} else {
win.ethereum = injected
}
},
})
)
}
)

View File

@@ -5,89 +5,18 @@
// https://on.cypress.io/configuration
// ***********************************************************
// Import commands.ts using ES2015 syntax:
import { injected } from './ethereum'
import assert = require('assert')
import '@cypress/code-coverage/support'
import './commands'
import './setupTests'
import { FeatureFlag } from '../../src/featureFlags/flags/featureFlags'
declare global {
// eslint-disable-next-line @typescript-eslint/no-namespace
namespace Cypress {
interface ApplicationWindow {
ethereum: typeof injected
}
interface VisitOptions {
serviceWorker?: true
featureFlags?: Array<FeatureFlag>
selectedWallet?: string
noWallet?: boolean
}
}
}
// sets up the injected provider to be a mock ethereum provider with the given mnemonic/index
// eslint-disable-next-line no-undef
Cypress.Commands.overwrite(
'visit',
(original, url: string | Partial<Cypress.VisitOptions>, options?: Partial<Cypress.VisitOptions>) => {
assert(typeof url === 'string')
cy.intercept('/service-worker.js', options?.serviceWorker ? undefined : { statusCode: 404 }).then(() => {
original({
...options,
url:
(url.startsWith('/') && url.length > 2 && !url.startsWith('/#') ? `/#${url}` : url) +
`${url.includes('?') ? '&' : '?'}chain=goerli`,
onBeforeLoad(win) {
options?.onBeforeLoad?.(win)
win.localStorage.clear()
const userState = {
selectedWallet: options?.noWallet !== true ? options?.selectedWallet || 'INJECTED' : undefined,
fiatOnrampDismissed: true,
}
win.localStorage.setItem('redux_localstorage_simple_user', JSON.stringify(userState))
if (options?.featureFlags) {
const featureFlags = options.featureFlags.reduce(
(flags, flag) => ({
...flags,
[flag]: 'enabled',
}),
{}
)
win.localStorage.setItem('featureFlags', JSON.stringify(featureFlags))
}
win.ethereum = injected
},
})
})
}
)
beforeEach(() => {
// Infura security policies are based on Origin headers.
// These are stripped by cypress because chromeWebSecurity === false; this adds them back in.
cy.intercept(/infura.io/, (res) => {
res.headers['origin'] = 'http://localhost:3000'
res.alias = res.body.method
res.continue()
})
// Graphql security policies are based on Origin headers.
// These are stripped by cypress because chromeWebSecurity === false; this adds them back in.
cy.intercept('https://api.uniswap.org/v1/graphql', (res) => {
res.headers['origin'] = 'https://app.uniswap.org'
res.continue()
})
cy.intercept('https://beta.api.uniswap.org/v1/graphql', (res) => {
res.headers['origin'] = 'https://app.uniswap.org'
res.continue()
})
})
// Squelch logs from fetches, as they clutter the logs so much as to make them unusable.
// See https://docs.cypress.io/api/commands/intercept#Disabling-logs-for-a-request.
// TODO(https://github.com/cypress-io/cypress/issues/26069): Squelch only wildcard logs once Cypress allows it.
const log = Cypress.log
Cypress.log = function (options, ...args) {
if (options.displayName === 'script' || options.name === 'request') return
return log(options, ...args)
} as typeof log
Cypress.on('uncaught:exception', () => {
// returning false here prevents Cypress from failing the test

View File

@@ -0,0 +1,41 @@
// @ts-ignore
import TokenListJSON from '@uniswap/default-token-list'
beforeEach(() => {
// Many API calls enforce that requests come from our app, so we must mock Origin and Referer.
cy.intercept('*', (req) => {
req.headers['referer'] = 'https://app.uniswap.org'
req.headers['origin'] = 'https://app.uniswap.org'
})
// Infura uses a test endpoint, which allow-lists http://localhost:3000 instead.
cy.intercept(/infura.io/, (req) => {
req.headers['referer'] = 'http://localhost:3000'
req.headers['origin'] = 'http://localhost:3000'
req.alias = req.body.method
req.continue()
})
// Mock analytics responses to avoid analytics in tests.
cy.intercept('https://api.uniswap.org/v1/amplitude-proxy', (req) => {
const requestBody = JSON.stringify(req.body)
const byteSize = new Blob([requestBody]).size
req.reply(
JSON.stringify({
code: 200,
server_upload_time: Date.now(),
payload_size_bytes: byteSize,
events_ingested: req.body.events.length,
})
)
}).intercept('https://*.sentry.io', { statusCode: 200 })
// Mock our own token list responses to avoid the latency of IPFS.
cy.intercept('https://gateway.ipfs.io/ipns/tokens.uniswap.org', TokenListJSON)
.intercept('https://gateway.ipfs.io/ipns/extendedtokens.uniswap.org', { statusCode: 404 })
.intercept('https://gateway.ipfs.io/ipns/unsupportedtokens.uniswap.org', { statusCode: 404 })
// Reset hardhat between tests to ensure isolation.
// This resets the fork, as well as options like automine.
cy.hardhat().then((hardhat) => hardhat.reset())
})

View File

@@ -1,9 +1,17 @@
{
"compilerOptions": {
"esModuleInterop": true,
"incremental": true,
"lib": ["DOM", "DOM.Iterable", "ESNext"],
"noEmit": true,
"strict": true,
"target": "es5",
"lib": ["es5", "dom"],
"types": ["cypress"]
"target": "ES5",
"tsBuildInfoFile": "../node_modules/.cache/tsbuildinfo/cypress", // avoid clobbering the build tsbuildinfo
"types": ["cypress", "node"]
},
"include": ["**/*.ts"]
"exclude": ["node_modules"],
"include": ["**/*.ts"],
"watchOptions": {
"excludeDirectories": ["node_modules"]
}
}

View File

@@ -1,5 +1,13 @@
import { Currency } from '@uniswap/sdk-core'
export const getTestSelector = (selectorId: string) => `[data-testid=${selectorId}]`
export const getTestSelectorStartsWith = (selectorId: string) => `[data-testid^=${selectorId}]`
export const getClassContainsSelector = (selectorId: string) => `[class*=${selectorId}]`
/** Gets the balance of a token as a Chainable. */
export function getBalance(token: Currency) {
return cy
.hardhat()
.then((hardhat) => hardhat.getBalance(hardhat.wallet, token))
.then((balance) => Number(balance.toFixed(1)))
}

View File

@@ -0,0 +1,3 @@
import { UserState } from '../../src/state/user/reducer'
export const CONNECTED_WALLET_USER_STATE: Partial<UserState> = { selectedWallet: 'INJECTED' }

View File

@@ -0,0 +1,36 @@
/* eslint-env node */
module.exports = {
meta: {
type: 'problem',
docs: {
description: 'enforce use of retry() for dynamic imports',
category: 'Best Practices',
recommended: false,
},
schema: [],
},
create(context) {
return {
ImportExpression(node) {
const grandParent = node.parent.parent
if (
!(
grandParent &&
grandParent.type === 'CallExpression' &&
// Technically, we are only checking that a function named `retry` wraps the dynamic import.
// We do not go as far as enforcing that it is import('utils/retry').retry
grandParent.callee.name === 'retry' &&
grandParent.arguments.length === 1 &&
grandParent.arguments[0].type === 'ArrowFunctionExpression'
)
) {
context.report({
node,
message: 'Dynamic import should be wrapped in retry (see `utils/retry.ts`): `retry(() => import(...))`',
})
}
},
}
},
}

View File

@@ -0,0 +1,44 @@
/* eslint-env node */
module.exports = {
meta: {
type: 'suggestion',
docs: {
description: 'Enforce the use of optional object fields',
category: 'Best Practices',
recommended: true,
},
fixable: 'code',
schema: [],
},
create(context) {
return {
'TSPropertySignature > TSTypeAnnotation > TSUnionType': (node) => {
const types = node.types
const hasUndefined = types.some((typeNode) => typeNode.type === 'TSUndefinedKeyword')
if (hasUndefined) {
const typesWithoutUndefined = types.filter((typeNode) => typeNode.type !== 'TSUndefinedKeyword')
// If there is more than one type left after removing 'undefined',
// join them together with ' | ' to create a new union type.
const newTypeSource =
typesWithoutUndefined.length > 1
? typesWithoutUndefined.map((typeNode) => context.getSourceCode().getText(typeNode)).join(' | ')
: context.getSourceCode().getText(typesWithoutUndefined[0])
context.report({
node,
message: `Prefer optional properties to "Type | undefined".`,
fix(fixer) {
const propertySignature = node.parent.parent
const isAlreadyOptional = propertySignature.optional
const newTypeAnnotation = isAlreadyOptional ? `: ${newTypeSource}` : `?: ${newTypeSource}`
return fixer.replaceText(node.parent, newTypeAnnotation)
},
})
}
},
}
},
}

View File

@@ -1,6 +1,6 @@
/* eslint-env node */
const defaultConfig = require('./graphql.config')
const defaultConfig = require('./graphql.data.config')
module.exports = {
src: defaultConfig.src,

View File

@@ -1,9 +1,13 @@
/* eslint-env node */
require('dotenv').config()
// Block selection is arbitrary, as e2e tests will build up their own state.
// The only requirement is that all infrastructure under test (eg Permit2 contracts) are already deployed.
const BLOCK_NUMBER = 17023328
const mainnetFork = {
url: `https://mainnet.infura.io/v3/${process.env.REACT_APP_INFURA_KEY}`,
blockNumber: 17023328,
blockNumber: BLOCK_NUMBER,
httpHeaders: {
Origin: 'localhost:3000', // infura allowlists requests by origin
},
@@ -15,7 +19,11 @@ module.exports = {
chainId: 1,
forking: mainnetFork,
accounts: {
count: 1,
count: 2,
},
mining: {
auto: true, // automine to make tests easier to write.
interval: 0, // do not interval mine so that tests remain deterministic
},
},
},

View File

@@ -1,8 +1,77 @@
/* eslint-env node */
import { default as babelExtractor } from '@lingui/cli/api/extractors/babel'
import { createHash } from 'crypto'
import { mkdirSync, readFileSync, writeFileSync } from 'fs'
import { existsSync } from 'fs'
import * as path from 'path'
/** A custom caching extractor built on top of babelExtractor. */
const cachingExtractor: typeof babelExtractor = {
/** Delegates to babelExtractor.match. */
match(filename: string) {
return babelExtractor.match(filename)
},
/**
* Checks a cache before extraction, only delegating to babelExtractor.extract if the file has changed.
*
* The lingui extractor works by extracting JSON (the catalog) from `filename` to `buildDir/filename.json`.
* Caching works by man-in-the-middling this:
* - File freshness is computed as a hash of `filename` contents.
* - Before extracting, we check the cache to see if we already have a fresh catalog for the file.
* If we do, we copy it to `localeDir/filename.json`. Copying is significantly faster than extracting.
* - After extracting, we copy the catalog to the cache.
*/
extract(filename: string, localeDir: string, ...options: unknown[]) {
// This runs from node_modules/@lingui/conf, so we need to back out to the root.
const root = __dirname.split('/node_modules')[0]
// This logic mimics catalogFilename in @lingui/babel-plugin-extract-messages.
const buildDir = path.join(localeDir, '_build')
const localePath = path.join(buildDir, filename + '.json')
const filePath = path.join(root, filename)
const fileHash = createHash('sha256').update(readFileSync(filePath)).digest('hex')
const cacheRoot = path.join(root, 'node_modules/.cache/lingui')
const cachePath = path.join(cacheRoot, filename + '.json')
// If we have a matching cached copy of the catalog, we can copy it to localePath and return early.
if (existsSync(cachePath)) {
const { hash, catalog } = JSON.parse(readFileSync(cachePath, 'utf8'))
if (hash === fileHash) {
if (catalog) {
mkdirSync(path.dirname(localePath), { recursive: true })
writeFileSync(localePath, JSON.stringify(catalog, null, 2))
}
return
}
}
babelExtractor.extract(filename, localeDir, ...options)
// Cache the extracted catalog.
mkdirSync(path.dirname(cachePath), { recursive: true })
if (existsSync(localePath)) {
const catalog = JSON.parse(readFileSync(localePath, 'utf8'))
writeFileSync(cachePath, JSON.stringify({ hash: fileHash, catalog }))
} else {
writeFileSync(cachePath, JSON.stringify({ hash: fileHash }))
}
},
}
const linguiConfig = {
catalogs: [
{
path: '<rootDir>/src/locales/{locale}',
include: ['<rootDir>/src'],
include: ['<rootDir>/src/**/*.ts', '<rootDir>/src/**/*.tsx'],
exclude: [
'<rootDir>/src/**/*.d.ts',
'<rootDir>/src/**/*.test.*',
'<rootDir>/src/types/v3/**',
'<rootDir>/src/abis/types/**',
'<rootDir>/src/graphql/**/__generated__/**',
],
},
],
compileNamespace: 'cjs',
@@ -53,6 +122,7 @@ const linguiConfig = {
runtimeConfigModule: ['@lingui/core', 'i18n'],
sourceLocale: 'en-US',
pseudoLocale: 'pseudo',
extractors: [cachingExtractor],
}
export default linguiConfig

View File

@@ -5,28 +5,28 @@
"homepage": ".",
"license": "GPL-3.0-or-later",
"scripts": {
"ajv": "node scripts/compile-ajv-validators.js",
"contracts:compile:abi": "typechain --target ethers-v5 --out-dir src/abis/types \"./src/abis/**/*.json\"",
"contracts:compile:v3": "typechain --target ethers-v5 --out-dir src/types/v3 \"./node_modules/@uniswap/**/artifacts/contracts/**/*[!dbg].json\"",
"contracts:compile": "yarn contracts:compile:abi && yarn contracts:compile:v3",
"contracts": "yarn contracts:compile:abi && yarn contracts:compile:v3",
"graphql:fetch": "node scripts/fetch-schema.js",
"graphql:generate:data": "graphql-codegen --config apollo-codegen.ts",
"graphql:generate:thegraph": "graphql-codegen --config apollo-codegen_thegraph.ts",
"graphql:generate:data": "graphql-codegen --config graphql.data.codegen.config.ts",
"graphql:generate:thegraph": "graphql-codegen --config graphql.thegraph.codegen.config.ts",
"graphql:generate": "yarn graphql:generate:data && yarn graphql:generate:thegraph",
"prei18n:extract": "node scripts/prei18n-extract.js",
"graphql": "yarn graphql:fetch && yarn graphql:generate",
"i18n:extract": "lingui extract --locale en-US",
"i18n:compile": "yarn i18n:extract && lingui compile",
"i18n:pseudo": "lingui extract --locale pseudo && lingui compile",
"prepare": "yarn contracts:compile && yarn graphql:fetch && yarn graphql:generate && yarn i18n:compile",
"postinstall": "patch-package",
"i18n:pseudo": "lingui extract --locale pseudo",
"i18n:compile": "lingui compile",
"i18n": "yarn i18n:extract --clean && yarn i18n:compile",
"prepare": "concurrently \"npm:ajv\" \"npm:contracts\" \"npm:graphql\" \"npm:i18n\"",
"start": "craco start",
"build": "craco build",
"build:e2e": "REACT_APP_CSP_ALLOW_UNSAFE_EVAL=true REACT_APP_ADD_COVERAGE_INSTRUMENTATION=true craco build",
"analyze": "source-map-explorer 'build/static/js/*.js'",
"analyze": "source-map-explorer 'build/static/js/*.js' --only-mapped",
"serve": "serve build -l 3000",
"lint": "yarn eslint --ignore-path .gitignore --cache --cache-location node_modules/.cache/eslint/ .",
"typecheck": "tsc --noEmit",
"test": "craco test --coverage",
"test:size": "node scripts/test-size.js",
"typecheck": "tsc",
"test": "craco test",
"cypress:open": "cypress open --browser chrome --e2e",
"cypress:run": "cypress run --browser chrome --e2e",
"deduplicate": "yarn-deduplicate --strategy=highest"
@@ -67,7 +67,7 @@
]
},
"devDependencies": {
"@craco/craco": "6.4.3",
"@craco/craco": "^7.1.0",
"@ethersproject/experimental": "^5.4.0",
"@lingui/cli": "^3.9.0",
"@testing-library/jest-dom": "^5.16.4",
@@ -95,26 +95,33 @@
"@types/rebass": "^4.0.7",
"@types/styled-components": "^5.1.25",
"@types/testing-library__cypress": "^5.0.5",
"@types/ua-parser-js": "^0.7.35",
"@types/ua-parser-js": "^0.7.36",
"@types/uuid": "^8.3.4",
"@types/wcag-contrast": "^3.0.0",
"@uniswap/eslint-config": "^1.1.1",
"@uniswap/default-token-list": "^9.4.0",
"@uniswap/eslint-config": "^1.2.0",
"@vanilla-extract/babel-plugin": "^1.1.7",
"@vanilla-extract/jest-transform": "^1.1.1",
"@vanilla-extract/webpack-plugin": "^2.1.11",
"babel-plugin-istanbul": "^6.1.1",
"cypress": "10.3.1",
"cypress-hardhat": "^1.0.0",
"buffer": "^6.0.3",
"concurrently": "^8.0.1",
"cypress": "12.12.0",
"cypress-hardhat": "^2.3.0",
"env-cmd": "^10.1.0",
"eslint": "^7.11.0",
"eslint-plugin-import": "^2.27",
"eslint-plugin-rulesdir": "^0.2.2",
"hardhat": "^2.14.0",
"jest-fail-on-console": "^3.1.1",
"jest-fetch-mock": "^3.0.3",
"jest-styled-components": "^7.0.8",
"ms.macro": "^2.0.0",
"patch-package": "^6.4.7",
"postinstall-postinstall": "^2.1.0",
"path-browserify": "^1.0.1",
"prettier": "^2.7.1",
"react-scripts": "^4.0.3",
"process": "^0.11.10",
"react-scripts": "^5.0.1",
"resize-observer-polyfill": "^1.5.1",
"serve": "^11.3.2",
"source-map-explorer": "^2.5.3",
"ts-transform-graphql-tag": "^0.2.1",
@@ -134,12 +141,13 @@
"@graphql-codegen/typescript-operations": "^2.5.8",
"@graphql-codegen/typescript-react-apollo": "^3.3.7",
"@graphql-codegen/typescript-resolvers": "^2.7.8",
"@juggle/resize-observer": "^3.4.0",
"@lingui/core": "^3.14.0",
"@lingui/macro": "^3.14.0",
"@lingui/react": "^3.14.0",
"@looksrare/sdk": "^0.10.2",
"@metamask/jazzicon": "^2.0.0",
"@opensea/seaport-js": "^1.0.10",
"@opensea/seaport-js": "^1.2.0",
"@popperjs/core": "^2.4.4",
"@reach/dialog": "^0.10.3",
"@reach/portal": "^0.10.3",
@@ -159,7 +167,7 @@
"@uniswap/router-sdk": "^1.3.0",
"@uniswap/sdk-core": "^3.2.2",
"@uniswap/smart-order-router": "^3.6.1",
"@uniswap/token-lists": "^1.0.0-beta.30",
"@uniswap/token-lists": "^1.0.0-beta.31",
"@uniswap/universal-router-sdk": "^1.3.8",
"@uniswap/v2-core": "1.0.0",
"@uniswap/v2-periphery": "^1.1.0-beta.0",
@@ -167,7 +175,6 @@
"@uniswap/v3-core": "1.0.0",
"@uniswap/v3-periphery": "^1.1.1",
"@uniswap/v3-sdk": "^3.9.0",
"@uniswap/widgets": "^2.49.0",
"@vanilla-extract/css": "^1.7.2",
"@vanilla-extract/css-utils": "^0.1.2",
"@vanilla-extract/dynamic": "^2.0.2",
@@ -190,6 +197,8 @@
"@web3-react/types": "^8.2.0",
"@web3-react/url": "^8.2.0",
"@web3-react/walletconnect": "^8.2.0",
"ajv": "^8.11.0",
"ajv-formats": "^2.1.1",
"array.prototype.flat": "^1.2.4",
"array.prototype.flatmap": "^1.2.4",
"cids": "^1.0.0",
@@ -238,7 +247,7 @@
"statsig-react": "^1.22.0",
"styled-components": "^5.3.5",
"tiny-invariant": "^1.2.0",
"ua-parser-js": "^0.7.28",
"ua-parser-js": "^1.0.35",
"use-resize-observer": "^9.0.2",
"uuid": "^8.3.2",
"video-extensions": "^1.2.0",
@@ -250,18 +259,6 @@
"workbox-routing": "^6.1.0",
"zustand": "^4.3.6"
},
"resolutions": {
"@web3-react/coinbase-wallet": "^8.2.0",
"@web3-react/core": "^8.2.0",
"@web3-react/eip1193": "^8.2.0",
"@web3-react/empty": "^8.2.0",
"@web3-react/gnosis-safe": "^8.2.0",
"@web3-react/metamask": "^8.2.0",
"@web3-react/network": "^8.2.0",
"@web3-react/types": "^8.2.0",
"@web3-react/url": "^8.2.0",
"@web3-react/walletconnect": "^8.2.0"
},
"engines": {
"npm": "please-use-yarn",
"node": "14",

View File

@@ -1,22 +0,0 @@
diff --git a/node_modules/@vanilla-extract/css/dist/vanilla-extract-css.cjs.dev.js b/node_modules/@vanilla-extract/css/dist/vanilla-extract-css.cjs.dev.js
index 6e40061..10283a2 100644
--- a/node_modules/@vanilla-extract/css/dist/vanilla-extract-css.cjs.dev.js
+++ b/node_modules/@vanilla-extract/css/dist/vanilla-extract-css.cjs.dev.js
@@ -177,11 +177,13 @@ function generateIdentifier(debugId) {
var fileScopeHash = hash__default["default"](packageName ? "".concat(packageName).concat(filePath) : filePath);
var identifier = "".concat(fileScopeHash).concat(refCount);
- if (adapter_dist_vanillaExtractCssAdapter.getIdentOption() === 'debug') {
- var devPrefix = getDevPrefix(debugId);
+ if (process.env.VANILLA_EXTRACT_DEV_PREFIX) {
+ if (adapter_dist_vanillaExtractCssAdapter.getIdentOption() === 'debug') {
+ var devPrefix = getDevPrefix(debugId);
- if (devPrefix) {
- identifier = "".concat(devPrefix, "__").concat(identifier);
+ if (devPrefix) {
+ identifier = "".concat(devPrefix, "__").concat(identifier);
+ }
}
}

View File

@@ -1,11 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 364 364">
<defs>
<style>.cls-1{fill:#0052ff;}.cls-2{fill:#fff;}</style>
</defs>
<g id="Layer_2" data-name="Layer 2">
<g id="Layer_1-2" data-name="Layer 1">
<rect class="cls-1" width="364" height="364" />
<path class="cls-2" d="M232.77,192.91a53.51,53.51,0,1,1-.08-22.16,1.38,1.38,0,0,0,1.34,1.09h51.48a1.38,1.38,0,0,0,1.37-1.52A107.12,107.12,0,0,0,178.39,74.9c-57.49,1-104.27,48-105.1,105.51a107.12,107.12,0,0,0,213.63,12.92,1.39,1.39,0,0,0-1.38-1.52H234.11A1.36,1.36,0,0,0,232.77,192.91Z"/>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 593 B

View File

@@ -1,14 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 364 364">
<defs>
<style>.cls-1{fill:#0083c9;}.cls-2{fill:#f89c1b;}.cls-3{fill:#80c241;}.cls-4{fill:#ed1c24;}</style>
</defs>
<g id="Layer_2" data-name="Layer 2">
<g id="Layer_1-2" data-name="Layer 1">
<rect width="364" height="364" />
<rect class="cls-1" x="140.51" y="105.4" width="39.59" height="153.99"/>
<rect class="cls-2" x="190.26" y="106.12" width="39.59" height="103.62"/>
<rect class="cls-3" x="226.65" y="183.4" width="39.59" height="112.38" transform="translate(6.86 486.04) rotate(-90)"/>
<rect class="cls-4" x="75.34" y="204.11" width="39.59" height="70.95" transform="translate(-144.45 334.72) rotate(-90)"/>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 743 B

View File

@@ -1,6 +0,0 @@
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="32" height="32" fill="black"/>
<path d="M8.4264 12.5C8.50338 12.3667 8.69583 12.3667 8.77281 12.5L12.7565 19.4C12.8335 19.5333 12.7373 19.7 12.5833 19.7H4.61589C4.46193 19.7 4.36571 19.5333 4.44269 19.4L8.4264 12.5Z" fill="white"/>
<circle cx="15.9" cy="16" r="3.9" fill="white"/>
<rect x="20.25" y="12.35" width="7.25" height="7.25" rx="0.2" fill="white"/>
</svg>

Before

Width:  |  Height:  |  Size: 474 B

View File

@@ -1,5 +0,0 @@
<svg width="200" height="200" viewBox="0 0 200 200" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="200" height="200" fill="#04CD58"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M100 112C87.3026 112 77 101.708 77 89C77 76.2923 87.3026 66 100 66C112.697 66 123 76.2923 123 89C123 101.708 112.697 112 100 112ZM90 89C90 94.5251 94.4794 99 100 99C105.521 99 110 94.5251 110 89C110 83.4749 105.521 79 100 79C94.4794 79 90 83.4749 90 89Z" fill="black"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M26 89.0304L70 45H130L174 89.0304L100 163L26 89.0304ZM134 72.9998C115.305 54.2224 84.6953 54.2225 66 72.9999L50 89.0001L66 105C84.6953 123.778 115.305 123.777 134 105L150 89.0001L134 72.9998Z" fill="black"/>
</svg>

Before

Width:  |  Height:  |  Size: 731 B

View File

@@ -1,17 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 363 364">
<defs>
<style>.cls-1{fill:#3d3d3d;}.cls-2{fill:#24ddd8;}</style>
</defs>
<g id="Layer_2" data-name="Layer 2">
<g id="Layer_1-2" data-name="Layer 1">
<rect class="cls-1" width="363" height="364"/>
<path class="cls-2" d="M218.1,115.8V102.1S187,101,177.94,115.76l-.22-13.45s-28.93-1.57-42.2,15.37c-3.53,4.51-6.09,8.14-7.88,12.7-3.86,9.81-3.27,24.88-3.27,24.88l0,66s-8.86-1.72-14.32,3a1.68,1.68,0,0,1-2,.23,1.4,1.4,0,0,1-.57-.8c-1.52-5.38-12.33-8.91-18.74-2.09-6.87,7.31-3.2,20.58,8.72,21.06,0,0-2.49,19.78,13.62,27.16,13.38,6.13,22,0,22,0s5,6.92,17.61,6.92h86.71c6.24,0,20.32-7.19,20.32-21.73V102.27S225,101.08,218.1,115.8ZM250.17,256c0,10.83-13.41,13.94-15.23,13.94H149.49a18.79,18.79,0,0,1-15.24-7.75c-5.62,4.89-13.89,3.68-13.89,3.68-16-1.09-16.76-19.32-15.67-22.93s.26-6.87-6.28-6.77-8.08-6.93-3.77-9.69,8.41-.16,8.44,3.19c0,2,2,4.43,5.19,4.53,4.23.13,6.21-5.52,12.68-5.88a19.4,19.4,0,0,1,10.77,2.4s.82-52.07.82-85.95c0-7.69,3.83-15.12,7.79-20.3a36.74,36.74,0,0,1,8-7.8,37.65,37.65,0,0,1,22.83-7.12v37.59h0c0-34.3,39.88-37.59,39.88-37.59v36.93h0c0-34.61,39.1-36.93,39.1-36.93Z"/>
<path class="cls-2" d="M238.24,214.27v31.47a11,11,0,0,1-11,11h-1.9a.65.65,0,0,1-.65-.65v-6.49a6.8,6.8,0,0,0-5.76-6.81,6.65,6.65,0,0,0-7.52,6.59v6.29a1.06,1.06,0,0,1-1.06,1.07h-37.8a1.07,1.07,0,0,1-1.07-1.07v-6a6.82,6.82,0,0,0-6.44-6.91,6.64,6.64,0,0,0-6.84,6.64v6.71a.65.65,0,0,1-.65.65h-1.45a11,11,0,0,1-11-11V214.28a11,11,0,0,1,11-11h1.45a.65.65,0,0,1,.65.65v6.84a6.64,6.64,0,0,0,13.28,0V204.3a1.06,1.06,0,0,1,1.07-1.06h37.8a1.05,1.05,0,0,1,1.06,1.06v6.14a6.82,6.82,0,0,0,6.44,6.91,6.63,6.63,0,0,0,6.84-6.62v-6.84a.65.65,0,0,1,.65-.65h1.91A11,11,0,0,1,238.24,214.27Z"/>
<rect class="cls-2" x="158.03" y="170.28" width="13.88" height="20.1" rx="3.72"/>
<rect class="cls-2" x="211.11" y="170.28" width="13.88" height="20.1" rx="3.72"/>
<path class="cls-2" d="M242.71,116.19s-29.8,2.46-25.51,37.8a92,92,0,0,1,8.33-23.15C229.06,124.34,237.32,119.4,242.71,116.19Z"/>
<path class="cls-2" d="M202.64,116.19s-29.8,2.46-25.51,37.8a92,92,0,0,1,8.33-23.15C189,124.34,197.25,119.4,202.64,116.19Z"/>
<path class="cls-2" d="M163.31,116.19s-29.8,2.46-25.51,37.8a92,92,0,0,1,8.33-23.15C149.65,124.34,157.92,119.4,163.31,116.19Z"/>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 2.3 KiB

View File

@@ -1,30 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 364 364.73">
<defs>
<style>.cls-1{fill:#fffdfd;}.cls-2{fill:url(#linear-gradient);}.cls-3{isolation:isolate;opacity:0.5;fill:url(#linear-gradient-2);}</style>
<linearGradient id="linear-gradient" x1="235.4" y1="269.1" x2="133.56" y2="167.26" gradientTransform="matrix(1, 0, 0, -1, 0, 364.73)" gradientUnits="userSpaceOnUse">
<stop offset="0.09" stop-color="#ff6d41"/>
<stop offset="0.5" stop-color="#fa297f"/>
<stop offset="1" stop-color="#fa297f"/>
</linearGradient>
<linearGradient id="linear-gradient-2" x1="207.26" y1="295.68" x2="105" y2="193.43" gradientTransform="matrix(1, 0, 0, -1, 0, 364.73)" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#652cb4"/><stop offset="0.07" stop-color="#652cb4" stop-opacity="0.82"/>
<stop offset="0.19" stop-color="#652cb4" stop-opacity="0.53"/>
<stop offset="0.3" stop-color="#652cb4" stop-opacity="0.3"/>
<stop offset="0.4" stop-color="#652cb4" stop-opacity="0.14"/>
<stop offset="0.49" stop-color="#652cb4" stop-opacity="0.04"/>
<stop offset="0.55" stop-color="#652cb4" stop-opacity="0"/>
</linearGradient>
</defs>
<g id="Layer_2" data-name="Layer 2">
<g id="Layer_1-2" data-name="Layer 1">
<g id="NFTX"><rect class="cls-1" width="364" height="364.73" />
<polygon class="cls-2" points="182.52 38.79 73.06 148.78 182.52 254.16 291.97 148.78 182.52 38.79"/>
<polygon class="cls-3" points="182.52 148.78 73.06 148.78 182.52 38.79 182.52 148.78"/>
<path d="M127.44,242.37a116.42,116.42,0,0,1,7.42-9.31C102,241,79.31,257.71,79.31,277c0,.72,0,1.44.1,2.15,3.23-1.23,6.64-2.7,10.25-4.49C111.39,263.94,119.92,252.48,127.44,242.37Z"/>
<path d="M161.7,277.86C168.28,269,174,261.38,190,253.45s25.54-7.77,36.53-7.6c7.36.11,15.3.24,25-1.85-11.81-6.37-27-11.2-44.15-13.78l-24.86,23.93-17.38-16.73c-12.39,8.38-18.73,16.9-24.49,24.63-6.58,8.85-12.26,16.49-28.32,24.41C101.07,292,93,293.61,85.45,294c6,8,16.06,15,29,20.35,3-1.17,6.16-2.56,9.5-4.2C145.65,299.43,154.17,288,161.7,277.86Z"/>
<path d="M261.22,255c-12.57-.21-26.8-.43-48.53,10.3s-30.26,22.19-37.78,32.3C168.33,306.4,162.65,314,146.6,322c-.73.36-1.43.7-2.14,1a193,193,0,0,0,33.94,2.95c2.5,0,5-.05,7.45-.14A135,135,0,0,0,196,313.35c6.58-8.85,12.26-16.49,28.32-24.41s25.55-7.77,36.53-7.6a130.72,130.72,0,0,0,16.37-.47,23.62,23.62,0,0,0,.31-3.85c0-7.92-3.82-15.4-10.59-22C265,255,263.14,255,261.22,255Z"/>
<path d="M218.12,321.83c26-5.61,46.1-16.56,54.75-30A97.82,97.82,0,0,0,247,300.73C232.93,307.66,224.42,314.87,218.12,321.83Z"/>
</g>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 2.6 KiB

View File

@@ -1,5 +0,0 @@
<svg width="100" height="100" viewBox="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="100" height="100" fill="#2081E2"/>
<path d="M24.6679 51.6802L24.8836 51.3411L37.8906 30.9933C38.0807 30.6954 38.5276 30.7262 38.6714 31.0498C40.8444 35.9197 42.7194 41.9763 41.841 45.7469C41.466 47.2983 40.4386 49.3993 39.2827 51.3411C39.1338 51.6237 38.9694 51.9011 38.7947 52.1682C38.7125 52.2915 38.5738 52.3634 38.4248 52.3634H25.048C24.6884 52.3634 24.4778 51.973 24.6679 51.6802Z" fill="white"/>
<path d="M82.6444 55.461V58.6819C82.6444 58.8668 82.5314 59.0312 82.367 59.1031C81.3602 59.5346 77.9132 61.1168 76.48 63.11C72.8224 68.2008 70.0279 75.48 63.7812 75.48H37.721C28.4847 75.48 21 67.9697 21 58.7024V58.4045C21 58.1579 21.2003 57.9576 21.4469 57.9576H35.9745C36.2621 57.9576 36.4727 58.2247 36.4471 58.5072C36.3443 59.4524 36.519 60.4182 36.9659 61.2966C37.8289 63.0484 39.6166 64.1426 41.5481 64.1426H48.74V58.5278H41.6303C41.2656 58.5278 41.0499 58.1065 41.2605 57.8086C41.3375 57.6904 41.4249 57.5672 41.5173 57.4285C42.1903 56.473 43.1509 54.9884 44.1064 53.2983C44.7588 52.1579 45.3906 50.9404 45.8992 49.7178C46.002 49.4969 46.0841 49.2708 46.1663 49.0499C46.305 48.6595 46.4489 48.2948 46.5516 47.9301C46.6544 47.6218 46.7365 47.2982 46.8187 46.9951C47.0602 45.9574 47.1629 44.8581 47.1629 43.7177C47.1629 43.2708 47.1424 42.8033 47.1013 42.3564C47.0807 41.8684 47.0191 41.3803 46.9574 40.8923C46.9163 40.4608 46.8393 40.0344 46.7571 39.5875C46.6544 38.9351 46.5105 38.2879 46.3461 37.6354L46.2896 37.3889C46.1663 36.9419 46.0636 36.5156 45.9198 36.0687C45.5139 34.6662 45.0465 33.2998 44.5533 32.0207C44.3735 31.5121 44.168 31.0241 43.9625 30.5361C43.6595 29.8015 43.3512 29.1337 43.0687 28.5018C42.9249 28.2141 42.8016 27.9521 42.6783 27.685C42.5396 27.3819 42.3958 27.0788 42.2519 26.7912C42.1492 26.5703 42.031 26.3648 41.9488 26.1593L41.0704 24.536C40.9471 24.3151 41.1526 24.0531 41.394 24.1199L46.8907 25.6096H46.9061C46.9163 25.6096 46.9215 25.6148 46.9266 25.6148L47.6509 25.8151L48.4472 26.0412L48.74 26.1233V22.8562C48.74 21.2791 50.0037 20 51.5654 20C52.3462 20 53.0551 20.3185 53.5637 20.8373C54.0722 21.3562 54.3907 22.0651 54.3907 22.8562V27.7056L54.9764 27.8699C55.0226 27.8854 55.0688 27.9059 55.1099 27.9367C55.2538 28.0446 55.4592 28.2038 55.7212 28.3991C55.9267 28.5634 56.1476 28.7638 56.4147 28.9693C56.9438 29.3956 57.5757 29.9453 58.2692 30.5772C58.4541 30.7364 58.6339 30.9008 58.7983 31.0652C59.6922 31.8974 60.6939 32.8734 61.6494 33.9522C61.9165 34.2553 62.1785 34.5635 62.4456 34.8871C62.7127 35.2159 62.9953 35.5395 63.2418 35.8632C63.5655 36.2947 63.9148 36.7416 64.2179 37.2091C64.3617 37.43 64.5261 37.656 64.6648 37.8769C65.0552 38.4676 65.3994 39.079 65.7282 39.6903C65.8669 39.9728 66.0107 40.281 66.134 40.5841C66.4987 41.4009 66.7864 42.2331 66.9713 43.0653C67.0278 43.2451 67.0689 43.4403 67.0895 43.615V43.6561C67.1511 43.9026 67.1717 44.1646 67.1922 44.4317C67.2744 45.2845 67.2333 46.1372 67.0484 46.9951C66.9713 47.3599 66.8686 47.704 66.7453 48.0688C66.622 48.4181 66.4987 48.7828 66.3395 49.127C66.0313 49.841 65.6665 50.5551 65.235 51.2229C65.0963 51.4695 64.9319 51.7315 64.7675 51.9781C64.5877 52.24 64.4028 52.4866 64.2384 52.7281C64.0124 53.0363 63.771 53.3599 63.5244 53.6476C63.3035 53.9507 63.0775 54.2538 62.8309 54.5209C62.4867 54.9267 62.1579 55.312 61.8137 55.6819C61.6083 55.9233 61.3874 56.1699 61.1613 56.3908C60.9405 56.6373 60.7144 56.8582 60.5089 57.0637C60.1648 57.4079 59.8771 57.675 59.6356 57.8959L59.0706 58.4148C58.9884 58.4867 58.8805 58.5278 58.7675 58.5278H54.3907V64.1426H59.8976C61.1305 64.1426 62.3018 63.7059 63.247 62.9045C63.5706 62.622 64.9833 61.3994 66.6528 59.5552C66.7093 59.4935 66.7813 59.4473 66.8635 59.4268L82.0742 55.0295C82.3568 54.9473 82.6444 55.163 82.6444 55.461Z" fill="white"/>
</svg>

Before

Width:  |  Height:  |  Size: 3.7 KiB

View File

@@ -1,12 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 363 364">
<defs>
<style>.cls-1{fill:#fcd903;}.cls-2{fill:#020100;}</style>
</defs>
<g id="Layer_2" data-name="Layer 2">
<g id="Layer_1-2" data-name="Layer 1">
<path class="cls-1" d="M285.85,364H77.15A77.15,77.15,0,0,1,0,286.85V77.15A77.15,77.15,0,0,1,77.15,0h208.7A77.15,77.15,0,0,1,363,77.15v209.7A77.15,77.15,0,0,1,285.85,364ZM74.09,107.52V267.34H137.6V213.41c26.76,0,53-.21,79.21.11,9.71.12,14.67,5.83,14.84,15.73.18,10.13,0,20.27,0,30.4v7.72h63.37c0-15.84.82-31.47-.24-47-1-14.64-9.32-25.25-22.75-31.56-1.29-.61-2.62-1.12-4-1.71.27-.36.35-.54.48-.61a8.62,8.62,0,0,1,1.32-.67c15-5.49,22.36-16.53,23.09-32.17.77-16.55-4.49-30-20-38.08-12.45-6.44-26-8.1-39.64-8.17-52.49-.26-105-.18-157.48-.21A11.44,11.44,0,0,0,74.09,107.52Z"/>
<path class="cls-2" d="M74.09,107.52a11.44,11.44,0,0,1,1.73-.3c52.5,0,105,0,157.48.21,13.65.07,27.19,1.73,39.64,8.17,15.55,8,20.81,21.53,20,38.08-.73,15.64-8.11,26.68-23.09,32.17a8.62,8.62,0,0,0-1.32.67c-.13.07-.21.25-.48.61,1.37.59,2.7,1.1,4,1.71,13.43,6.31,21.75,16.92,22.75,31.56,1.06,15.5.24,31.13.24,47H231.7v-7.72c0-10.13.13-20.27,0-30.4-.17-9.9-5.13-15.61-14.84-15.73-26.22-.32-52.45-.11-79.21-.11v53.93H74.09Zm63.65,62.39c23.83,0,47.39,0,70.95,0,3.66,0,7.32,0,11-.28,5.58-.49,6.71-2.27,7.14-8.08.41-5.37-2.86-6.48-6.93-7a49.5,49.5,0,0,0-6-.25q-35.89,0-71.78,0c-1.44,0-2.87.13-4.37.2Z"/>
<path class="cls-1" d="M137.74,169.91v-15.4c1.5-.07,2.93-.2,4.37-.2q35.89,0,71.78,0a49.5,49.5,0,0,1,6,.25c4.07.5,7.34,1.61,6.93,7-.43,5.81-1.56,7.59-7.14,8.08-3.64.32-7.3.28-11,.28C185.13,169.92,161.57,169.91,137.74,169.91Z"/>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.6 KiB

View File

@@ -1,6 +0,0 @@
<svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0 14.4C0 6.4471 6.4471 0 14.4 0H33.6C41.5529 0 48 6.4471 48 14.4V33.6C48 41.5529 41.5529 48 33.6 48H14.4C6.4471 48 0 41.5529 0 33.6V14.4Z" fill="#B9B9FF"/>
<path d="M9.29999 15H12L15.3 24L12 33H9.29999L12.6 24L9.29999 15Z" fill="#121212"/>
<path d="M23.5727 33.192C22.1807 33.192 21.0367 32.872 20.1407 32.232C19.2607 31.592 18.8207 30.72 18.8207 29.616H21.2447C21.2447 30.112 21.4607 30.496 21.8927 30.768C22.3407 31.024 22.9167 31.152 23.6207 31.152H24.6287C25.4607 31.152 26.0767 30.992 26.4767 30.672C26.8767 30.336 27.0767 29.896 27.0767 29.352C27.0767 28.808 26.8847 28.384 26.5007 28.08C26.1167 27.776 25.5727 27.56 24.8687 27.432L23.0687 27.168C20.4447 26.752 19.1327 25.48 19.1327 23.352C19.1327 22.136 19.5327 21.208 20.3327 20.568C21.1327 19.928 22.2767 19.608 23.7647 19.608H24.6767C26.0527 19.608 27.1567 19.92 27.9887 20.544C28.8207 21.152 29.2367 21.96 29.2367 22.968H26.8127C26.8127 22.568 26.6127 22.248 26.2127 22.008C25.8287 21.768 25.3007 21.648 24.6287 21.648H23.7167C22.9807 21.648 22.4207 21.8 22.0367 22.104C21.6527 22.392 21.4607 22.808 21.4607 23.352C21.4607 24.28 22.1167 24.848 23.4287 25.056L25.2287 25.344C26.6687 25.568 27.7247 25.992 28.3967 26.616C29.0687 27.224 29.4047 28.104 29.4047 29.256C29.4047 30.488 28.9967 31.456 28.1807 32.16C27.3807 32.848 26.1807 33.192 24.5807 33.192H23.5727Z" fill="#121212"/>
<path d="M38.7 15H36L32.7 24L36 33H38.7L35.4 24L38.7 15Z" fill="#121212"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -1,34 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 364 364">
<defs>
<style>.cls-1{fill:#fff;}.cls-2{fill:#0d0f23;}.cls-3{fill:#2f2f2f;}.cls-4{fill:url(#linear-gradient);}.cls-5{fill:url(#linear-gradient-2);}</style>
<linearGradient id="linear-gradient" x1="177.18" y1="64.07" x2="177.18" y2="292.45" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#1bace4"/>
<stop offset="0.7" stop-color="#fa52a0"/>
</linearGradient>
<linearGradient id="linear-gradient-2" x1="164.69" y1="109.21" x2="242.53" y2="187.05" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#8f82c4"/>
<stop offset="1" stop-color="#e65ba7"/>
</linearGradient>
</defs>
<g id="Layer_2" data-name="Layer 2">
<g id="Layer_1-2" data-name="Layer 1">
<rect class="cls-1" width="364" height="364" />
<path class="cls-2" d="M306.26,190.34h0a58.84,58.84,0,0,0-2.07-7.38c-1-2.39-10.24-2.92-11.31-3q.81,2.07,1.5,4.2c2.1,6.46,3.42,13.7,2.81,20.39-1,10.66-9.86,16.75-23.12,17.78-1.82.14-3.65,0-5.48,0a101.74,101.74,0,0,1-16.07-1.56l-1.17-.21c-1-.18-1.72-.32-2.06-.4l-.13,0-.11,0a80.19,80.19,0,0,1-5.48,7.6l.22,0c.76,0,2.81.49,2.3,2.4-.56,2.08-3.55,1.64-5.55,1.17-6.58,7.34-16.39,15.73-27.36,16.53-19.84,1.44-30-20.5-47-39.29S122.52,218.3,112,228.86s-20.79,9.39-20.79,9.39.38.39,1.12,1.08l0,0a249,249,0,0,0,42.51,33.08c7.21,4.5,14.53,8.74,22,12.53,5.1,2.45,10.56,4.92,16.42,7.4l.18.08.13.05A135.84,135.84,0,0,0,215.92,302c15.06.83,28.52-2.82,37.92-16.29l1.1-1.57,0-.05.84-1.19c13.11-18.76,26.36-37.44,40-55.84l.74-1C307.51,211.05,307.64,198.11,306.26,190.34Zm-30.21,43.22c-1.38.18-12.9,1.8-23.57-.17,0,0-2.84,0-2.84-2.5,0,0,.22-2.36,4.14-1.36s16.31,1.41,21.63.16c0,0,2.19-.72,2.89,1S277.43,233.39,276.05,233.56Z"/>
<path class="cls-3" d="M255.81,282.93c13.11-18.76,26.36-37.44,40-55.84C280.76,247.75,260.19,276.75,255.81,282.93Z"/>
<path class="cls-1" d="M249.64,230.89s.22-2.36,4.14-1.36,16.31,1.41,21.63.16c0,0,2.19-.72,2.89,1s-.87,2.67-2.25,2.84-12.9,1.8-23.57-.17C252.48,233.39,249.64,233.34,249.64,230.89Z"/>
<path class="cls-3" d="M252.52,220.8c.78.14,1.57.25,2.36.37C254,221.05,253.22,220.92,252.52,220.8Z"/>
<path class="cls-3" d="M306.26,190.33a58.84,58.84,0,0,0-2.07-7.38.3.3,0,0,1,0,.08A44.32,44.32,0,0,1,306.26,190.33Z"/>
<path class="cls-4" d="M173.32,292.37c-5.86-2.48-11.32-4.95-16.42-7.4l2,1,2.05,1,1,.49q.76.38,1.53.72,1.16.54,2.31,1.05c2.52,1.13,5.06,2.19,7.63,3.2Zm76.49-72.08-.38-.07,1.22.24Zm53.89-38.72c-6.17-16.66-16.37-30.79-28.17-43.74a252.85,252.85,0,0,0-83.44-60.35c-16.76-7.58-34.22-12.78-49.8-13.41-19.83,0-29.84,4.43-38.43,16.31q-24.23,33.54-48.19,67.27c-4.61,6.48-6.07,13.87-5.31,21.77,1.46,15.06,8.21,28,16.64,40.05.56.81,1.13,1.61,1.71,2.41,1.35,1.89,2.73,3.74,4.15,5.57.5.65,1,1.3,1.51,1.94,1.69,2.15,3.43,4.25,5.2,6.31.63.74,1.26,1.47,1.91,2.2.35.41.71.81,1.08,1.21s.87,1,1.32,1.47c.6.65,1.19,1.3,1.79,1.94,1.07,1.14,2.14,2.26,3.23,3.37l1.71,1.73c.58.58.06.09.63.65.61,0,.93,0,.93,0s9.33,1.13,19.84-9.42,37.05-39.07,54.13-20.29,27.2,40.73,47,39.29c11-.8,20.78-9.19,27.36-16.53l-.72-.18c-1.91-.5-5-1.84-3.91-4s4-.33,4-.33c.4.17.79.3,1.16.42l.54.15.5.12c.32.07.61.12.86.16l.28,0,.35,0a80.19,80.19,0,0,0,5.48-7.6c-1.86-.39-3.72-.83-5.56-1.3-.93-.24-1.86-.49-2.78-.76s-2-.58-3-.89-2-.62-3-1-2-.67-3-1q-3-1-5.87-2.24l-2.28-.94c-1.19-.5-2.36-1-3.54-1.53a243.23,243.23,0,0,1-78.8-55.73c-12.58-13.35-23.67-27.87-29.42-45.7a43.18,43.18,0,0,1-2-8.71c-1.7-16.83,7.61-26.45,26.48-27.06,13.66-.45,26.76,2.92,39.4,7.6,36.7,13.6,67.77,35.63,94.09,64.38.66.72,1.31,1.43,1.95,2.16.52.58,1,1.17,1.55,1.76.37.43.74.85,1.1,1.28s.77.9,1.14,1.36c.56.66,1.11,1.33,1.65,2l1.41,1.79c.32.4.63.81.94,1.22s.78,1,1.16,1.56l.31.42c.48.65.95,1.3,1.4,2l.22.32c.44.63.88,1.26,1.3,1.91q1.47,2.2,2.82,4.48c.16.27.33.55.48.83s.2.33.29.5c.29.51.58,1,.86,1.54l.26.47c.35.65.69,1.3,1,2s.63,1.27.94,1.91l.33.7c.32.71.64,1.42.95,2.14s.58,1.38.86,2.08c.05.12.1.23.14.35,1.07.06,10.33.59,11.31,3C304,182.49,303.87,182,303.7,181.57ZM99,102.88a2.27,2.27,0,0,1,2.17-2.1,2.11,2.11,0,0,1,1.76,2.79,4.47,4.47,0,0,0,.07,2.28c.11.38.29,2.87-1.83,2.87C98.46,108.72,98.89,104.88,99,102.88Zm20.73,40.6c12.15,18.19,35.9,37.84,36.79,38.66s3.51,2,2,3.94-4.56-.5-4.56-.5c-6.44-4.67-24.83-23.05-36.73-38.23S101.1,117,100.53,114.44a8,8,0,0,1-.14-1,2.07,2.07,0,0,1,2.88-2.15,2.36,2.36,0,0,1,1.18,1.63C105.09,115.88,107.61,125.28,119.76,143.48Z"/>
<path class="cls-1" d="M246.09,230.16c-.63,2.35-4.37,1.48-6.27,1s-5-1.84-3.91-4,4-.33,4-.33a13,13,0,0,0,3.91.95C244.55,227.76,246.6,228.25,246.09,230.16Z"/>
<path class="cls-1" d="M276.05,233.56c-1.38.18-12.9,1.8-23.57-.17,0,0-2.84,0-2.84-2.5,0,0,.22-2.36,4.14-1.36s16.31,1.41,21.63.16c0,0,2.19-.72,2.89,1S277.43,233.39,276.05,233.56Z"/>
<path class="cls-1" d="M101.2,108.72c-2.74,0-2.31-3.84-2.17-5.84a2.27,2.27,0,0,1,2.17-2.1,2.11,2.11,0,0,1,1.76,2.79,4.47,4.47,0,0,0,.07,2.28C103.14,106.23,103.32,108.72,101.2,108.72Z"/>
<path class="cls-1" d="M158.55,186.08c-1.51,2-4.56-.5-4.56-.5-6.44-4.67-24.83-23.05-36.73-38.23S101.1,117,100.53,114.44a8,8,0,0,1-.14-1,2.07,2.07,0,0,1,2.88-2.15,2.36,2.36,0,0,1,1.18,1.63c.64,2.92,3.16,12.32,15.31,30.52s35.9,37.84,36.79,38.66S160.06,184.11,158.55,186.08Z"/>
<path class="cls-1" d="M294.38,184.17q-.69-2.13-1.5-4.2c0-.12-.09-.23-.14-.35-.28-.7-.56-1.4-.86-2.08s-.63-1.43-.95-2.14l-.33-.7c-.31-.64-.62-1.28-.94-1.91s-.67-1.31-1-2l-.26-.47c-.28-.52-.57-1-.86-1.54-.09-.17-.19-.34-.29-.5l-.48-.83q-1.35-2.28-2.82-4.48c-.42-.64-.85-1.28-1.3-1.91l-.22-.32c-.46-.66-.92-1.31-1.4-2l-.31-.42c-.38-.53-.77-1-1.16-1.56s-.62-.82-.94-1.22l-1.41-1.79c-.55-.67-1.09-1.34-1.65-2-.37-.46-.76-.91-1.14-1.36s-.73-.85-1.1-1.28c-.51-.59-1-1.18-1.55-1.76q-1-1.1-1.95-2.16c-26.32-28.75-57.39-50.78-94.09-64.38-12.64-4.68-25.74-8-39.4-7.6-18.87.61-28.18,10.23-26.48,27.06a43.18,43.18,0,0,0,2,8.71c5.75,17.83,16.84,32.35,29.42,45.7a243.23,243.23,0,0,0,78.8,55.73c1.18.52,2.35,1,3.54,1.53l2.28.94c1.95.79,3.9,1.53,5.87,2.24,1,.35,2,.7,3,1s2,.65,3,1,2,.61,3,.89,1.85.52,2.78.76c1.84.47,3.7.91,5.56,1.3l.11,0,.13,0,.14,0,.38.07.13,0,.71.14.33.06a102.69,102.69,0,0,0,17.61,1.84c1.83,0,3.66.12,5.48,0,13.26-1,22.16-7.12,23.12-17.78C297.8,197.87,296.48,190.63,294.38,184.17ZM247,182.45a11.09,11.09,0,0,1-1,.65,5.7,5.7,0,0,1-.54.3,12.23,12.23,0,0,1-1.15.55l-.62.24c-.42.16-.86.3-1.31.43l-.71.19c-.47.12-1,.22-1.48.31l-.78.13-.81.11-1.27.13q-.65.06-1.32.09l-.15,0A70.87,70.87,0,0,1,220.6,182c-23.73-8.6-43.49-22.66-57.57-43.88-2.77-4.18-4.52-9.23-5.9-14.11-.08-.3-.16-.59-.21-.89s-.07-.36-.1-.54a10.55,10.55,0,0,1-.12-1.56c0-.17,0-.33,0-.49a10,10,0,0,1,.16-1.35,3.12,3.12,0,0,1,.09-.44l.12-.42a2.58,2.58,0,0,1,.13-.42c0-.14.1-.27.16-.4s.11-.27.17-.4a2.4,2.4,0,0,1,.2-.39c.06-.13.14-.25.21-.38a3.17,3.17,0,0,1,.23-.36c.08-.12.16-.24.25-.35a3,3,0,0,1,.27-.35c.09-.11.19-.22.29-.32a1.79,1.79,0,0,1,.3-.32c.16-.16.33-.32.51-.47s.36-.29.55-.43.38-.27.59-.4l.63-.36.67-.32c.22-.1.46-.19.7-.28s.49-.17.74-.24l.52-.14.26-.06c.66-.15,1.32-.27,2-.38s1.35-.19,2-.25a35.2,35.2,0,0,1,4.11-.12l1.16.07q.66,0,1.32.12c.39,0,.78.1,1.17.16.65.1,1.31.22,1.95.34l1,.19,1.23.26c.19,0,.39.08.57.12,1.43.32,2.84.67,4.23,1.07l.16,0c.53.15,1.06.3,1.58.47.33.09.66.19,1,.3,1.75.55,3.48,1.16,5.17,1.83.5.2,1,.39,1.47.6s.88.36,1.32.55c.62.26,1.23.54,1.85.82l1.75.83,1.32.66c.51.25,1,.51,1.52.78,1,.51,1.92,1,2.87,1.58,1.53.87,3,1.78,4.52,2.73,1,.61,1.91,1.24,2.86,1.89h0c.94.64,1.88,1.3,2.81,2s1.62,1.2,2.43,1.81q3.61,2.76,7.08,5.77l1.18,1,1.17,1.07,1.53,1.45c1,1,2.09,2.06,3.1,3.12l1,1c.91,1,1.8,1.94,2.67,2.92.53.59,1.06,1.18,1.58,1.78l1.56,1.8c.52.6,1,1.21,1.54,1.81l2,2.43.29.36c.17.19.31.38.45.57s.38.49.55.74.47.66.69,1,.52.79.75,1.2c.14.22.28.45.4.68s.21.35.31.53.31.56.46.85c.38.72.75,1.44,1.09,2.18.57,1.17,1.08,2.36,1.57,3.55C251.92,173.82,251.21,179.29,247,182.45Z"/>
<path class="cls-5" d="M247,182.45a11.09,11.09,0,0,1-1,.65,5.7,5.7,0,0,1-.54.3,12.23,12.23,0,0,1-1.15.55l-.62.24c-.42.16-.86.3-1.31.43l-.71.19c-.47.11-1,.22-1.48.31l-.78.13-.81.11-1.27.13-1.32.08h-.15A70.87,70.87,0,0,1,220.6,182c-23.73-8.6-43.49-22.66-57.57-43.88-2.77-4.18-4.52-9.23-5.9-14.11-.08-.3-.16-.59-.21-.89s-.07-.36-.1-.54a10.55,10.55,0,0,1-.12-1.56c0-.17,0-.33,0-.49a10,10,0,0,1,.16-1.35,3.12,3.12,0,0,1,.09-.44l.12-.42a2.58,2.58,0,0,1,.13-.42c0-.14.1-.27.16-.4s.11-.27.17-.4a2.4,2.4,0,0,1,.2-.39c.06-.13.14-.25.21-.38a3.17,3.17,0,0,1,.23-.36c.08-.12.16-.24.25-.35a3,3,0,0,1,.27-.35c.09-.11.19-.22.29-.32a1.79,1.79,0,0,1,.3-.32c.16-.16.33-.32.51-.47s.36-.29.55-.43.38-.27.59-.4l.63-.36.67-.32c.22-.1.46-.19.7-.28s.49-.17.74-.24l.52-.14.26-.06c.66-.15,1.32-.27,2-.38s1.35-.19,2-.25a35.2,35.2,0,0,1,4.11-.12l1.16.07q.66,0,1.32.12c.39,0,.78.1,1.17.16.65.1,1.31.22,1.95.34l1,.19,1.23.26c.19,0,.39.08.57.12,1.43.32,2.84.67,4.23,1.07l.16,0c.53.15,1.06.3,1.58.47.33.09.66.19,1,.3,1.75.55,3.48,1.16,5.17,1.83.5.2,1,.39,1.47.6s.88.36,1.32.55c.62.26,1.23.54,1.85.82l1.75.83,1.32.66c.51.25,1,.51,1.52.78,1,.51,1.92,1,2.87,1.58,1.53.87,3,1.78,4.52,2.73,1,.61,1.91,1.24,2.86,1.89h0c.94.64,1.88,1.3,2.81,2s1.62,1.2,2.43,1.81q3.61,2.76,7.08,5.77l1.18,1,1.17,1.07,1.53,1.45c1,1,2.09,2.06,3.1,3.12l1,1c.91,1,1.8,1.94,2.67,2.92.53.59,1.06,1.18,1.58,1.78l1.56,1.8c.52.6,1,1.21,1.54,1.81l2,2.43.29.36c.17.19.31.38.45.57s.37.49.55.74.47.66.69,1,.52.79.75,1.2c.14.22.28.45.4.68s.21.35.31.53.31.56.46.85c.38.72.75,1.44,1.09,2.18.57,1.17,1.08,2.36,1.57,3.55C251.92,173.82,251.21,179.29,247,182.45Z"/>
<path d="M156.87,119.18a10,10,0,0,0-.16,1.35A6.73,6.73,0,0,1,156.87,119.18Z"/>
<path d="M166.45,111.55c-.67.11-1.33.23-2,.38l-.26.06.26-.07C165.12,111.78,165.78,111.65,166.45,111.55Z"/>
<path d="M226.1,137.19l-1.18-1q-3.46-3-7.08-5.77c2.41,1.83,4.77,3.76,7.08,5.76C225.32,136.49,225.71,136.84,226.1,137.19Z"/>
<path d="M235.87,185.56A55.12,55.12,0,0,1,220.6,182,70.87,70.87,0,0,0,235.87,185.56Z"/><path d="M242.53,154.94l-.29-.36-2-2.43q1,1.22,2,2.43A4.39,4.39,0,0,1,242.53,154.94Z"/>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 9.7 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 7.8 KiB

View File

@@ -1,19 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 363 363.93">
<defs>
<style>.cls-1{fill:#fff;}.cls-2,.cls-3{fill:#ff007a;}.cls-3{fill-rule:evenodd;}</style>
</defs>
<g id="Layer_2" data-name="Layer 2">
<g id="US">
<rect class="cls-1" width="363" height="363.93" />
<path class="cls-2" d="M133.66,88.51c-2.58-.4-2.69-.45-1.48-.64,2.34-.36,7.84.13,11.64,1,8.85,2.11,16.91,7.51,25.51,17.12l2.29,2.55,3.27-.53c13.77-2.22,27.78-.46,39.5,5,3.23,1.5,8.31,4.47,9,5.23a14.12,14.12,0,0,1,.82,3.49c.87,5.78.44,10.21-1.33,13.53-1,1.8-1,2.37-.37,3.91a3.79,3.79,0,0,0,3.38,2.14c2.91,0,6-4.72,7.5-11.29l.58-2.61,1.14,1.3c6.27,7.12,11.19,16.83,12,23.75l.22,1.8-1-1.64a20.33,20.33,0,0,0-6-6.29c-4.21-2.79-8.66-3.74-20.44-4.36-10.64-.56-16.66-1.47-22.63-3.42-10.15-3.33-15.27-7.75-27.34-23.62-5.36-7-8.67-11-12-14.09C150.44,93.72,143.08,90,133.66,88.51Z"/>
<path class="cls-2" d="M225.74,104.25c.26-4.73.9-7.84,2.19-10.69a11.42,11.42,0,0,1,1.06-2,7.31,7.31,0,0,1-.49,1.85c-.94,2.76-1.09,6.54-.45,10.93.82,5.58,1.28,6.38,7.16,12.41,2.76,2.82,6,6.39,7.13,7.92l2.11,2.78-2.11-2a90.34,90.34,0,0,0-9.85-7.85c-.88-.45-1-.45-1.55.09s-.61,1.25-.68,4.8c-.1,5.53-.86,9.08-2.67,12.63-1,1.92-1.14,1.51-.25-.66.66-1.61.73-2.33.73-7.68,0-10.75-1.29-13.34-8.76-17.77-1.89-1.12-5-2.74-6.92-3.59a28.13,28.13,0,0,1-3.39-1.66c.21-.21,7.5,1.92,10.43,3,4.36,1.68,5.08,1.9,5.61,1.7C225.39,108.33,225.56,107.29,225.74,104.25Z"/>
<path class="cls-2" d="M138.68,122.64c-5.24-7.24-8.49-18.35-7.79-26.65l.22-2.57,1.19.22a29.77,29.77,0,0,1,7.93,3c5,3,7.12,7,9.31,17.24.64,3,1.48,6.39,1.87,7.54a51.22,51.22,0,0,0,4.89,9c1.38,2,.46,3-2.58,2.7C149.06,132.62,142.75,128.26,138.68,122.64Z"/>
<path class="cls-2" d="M219.37,176.56c-24.53-9.9-33.17-18.49-33.17-33a22.78,22.78,0,0,1,.17-3.88,18.57,18.57,0,0,1,2.1,1.56c5,4,10.55,5.71,26,8,9.07,1.33,14.18,2.4,18.89,4,15,5,24.23,15.08,26.44,28.84a47.74,47.74,0,0,1-.78,15.44c-.82,3.12-3.32,8.75-4,9-.18.06-.36-.64-.41-1.61a20,20,0,0,0-7.23-14C242.39,186.56,235.7,183.15,219.37,176.56Z"/>
<path class="cls-2" d="M202.15,180.67a45.44,45.44,0,0,0-1.18-5.2l-.63-1.87,1.16,1.31a21,21,0,0,1,4,7.21c.82,2.36.92,3.06.91,6.88s-.11,4.55-.87,6.67a21,21,0,0,1-5.18,8.25c-4.48,4.57-10.24,7.11-18.55,8.16-1.44.18-5.66.49-9.36.68-9.33.48-15.47,1.49-21,3.43a4.46,4.46,0,0,1-1.57.38c-.23-.22,3.53-2.47,6.64-4,4.37-2.11,8.73-3.26,18.49-4.89,4.82-.81,9.8-1.78,11.06-2.17C198,201.87,204.11,192.4,202.15,180.67Z"/>
<path class="cls-2" d="M213.39,200.69c-3.25-7-4-13.8-2.22-20.13.19-.67.5-1.22.69-1.22a6.48,6.48,0,0,1,1.7.92c1.51,1,4.52,2.72,12.54,7.11,10,5.47,15.72,9.71,19.61,14.55a31.65,31.65,0,0,1,6.51,15,50.93,50.93,0,0,1-.61,14.72c-2.7,10.6-9,18.91-17.88,23.77a25.08,25.08,0,0,1-2.6,1.3,9.37,9.37,0,0,1,1.05-2.7,24.32,24.32,0,0,0,1.08-19.23c-1.39-4.17-4.23-9.26-9.95-17.86C216.66,206.88,215,204.22,213.39,200.69Z"/>
<path class="cls-2" d="M121.27,238.59c9.1-7.71,20.42-13.18,30.74-14.86a47.87,47.87,0,0,1,16,.62c6.6,1.69,12.5,5.49,15.57,10s4.29,8.27,5.63,16.83c.53,3.38,1.1,6.77,1.28,7.54,1,4.43,3,8,5.39,9.76,3.85,2.83,10.49,3,17,.45a8.38,8.38,0,0,1,2.14-.66c.24.23-3,2.44-5.37,3.6a18.13,18.13,0,0,1-8.9,2.17c-6,0-11-3.05-15.09-9.27a71.72,71.72,0,0,1-4.07-8.15c-4.38-10-6.54-13.05-11.62-16.38-4.42-2.9-10.12-3.42-14.41-1.31a9.37,9.37,0,0,0-3.17,14.55,12.49,12.49,0,0,0,7,3.69,7.51,7.51,0,0,0,8.51-7.54c0-3-1.16-4.73-4.06-6-4-1.79-8.22.3-8.2,4a3.34,3.34,0,0,0,2.29,3.31c1,.46,1,.5.21.33-3.63-.76-4.48-5.15-1.56-8.07,3.51-3.5,10.77-2,13.26,2.82a12.36,12.36,0,0,1,.26,8.42c-2,5.41-8,8.25-14,6.7-4.11-1.05-5.79-2.19-10.75-7.31-8.62-8.9-12-10.63-24.39-12.57l-2.38-.38Z"/>
<path class="cls-3" d="M56.26,39.17c28.79,35,73.17,89.56,75.38,92.62,1.82,2.52,1.13,4.79-2,6.56a18.77,18.77,0,0,1-7.08,2,9,9,0,0,1-5.94-2.55c-1.17-1.11-5.9-8.18-16.8-25.14-8.34-13-15.32-23.74-15.51-23.92-.44-.42-.43-.4,14.66,26.67,9.48,17,12.68,23,12.68,23.82,0,1.64-.44,2.5-2.46,4.75-3.35,3.75-4.85,8-5.93,16.69-1.22,9.78-4.63,16.68-14.08,28.51-5.54,6.92-6.44,8.19-7.84,11a20.31,20.31,0,0,0-2.44,9.91,30.85,30.85,0,0,0,1.63,12.2c1.25,3.93,2.55,6.52,5.89,11.71,2.88,4.47,4.54,7.79,4.54,9.1,0,1,.2,1,4.66,0,10.69-2.42,19.36-6.67,24.24-11.89,3-3.22,3.73-5,3.76-9.43,0-2.89-.09-3.5-.87-5.16-1.27-2.71-3.58-5-8.68-8.45-6.67-4.58-9.52-8.26-10.31-13.32-.64-4.16.11-7.09,3.8-14.85,3.82-8,4.77-11.45,5.41-19.55.41-5.23,1-7.29,2.48-8.94s3-2.31,6.84-2.84c6.31-.87,10.32-2.5,13.62-5.55a11.86,11.86,0,0,0,4.25-9l.14-2.9-1.6-1.87C132.92,122.52,52.38,33.84,52,33.84,52,33.84,53.86,36.24,56.26,39.17Zm38,176.2a5.16,5.16,0,0,0-1.58-6.78c-2.07-1.38-5.29-.73-5.29,1.07,0,.55.3,1,1,1.3,1.14.59,1.23,1.26.32,2.62s-.84,2.59.21,3.42C90.57,218.33,93,217.6,94.23,215.37Z"/>
<path class="cls-3" d="M144.29,150.18a11.14,11.14,0,0,0-6.73,7.36c-.54,2-.23,5.54.58,6.63,1.31,1.76,2.59,2.22,6,2.2,6.74-.05,12.6-3,13.29-6.57.55-3-2-7.09-5.57-8.9A13.16,13.16,0,0,0,144.29,150.18Zm7.88,6.18c1-1.48.59-3.08-1.18-4.16-3.37-2.07-8.46-.36-8.46,2.83,0,1.59,2.65,3.32,5.09,3.32A6.55,6.55,0,0,0,152.17,156.36Z"/>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 4.9 KiB

View File

@@ -1,20 +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" fill="#F9F9F9"/>
<path d="M21.94 7.92004C20.4077 6.42277 18.3116 5.5 16 5.5C11.3056 5.5 7.5 9.30559 7.5 14C7.5 18.6944 11.3056 22.5 16 22.5C18.3116 22.5 20.4077 21.5772 21.94 20.08C20.1123 22.4633 17.2356 24 14 24C8.47715 24 4 19.5229 4 14C4 8.47715 8.47715 4 14 4C17.2356 4 20.1123 5.53668 21.94 7.92004Z" fill="url(#paint0_linear_6993_17582)"/>
<path d="M9.64795 18.864C10.8738 20.0618 12.5507 20.8 14.4 20.8C18.1555 20.8 21.2 17.7555 21.2 14C21.2 10.2445 18.1555 7.2 14.4 7.2C12.5507 7.2 10.8738 7.9382 9.64795 9.13601C11.1102 7.22934 13.4115 6 16 6C20.4183 6 24 9.58172 24 14C24 18.4183 20.4183 22 16 22C13.4115 22 11.1102 20.7707 9.64795 18.864Z" fill="url(#paint1_linear_6993_17582)"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M20 14C20 17.3137 17.3137 20 14 20C10.6863 20 8 17.3137 8 14C8 10.6863 10.6863 8 14 8C17.3137 8 20 10.6863 20 14ZM18 14C18 16.2091 16.2091 18 14 18C11.7909 18 10 16.2091 10 14C10 11.7909 11.7909 10 14 10C16.2091 10 18 11.7909 18 14Z" fill="url(#paint2_linear_6993_17582)"/>
<defs>
<linearGradient id="paint0_linear_6993_17582" x1="4" y1="13.6552" x2="24" y2="13.6552" gradientUnits="userSpaceOnUse">
<stop stop-color="#00E0FF"/>
<stop offset="1" stop-color="#562EC8"/>
</linearGradient>
<linearGradient id="paint1_linear_6993_17582" x1="3.99998" y1="13.6552" x2="24" y2="13.6552" gradientUnits="userSpaceOnUse">
<stop stop-color="#00E0FF"/>
<stop offset="1" stop-color="#562EC8"/>
</linearGradient>
<linearGradient id="paint2_linear_6993_17582" x1="4" y1="13.6552" x2="24" y2="13.6552" gradientUnits="userSpaceOnUse">
<stop stop-color="#00E0FF"/>
<stop offset="1" stop-color="#562EC8"/>
</linearGradient>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 1.7 KiB

View File

@@ -0,0 +1,20 @@
/* eslint-env node */
const fs = require('fs')
const path = require('path')
const Ajv = require('ajv')
const standaloneCode = require('ajv/dist/standalone').default
const addFormats = require('ajv-formats')
const schema = require('@uniswap/token-lists/dist/tokenlist.schema.json')
const tokenListAjv = new Ajv({ code: { source: true, esm: true } })
addFormats(tokenListAjv)
const validateTokenList = tokenListAjv.compile(schema)
let tokenListModuleCode = standaloneCode(tokenListAjv, validateTokenList)
fs.writeFileSync(path.join(__dirname, '../src/utils/__generated__/validateTokenList.js'), tokenListModuleCode)
const tokensAjv = new Ajv({ code: { source: true, esm: true } })
addFormats(tokensAjv)
const validateTokens = tokensAjv.compile({ ...schema, required: ['tokens'] })
let tokensModuleCode = standaloneCode(tokensAjv, validateTokens)
fs.writeFileSync(path.join(__dirname, '../src/utils/__generated__/validateTokens.js'), tokensModuleCode)

View File

@@ -4,8 +4,8 @@ require('dotenv').config({ path: '.env.production' })
const child_process = require('child_process')
const fs = require('fs/promises')
const { promisify } = require('util')
const dataConfig = require('../graphql.config')
const thegraphConfig = require('../graphql_thegraph.config')
const dataConfig = require('../graphql.data.config')
const thegraphConfig = require('../graphql.thegraph.config')
const exec = promisify(child_process.exec)

View File

@@ -1,10 +0,0 @@
/* eslint-env node */
const { exec } = require('child_process')
const isWindows = process.platform === 'win32' || /^(msys|cygwin)$/.test(process.env.OSTYPE)
if (isWindows) {
exec(`type nul > src/locales/en-US.po`)
} else {
exec(`touch src/locales/en-US.po`)
}

View File

@@ -1,56 +0,0 @@
/* eslint-disable no-undef */
const assert = require('assert')
const chalk = require('chalk')
const fs = require('fs')
const gzipSize = require('gzip-size').sync
const path = require('path')
const buildDir = path.join(__dirname, '../build')
let entrypoints
try {
entrypoints = require(path.join(buildDir, 'asset-manifest.json')).entrypoints
} catch (e) {
console.log(chalk.yellow('You must build first: `yarn build`'))
process.exit(1)
}
// The last recorded size for these assets, as reported by `yarn build`.
const LAST_SIZE_MAIN_KB = 420
// This is the async-loaded js, called <number>.<hash>.js, with a matching css file.
const LAST_SIZE_ENTRY_KB = 1442
const SIZE_TOLERANCE_KB = 10
const jsEntrypoints = entrypoints.filter((entrypoint) => entrypoint.endsWith('js'))
assert(jsEntrypoints.length === 3)
let fail = false
console.log('File sizes after gzip:\n')
jsEntrypoints.forEach((entrypoint) => {
const name = entrypoint.match(/\/([\w\d-]*)\./)[1]
const size = gzipSize(fs.readFileSync(path.join(buildDir, entrypoint))) / 1024
let maxSize = LAST_SIZE_ENTRY_KB + SIZE_TOLERANCE_KB
if (name === 'runtime-main') {
return
} else if (name === 'main') {
maxSize = LAST_SIZE_MAIN_KB + SIZE_TOLERANCE_KB
}
const report = `\t${size.toFixed(2).padEnd(8)}kB\t${chalk.dim(
`max: ${maxSize.toFixed().padEnd(4)} kB`
)}\t${entrypoint}`
if (maxSize > size) {
console.log(chalk.green(report))
} else {
console.log(chalk.red(report), '\tdid you import an unnecessary dependency?')
fail = true
}
})
if (fail) {
console.log(chalk.yellow('\nOne or more of your files has grown too large.'))
console.log(chalk.yellow('Reduce the file size or update the size limit (in scripts/test-size.js)'))
process.exit(1)
}

View File

@@ -10,14 +10,14 @@ export enum CardType {
Secondary = 'Secondary',
}
const StyledCard = styled.div<{ isDarkMode: boolean; backgroundImgSrc?: string; type: CardType }>`
const StyledCard = styled.div<{ $isDarkMode: boolean; $backgroundImgSrc?: string; $type: CardType }>`
display: flex;
background: ${({ isDarkMode, backgroundImgSrc, type, theme }) =>
isDarkMode
? `${type === CardType.Primary ? theme.backgroundModule : theme.backgroundSurface} ${
backgroundImgSrc ? ` url(${backgroundImgSrc})` : ''
background: ${({ $isDarkMode, $backgroundImgSrc, $type, theme }) =>
$isDarkMode
? `${$type === CardType.Primary ? theme.backgroundModule : theme.backgroundSurface} ${
$backgroundImgSrc ? ` url(${$backgroundImgSrc})` : ''
}`
: `${type === CardType.Primary ? 'white' : theme.backgroundModule} url(${backgroundImgSrc})`};
: `${$type === CardType.Primary ? 'white' : theme.backgroundModule} url(${$backgroundImgSrc})`};
background-size: auto 100%;
background-position: right;
background-repeat: no-repeat;
@@ -30,15 +30,15 @@ const StyledCard = styled.div<{ isDarkMode: boolean; backgroundImgSrc?: string;
padding: 24px;
height: 212px;
border-radius: 24px;
border: 1px solid ${({ theme, type }) => (type === CardType.Primary ? 'transparent' : theme.backgroundOutline)};
border: 1px solid ${({ theme, $type }) => ($type === CardType.Primary ? 'transparent' : theme.backgroundOutline)};
box-shadow: 0px 10px 24px 0px rgba(51, 53, 72, 0.04);
transition: ${({ theme }) => `${theme.transition.duration.medium} ${theme.transition.timing.ease} border`};
&:hover {
border: 1px solid ${({ theme, isDarkMode }) => (isDarkMode ? theme.backgroundInteractive : theme.textTertiary)};
border: 1px solid ${({ theme, $isDarkMode }) => ($isDarkMode ? theme.backgroundInteractive : theme.textTertiary)};
}
@media screen and (min-width: ${BREAKPOINTS.sm}px) {
height: ${({ backgroundImgSrc }) => (backgroundImgSrc ? 360 : 260)}px;
height: ${({ $backgroundImgSrc }) => ($backgroundImgSrc ? 360 : 260)}px;
}
@media screen and (min-width: ${BREAKPOINTS.xl}px) {
padding: 32px;
@@ -125,14 +125,14 @@ const Card = ({
return (
<TraceEvent events={[BrowserEvent.onClick]} name={SharedEventName.ELEMENT_CLICKED} element={elementName}>
<StyledCard
type={type}
as={external ? 'a' : Link}
to={external ? undefined : to}
href={external ? to : undefined}
target={external ? '_blank' : undefined}
rel={external ? 'noopenener noreferrer' : undefined}
isDarkMode={isDarkMode}
backgroundImgSrc={backgroundImgSrc}
$backgroundImgSrc={backgroundImgSrc}
$isDarkMode={isDarkMode}
$type={type}
>
<TitleRow>
<CardTitle>{title}</CardTitle>

View File

@@ -114,7 +114,7 @@ const AccountNamesWrapper = styled.div`
width: 100%;
flex-direction: column;
justify-content: center;
gap: 4px;
gap: 2px;
`
const StyledInfoIcon = styled(Info)`
@@ -240,7 +240,7 @@ export default function AuthenticatedHeader({ account, openSettings }: { account
<StatusIcon connection={connection} size={40} />
{account && (
<AccountNamesWrapper>
<ThemedText.SubHeader color="textPrimary" fontWeight={500}>
<ThemedText.SubHeader>
<CopyText toCopy={ENSName ?? account}>{ENSName ?? shortenAddress(account, 4, 4)}</CopyText>
</ThemedText.SubHeader>
{/* Displays smaller view of account if ENS name was rendered above */}
@@ -266,7 +266,7 @@ export default function AuthenticatedHeader({ account, openSettings }: { account
<PortfolioDrawerContainer>
{totalBalance !== undefined ? (
<FadeInColumn gap="xs">
<ThemedText.HeadlineLarge fontWeight={500}>
<ThemedText.HeadlineLarge fontWeight={500} data-testid="portfolio-total-balance">
{formatNumber(totalBalance, NumberType.PortfolioBalance)}
</ThemedText.HeadlineLarge>
<AutoRow marginBottom="20px">

View File

@@ -48,7 +48,7 @@ export function ActivityRow({
<PortfolioLogo chainId={chainId} currencies={currencies} images={logos} accountAddress={otherAccount} />
</Column>
}
title={<ThemedText.SubHeader fontWeight={500}>{title}</ThemedText.SubHeader>}
title={<ThemedText.SubHeader>{title}</ThemedText.SubHeader>}
descriptor={
<ActivityRowDescriptor color="textSecondary">
{descriptor}

View File

@@ -82,15 +82,38 @@ const ActivityGroupWrapper = styled(Column)`
gap: 8px;
`
function combineActivities(localMap: ActivityMap = {}, remoteMap: ActivityMap = {}): Array<Activity> {
/* Detects transactions from same account with the same nonce and different hash */
function wasTxCancelled(localActivity: Activity, remoteMap: ActivityMap, account: string): boolean {
// handles locally cached tx's that were stored before we started tracking nonces
if (!localActivity.nonce || localActivity.status !== TransactionStatus.Pending) return false
return Object.values(remoteMap).some((remoteTx) => {
if (!remoteTx) return false
// Cancellations are only possible when both nonce and tx.from are the same
if (remoteTx.nonce === localActivity.nonce && remoteTx.receipt?.from.toLowerCase() === account.toLowerCase()) {
// If the remote tx has a different hash than the local tx, the local tx was cancelled
return remoteTx.hash.toLowerCase() !== localActivity.hash.toLowerCase()
}
return false
})
}
function combineActivities(localMap: ActivityMap = {}, remoteMap: ActivityMap = {}, account: string): Array<Activity> {
const txHashes = [...new Set([...Object.keys(localMap), ...Object.keys(remoteMap)])]
// Merges local and remote activities w/ same hash, preferring remote data
return txHashes.reduce((acc: Array<Activity>, hash) => {
const localActivity = localMap?.[hash] ?? {}
const remoteActivity = remoteMap?.[hash] ?? {}
// TODO(cartcrom): determine best logic for which fields to prefer from which sources, i.e. prefer remote exact swap output instead of local estimated output
acc.push({ ...remoteActivity, ...localActivity } as Activity)
const localActivity = (localMap?.[hash] ?? {}) as Activity
const remoteActivity = (remoteMap?.[hash] ?? {}) as Activity
// TODO(WEB-2064): Display cancelled status in UI rather than completely hiding cancelled TXs
if (wasTxCancelled(localActivity, remoteMap, account)) return acc
// TODO(cartcrom): determine best logic for which fields to prefer from which sources
// i.e.prefer remote exact swap output instead of local estimated output
acc.push({ ...localActivity, ...remoteActivity } as Activity)
return acc
}, [])
}
@@ -122,9 +145,9 @@ export function ActivityTab({ account }: { account: string }) {
const activityGroups = useMemo(() => {
const remoteMap = parseRemoteActivities(data?.portfolios?.[0].assetActivities)
const allActivities = combineActivities(localMap, remoteMap)
const allActivities = combineActivities(localMap, remoteMap, account)
return createGroups(allActivities)
}, [data?.portfolios, localMap])
}, [data?.portfolios, localMap, account])
if (!data && loading)
return (
@@ -140,7 +163,7 @@ export function ActivityTab({ account }: { account: string }) {
<PortfolioTabWrapper>
{activityGroups.map((activityGroup) => (
<ActivityGroupWrapper key={activityGroup.title}>
<ThemedText.SubHeader color="textSecondary" fontWeight={500} marginLeft="16px">
<ThemedText.SubHeader color="textSecondary" marginLeft="16px">
{activityGroup.title}
</ThemedText.SubHeader>
<Column>

View File

@@ -2,7 +2,7 @@ import { SupportedChainId, Token, TradeType as MockTradeType } from '@uniswap/sd
import { PERMIT2_ADDRESS } from '@uniswap/universal-router-sdk'
import { DAI as MockDAI, nativeOnChain, USDC_MAINNET as MockUSDC_MAINNET } from 'constants/tokens'
import { TransactionStatus as MockTxStatus } from 'graphql/data/__generated__/types-and-hooks'
import { TokenAddressMap } from 'state/lists/hooks'
import { ChainTokenMap } from 'hooks/Tokens'
import {
ExactInputSwapTransactionInfo,
ExactOutputSwapTransactionInfo,
@@ -89,15 +89,15 @@ function mockMultiStatus(info: TransactionInfo, id: string): [TransactionDetails
]
}
const mockTokenAddressMap: TokenAddressMap = {
const mockTokenAddressMap: ChainTokenMap = {
[mockChainId]: {
[MockDAI.address]: { token: MockDAI },
[MockUSDC_MAINNET.address]: { token: MockUSDC_MAINNET },
} as TokenAddressMap[number],
[MockDAI.address]: MockDAI,
[MockUSDC_MAINNET.address]: MockUSDC_MAINNET,
},
}
jest.mock('../../../../state/lists/hooks', () => ({
useCombinedActiveList: () => mockTokenAddressMap,
jest.mock('../../../../hooks/Tokens', () => ({
useAllTokensMultichain: () => mockTokenAddressMap,
}))
jest.mock('../../../../state/transactions/hooks', () => {
@@ -300,7 +300,7 @@ describe('parseLocalActivity', () => {
},
} as TransactionDetails
const chainId = SupportedChainId.MAINNET
const tokens = {} as TokenAddressMap
const tokens = {} as ChainTokenMap
expect(parseLocalActivity(details, chainId, tokens)).toMatchObject({
chainId: 1,
currencies: [undefined, undefined],

View File

@@ -3,9 +3,9 @@ import { formatCurrencyAmount } from '@uniswap/conedison/format'
import { Currency, CurrencyAmount, TradeType } from '@uniswap/sdk-core'
import { nativeOnChain } from '@uniswap/smart-order-router'
import { SupportedChainId } from 'constants/chains'
import { TransactionPartsFragment, TransactionStatus } from 'graphql/data/__generated__/types-and-hooks'
import { TransactionStatus } from 'graphql/data/__generated__/types-and-hooks'
import { ChainTokenMap, useAllTokensMultichain } from 'hooks/Tokens'
import { useMemo } from 'react'
import { TokenAddressMap, useCombinedActiveList } from 'state/lists/hooks'
import { useMultichainTransactions } from 'state/transactions/hooks'
import {
AddLiquidityV2PoolTransactionInfo,
@@ -25,8 +25,8 @@ import {
import { getActivityTitle } from '../constants'
import { Activity, ActivityMap } from './types'
function getCurrency(currencyId: string, chainId: SupportedChainId, tokens: TokenAddressMap): Currency | undefined {
return currencyId === 'ETH' ? nativeOnChain(chainId) : tokens[chainId]?.[currencyId]?.token
function getCurrency(currencyId: string, chainId: SupportedChainId, tokens: ChainTokenMap): Currency | undefined {
return currencyId === 'ETH' ? nativeOnChain(chainId) : tokens[chainId]?.[currencyId]
}
function buildCurrencyDescriptor(
@@ -46,7 +46,7 @@ function buildCurrencyDescriptor(
function parseSwap(
swap: ExactInputSwapTransactionInfo | ExactOutputSwapTransactionInfo,
chainId: SupportedChainId,
tokens: TokenAddressMap
tokens: ChainTokenMap
): Partial<Activity> {
const tokenIn = getCurrency(swap.inputCurrencyId, chainId, tokens)
const tokenOut = getCurrency(swap.outputCurrencyId, chainId, tokens)
@@ -76,7 +76,7 @@ function parseWrap(wrap: WrapTransactionInfo, chainId: SupportedChainId, status:
function parseApproval(
approval: ApproveTransactionInfo,
chainId: SupportedChainId,
tokens: TokenAddressMap
tokens: ChainTokenMap
): Partial<Activity> {
// TODO: Add 'amount' approved to ApproveTransactionInfo so we can distinguish between revoke and approve
const currency = getCurrency(approval.tokenAddress, chainId, tokens)
@@ -91,7 +91,7 @@ type GenericLPInfo = Omit<
AddLiquidityV3PoolTransactionInfo | RemoveLiquidityV3TransactionInfo | AddLiquidityV2PoolTransactionInfo,
'type'
>
function parseLP(lp: GenericLPInfo, chainId: SupportedChainId, tokens: TokenAddressMap): Partial<Activity> {
function parseLP(lp: GenericLPInfo, chainId: SupportedChainId, tokens: ChainTokenMap): Partial<Activity> {
const baseCurrency = getCurrency(lp.baseCurrencyId, chainId, tokens)
const quoteCurrency = getCurrency(lp.quoteCurrencyId, chainId, tokens)
const [baseRaw, quoteRaw] = [lp.expectedAmountBaseRaw, lp.expectedAmountQuoteRaw]
@@ -103,7 +103,7 @@ function parseLP(lp: GenericLPInfo, chainId: SupportedChainId, tokens: TokenAddr
function parseCollectFees(
collect: CollectFeesTransactionInfo,
chainId: SupportedChainId,
tokens: TokenAddressMap
tokens: ChainTokenMap
): Partial<Activity> {
// Adapts CollectFeesTransactionInfo to generic LP type
const {
@@ -118,7 +118,7 @@ function parseCollectFees(
function parseMigrateCreateV3(
lp: MigrateV2LiquidityToV3TransactionInfo | CreateV3PoolTransactionInfo,
chainId: SupportedChainId,
tokens: TokenAddressMap
tokens: ChainTokenMap
): Partial<Activity> {
const baseCurrency = getCurrency(lp.baseCurrencyId, chainId, tokens)
const baseSymbol = baseCurrency?.symbol ?? t`Unknown`
@@ -132,7 +132,7 @@ function parseMigrateCreateV3(
export function parseLocalActivity(
details: TransactionDetails,
chainId: SupportedChainId,
tokens: TokenAddressMap
tokens: ChainTokenMap
): Activity | undefined {
try {
const status = !details.receipt
@@ -141,7 +141,7 @@ export function parseLocalActivity(
? TransactionStatus.Confirmed
: TransactionStatus.Failed
const receipt: TransactionPartsFragment | undefined = details.receipt
const receipt = details.receipt
? {
id: details.receipt.transactionHash,
...details.receipt,
@@ -157,6 +157,7 @@ export function parseLocalActivity(
status,
timestamp: (details.confirmedTime ?? details.addedTime) / 1000,
receipt,
nonce: details.nonce,
}
let additionalFields: Partial<Activity> = {}
@@ -188,7 +189,7 @@ export function parseLocalActivity(
export function useLocalActivities(account: string): ActivityMap {
const allTransactions = useMultichainTransactions()
const tokens = useCombinedActiveList()
const tokens = useAllTokensMultichain()
return useMemo(() => {
const activityByHash: ActivityMap = {}

View File

@@ -254,6 +254,7 @@ function parseRemoteActivity(assetActivity: AssetActivityPartsFragment): Activit
title: assetActivity.type,
descriptor: assetActivity.transaction.to,
receipt: assetActivity.transaction,
nonce: assetActivity.transaction.nonce,
}
const parsedFields = ActivityParserByType[assetActivity.type]?.(changes, assetActivity)

View File

@@ -14,7 +14,8 @@ export type Activity = {
logos?: Array<string | undefined>
currencies?: Array<Currency | undefined>
otherAccount?: string
receipt?: Receipt
receipt?: Omit<Receipt, 'nonce'>
nonce?: number | null
}
export type ActivityMap = { [hash: string]: Activity | undefined }

View File

@@ -30,6 +30,7 @@ const Wrapper = styled(Column)<{ numItems: number; isExpanded: boolean }>`
overflow: hidden;
`
// TODO(WEB-3288): Replace this component to use `components/Expand` under the hood
type ExpandoRowProps = PropsWithChildren<{ title?: string; numItems: number; isExpanded: boolean; toggle: () => void }>
export function ExpandoRow({ title = t`Hidden`, numItems, isExpanded, toggle, children }: ExpandoRowProps) {
if (numItems === 0) return null

View File

@@ -126,7 +126,7 @@ export function useGetCachedTokens(chains: SupportedChainId[]): TokenGetterFn {
const local: { [address: string]: Token | undefined } = {}
const missing = new Set<string>()
addresses.forEach((address) => {
const cached = tokenCache.get(chainId, address) ?? allTokens[chainId][address]?.token
const cached = tokenCache.get(chainId, address) ?? allTokens[chainId]?.[address]
cached ? (local[address] = cached) : missing.add(address)
})

View File

@@ -1,7 +1,7 @@
import { Token } from '@uniswap/sdk-core'
import { AddressMap } from '@uniswap/smart-order-router'
import { abi as MulticallABI } from '@uniswap/v3-periphery/artifacts/contracts/lens/UniswapInterfaceMulticall.sol/UniswapInterfaceMulticall.json'
import { abi as NFTPositionManagerABI } from '@uniswap/v3-periphery/artifacts/contracts/NonfungiblePositionManager.sol/NonfungiblePositionManager.json'
import MulticallJSON from '@uniswap/v3-periphery/artifacts/contracts/lens/UniswapInterfaceMulticall.sol/UniswapInterfaceMulticall.json'
import NFTPositionManagerJSON from '@uniswap/v3-periphery/artifacts/contracts/NonfungiblePositionManager.sol/NonfungiblePositionManager.json'
import { useWeb3React } from '@web3-react/core'
import { MULTICALL_ADDRESS, NONFUNGIBLE_POSITION_MANAGER_ADDRESSES as V3NFT_ADDRESSES } from 'constants/addresses'
import { isSupportedChain, SupportedChainId } from 'constants/chains'
@@ -43,11 +43,11 @@ function useContractMultichain<T extends BaseContract>(
}
export function useV3ManagerContracts(chainIds: SupportedChainId[]): ContractMap<NonfungiblePositionManager> {
return useContractMultichain<NonfungiblePositionManager>(V3NFT_ADDRESSES, NFTPositionManagerABI, chainIds)
return useContractMultichain<NonfungiblePositionManager>(V3NFT_ADDRESSES, NFTPositionManagerJSON.abi, chainIds)
}
export function useInterfaceMulticallContracts(chainIds: SupportedChainId[]): ContractMap<UniswapInterfaceMulticall> {
return useContractMultichain<UniswapInterfaceMulticall>(MULTICALL_ADDRESS, MulticallABI, chainIds)
return useContractMultichain<UniswapInterfaceMulticall>(MULTICALL_ADDRESS, MulticallJSON.abi, chainIds)
}
type PriceMap = { [key: CurrencyKey]: number | undefined }

View File

@@ -22,12 +22,12 @@ import { PositionInfo } from './cache'
import { useFeeValues } from './hooks'
import useMultiChainPositions from './useMultiChainPositions'
/*
This hook takes an array of PositionInfo objects (format used by the Uniswap Labs gql API).
The hook access PositionInfo.details (format used by the NFT position contract),
filters the PositionDetails data for malicious content,
and then returns the original data in its original format.
*/
/**
* Takes an array of PositionInfo objects (format used by the Uniswap Labs gql API).
* The hook access PositionInfo.details (format used by the NFT position contract),
* filters the PositionDetails data for malicious content,
* and then returns the original data in its original format.
*/
function useFilterPossiblyMaliciousPositionInfo(positions: PositionInfo[] | undefined): PositionInfo[] {
const tokenIdsToPositionInfo: Record<string, PositionInfo> = useMemo(
() =>
@@ -154,7 +154,7 @@ function PositionListItem({ positionInfo }: { positionInfo: PositionInfo }) {
left={<PortfolioLogo chainId={chainId} currencies={[pool.token0, pool.token1]} />}
title={
<Row>
<ThemedText.SubHeader fontWeight={500}>
<ThemedText.SubHeader>
{pool.token0.symbol} / {pool.token1?.symbol}
</ThemedText.SubHeader>
</Row>
@@ -173,7 +173,7 @@ function PositionListItem({ positionInfo }: { positionInfo: PositionInfo }) {
</div>
}
>
<ThemedText.SubHeader fontWeight={500}>
<ThemedText.SubHeader>
{formatNumber((liquidityValue ?? 0) + (feeValue ?? 0), NumberType.PortfolioBalance)}
</ThemedText.SubHeader>
</MouseoverTooltip>

View File

@@ -1,5 +1,5 @@
import { CurrencyAmount, Token } from '@uniswap/sdk-core'
import { abi as IUniswapV3PoolStateABI } from '@uniswap/v3-core/artifacts/contracts/interfaces/pool/IUniswapV3PoolState.sol/IUniswapV3PoolState.json'
import IUniswapV3PoolStateJSON from '@uniswap/v3-core/artifacts/contracts/interfaces/pool/IUniswapV3PoolState.sol/IUniswapV3PoolState.json'
import { computePoolAddress, Pool, Position } from '@uniswap/v3-sdk'
import { V3_CORE_FACTORY_ADDRESSES } from 'constants/addresses'
import { SupportedChainId } from 'constants/chains'
@@ -49,7 +49,7 @@ const DEFAULT_CHAINS = [
SupportedChainId.CELO,
]
type UseMultiChainPositionsData = { positions: PositionInfo[] | undefined; loading: boolean }
type UseMultiChainPositionsData = { positions?: PositionInfo[]; loading: boolean }
/**
* Returns all positions for a given account on multiple chains.
@@ -118,7 +118,7 @@ export default function useMultiChainPositions(account: string, chains = DEFAULT
// Combines PositionDetails with Pool data to build our return type
const fetchPositionInfo = useCallback(
async (positionDetails: PositionDetails[], chainId: SupportedChainId, multicall: UniswapInterfaceMulticall) => {
const poolInterface = new Interface(IUniswapV3PoolStateABI) as UniswapV3PoolInterface
const poolInterface = new Interface(IUniswapV3PoolStateJSON.abi) as UniswapV3PoolInterface
const tokens = await getTokens(
positionDetails.flatMap((details) => [details.token0, details.token1]),
chainId

View File

@@ -22,12 +22,14 @@ const EndColumn = styled(Column)`
`
export default function PortfolioRow({
['data-testid']: testId,
left,
title,
descriptor,
right,
onClick,
}: {
'data-testid'?: string
left: React.ReactNode
title: React.ReactNode
descriptor?: React.ReactNode
@@ -36,7 +38,7 @@ export default function PortfolioRow({
onClick?: () => void
}) {
return (
<PortfolioRowWrapper onClick={onClick}>
<PortfolioRowWrapper data-testid={testId} onClick={onClick}>
{left}
<AutoColumn grow>
{title}

View File

@@ -112,7 +112,7 @@ function TokenRow({ token, quantity, denominatedValue, tokenProjectMarket }: Tok
>
<PortfolioRow
left={<PortfolioLogo chainId={currency.chainId} currencies={[currency]} size="40px" />}
title={<ThemedText.SubHeader fontWeight={500}>{token?.name}</ThemedText.SubHeader>}
title={<ThemedText.SubHeader>{token?.name}</ThemedText.SubHeader>}
descriptor={
<TokenBalanceText>
{formatNumber(quantity, NumberType.TokenNonTx)} {token?.symbol}
@@ -122,7 +122,7 @@ function TokenRow({ token, quantity, denominatedValue, tokenProjectMarket }: Tok
right={
denominatedValue && (
<>
<ThemedText.SubHeader fontWeight={500}>
<ThemedText.SubHeader>
{formatNumber(denominatedValue?.value, NumberType.PortfolioBalance)}
</ThemedText.SubHeader>
<Row justify="flex-end">

View File

@@ -13,7 +13,7 @@ function wasPending(previousTxs: { [hash: string]: TransactionDetails | undefine
return previousTx && isTxPending(previousTx)
}
function useHasUpdatedTx() {
function useHasUpdatedTx(account: string | undefined) {
// TODO: consider monitoring tx's on chains other than the wallet's current chain
const currentChainTxs = useAllTransactions()
@@ -27,12 +27,12 @@ function useHasUpdatedTx() {
const previousPendingTxs = usePrevious(pendingTxs)
return useMemo(() => {
if (!previousPendingTxs) return false
if (!previousPendingTxs || !account) return false
return Object.values(currentChainTxs).some(
(tx) => !isTxPending(tx) && wasPending(previousPendingTxs, tx),
(tx) => tx.from === account && !isTxPending(tx) && wasPending(previousPendingTxs, tx),
[currentChainTxs, previousPendingTxs]
)
}, [currentChainTxs, previousPendingTxs])
}, [account, currentChainTxs, previousPendingTxs])
}
/* Prefetches & caches portfolio balances when the wrapped component is hovered or the user completes a transaction */
@@ -49,16 +49,22 @@ export default function PrefetchBalancesWrapper({ children }: PropsWithChildren)
}
}, [account, prefetchPortfolioBalances])
// TODO(cartcrom): add delay for refetching on optimism, as there is high latency in new balances being available
const hasUpdatedTx = useHasUpdatedTx()
// Listens for recently updated transactions to keep portfolio balances fresh in apollo cache
useEffect(() => {
if (!hasUpdatedTx) return
const prevAccount = usePrevious(account)
// If the drawer is open, fetch balances immediately, else set a flag to fetch on next hover
if (drawerOpen) fetchBalances()
else setHasUnfetchedBalances(true)
}, [drawerOpen, fetchBalances, hasUpdatedTx])
// TODO(cartcrom): add delay for refetching on optimism, as there is high latency in new balances being available
const hasUpdatedTx = useHasUpdatedTx(account)
// Listens for account changes & recently updated transactions to keep portfolio balances fresh in apollo cache
useEffect(() => {
const accountChanged = prevAccount !== undefined && prevAccount !== account
if (hasUpdatedTx || accountChanged) {
// If the drawer is open, fetch balances immediately, else set a flag to fetch on next hover
if (drawerOpen) {
fetchBalances()
} else {
setHasUnfetchedBalances(true)
}
}
}, [account, prevAccount, drawerOpen, fetchBalances, hasUpdatedTx])
const onHover = useCallback(() => {
if (hasUnfetchedBalances) fetchBalances()

View File

@@ -49,7 +49,7 @@ export const SlideOutMenu = ({
<Header>
<StyledArrow data-testid="wallet-back" onClick={onClose} size={24} />
<Title>
<ThemedText.SubHeader fontWeight={500}>{title}</ThemedText.SubHeader>
<ThemedText.SubHeader>{title}</ThemedText.SubHeader>
</Title>
</Header>

View File

@@ -1,17 +1,16 @@
import { Trans } from '@lingui/macro'
import { sendAnalyticsEvent } from '@uniswap/analytics'
import { InterfaceElementName } from '@uniswap/analytics-events'
import { useWeb3React } from '@web3-react/core'
import { WalletConnect } from '@web3-react/walletconnect'
import Column, { AutoColumn } from 'components/Column'
import Modal from 'components/Modal'
import { RowBetween } from 'components/Row'
import { uniwalletConnectConnection } from 'connection'
import { ActivationStatus, useActivationState } from 'connection/activate'
import { ConnectionType } from 'connection/types'
import { UniwalletConnect } from 'connection/WalletConnect'
import { QRCodeSVG } from 'qrcode.react'
import { useCallback, useEffect, useState } from 'react'
import { useModalIsOpen, useToggleUniwalletModal } from 'state/application/hooks'
import { ApplicationModal } from 'state/application/reducer'
import { useEffect, useState } from 'react'
import styled, { useTheme } from 'styled-components/macro'
import { CloseIcon, ThemedText } from 'theme'
@@ -39,44 +38,37 @@ const Divider = styled.div`
`
export default function UniwalletModal() {
const open = useModalIsOpen(ApplicationModal.UNIWALLET_CONNECT)
const toggle = useToggleUniwalletModal()
const { activationState, cancelActivation } = useActivationState()
const [uri, setUri] = useState<string>()
// Displays the modal if a Uniswap Wallet Connection is pending & qrcode URI is available
const open =
activationState.status === ActivationStatus.PENDING &&
activationState.connection.type === ConnectionType.UNIWALLET &&
!!uri
useEffect(() => {
;(uniwalletConnectConnection.connector as WalletConnect).events.addListener(
UniwalletConnect.UNI_URI_AVAILABLE,
(uri) => {
uri && setUri(uri)
toggle()
}
)
}, [toggle])
}, [])
const { account } = useWeb3React()
useEffect(() => {
if (open) {
sendAnalyticsEvent('Uniswap wallet modal opened', { userConnected: !!account })
if (account) {
toggle()
}
}
}, [account, open, toggle])
const onClose = useCallback(() => {
uniwalletConnectConnection.connector.deactivate?.()
toggle()
}, [toggle])
if (open) sendAnalyticsEvent('Uniswap wallet modal opened')
}, [open])
const theme = useTheme()
return (
<Modal isOpen={open} onDismiss={onClose}>
<Modal isOpen={open} onDismiss={cancelActivation}>
<UniwalletConnectWrapper>
<HeaderRow>
<ThemedText.SubHeader>
<Trans>Scan with Uniswap Wallet</Trans>
</ThemedText.SubHeader>
<CloseIcon onClick={onClose} />
<CloseIcon onClick={cancelActivation} />
</HeaderRow>
<QRCodeWrapper>
{uri && (

View File

@@ -27,6 +27,11 @@ export function useToggleAccountDrawer() {
}, [updateAccountDrawerOpen])
}
export function useCloseAccountDrawer() {
const updateAccountDrawerOpen = useUpdateAtom(accountDrawerOpenAtom)
return useCallback(() => updateAccountDrawerOpen(false), [updateAccountDrawerOpen])
}
export function useAccountDrawer(): [boolean, () => void] {
const accountDrawerOpen = useAtomValue(accountDrawerOpenAtom)
return [accountDrawerOpen, useToggleAccountDrawer()]
@@ -200,7 +205,7 @@ function AccountDrawer() {
name={InterfaceEventName.MINI_PORTFOLIO_TOGGLED}
properties={{ type: 'close' }}
>
<CloseDrawer onClick={toggleWalletDrawer}>
<CloseDrawer onClick={toggleWalletDrawer} data-testid="close-account-drawer">
<CloseIcon />
</CloseDrawer>
</TraceEvent>

View File

@@ -0,0 +1,31 @@
import { render, screen, waitFor } from 'test-utils/render'
import AnimatedDropdown from './index'
describe('AnimatedDropdown', () => {
it('does not render children when closed', () => {
render(<AnimatedDropdown open={false}>Body</AnimatedDropdown>)
expect(screen.getByText('Body')).not.toBeVisible()
})
it('renders children when open', () => {
render(<AnimatedDropdown open={true}>Body</AnimatedDropdown>)
expect(screen.getByText('Body')).toBeVisible()
})
it('animates when open changes', async () => {
const { rerender } = render(<AnimatedDropdown open={false}>Body</AnimatedDropdown>)
const body = screen.getByText('Body')
expect(body).not.toBeVisible()
rerender(<AnimatedDropdown open={true}>Body</AnimatedDropdown>)
expect(body).not.toBeVisible()
// wait for React Spring animation to finish
await waitFor(() => {
expect(body).toBeVisible()
})
})
})

View File

@@ -9,7 +9,10 @@ export default function AnimatedDropdown({ open, children }: React.PropsWithChil
const { ref, height } = useResizeObserver()
const props = useSpring({
height: open ? height ?? 0 : 0,
// On initial render, `height` will be undefined as ref has not been set yet.
// If the dropdown should be open, we fallback to `auto` to avoid flickering.
// Otherwise, we just animate between actual height (when open) and 0 (when closed).
height: open ? height ?? 'auto' : 0,
config: {
mass: 1.2,
tension: 300,
@@ -20,14 +23,7 @@ export default function AnimatedDropdown({ open, children }: React.PropsWithChil
})
return (
<animated.div
style={{
...props,
overflow: 'hidden',
width: '100%',
willChange: 'height',
}}
>
<animated.div style={{ ...props, overflow: 'hidden', width: '100%', willChange: 'height' }}>
<div ref={ref}>{children}</div>
</animated.div>
)

View File

@@ -32,13 +32,7 @@ const LabelText = styled.div<{ color: string }>`
justify-content: flex-end;
`
export default function RangeBadge({
removed,
inRange,
}: {
removed: boolean | undefined
inRange: boolean | undefined
}) {
export default function RangeBadge({ removed, inRange }: { removed?: boolean; inRange?: boolean }) {
const theme = useTheme()
return (
<BadgeWrapper>

View File

@@ -14,7 +14,7 @@ import { useHideUniswapWalletBanner } from 'state/user/hooks'
import styled from 'styled-components/macro'
import { ThemedText } from 'theme'
import { Z_INDEX } from 'theme/zIndex'
import { isIOS } from 'utils/userAgent'
import { isIOS, isMobileSafari } from 'utils/userAgent'
const PopupContainer = styled.div<{ show: boolean }>`
display: flex;
@@ -93,6 +93,8 @@ export default function UniswapWalletBanner() {
const screenSize = useScreenSize()
if (isMobileSafari) return null
return (
<PopupContainer show={shouldDisplay}>
<StyledXButton

View File

@@ -296,7 +296,7 @@ export function ButtonConfirmed({
}
}
export function ButtonError({ error, ...rest }: { error?: boolean } & ButtonProps) {
export function ButtonError({ error, ...rest }: { error?: boolean } & BaseButtonProps) {
if (error) {
return <ButtonErrorStyle {...rest} />
} else {

View File

@@ -19,7 +19,7 @@ interface SparklineChartProps {
width: number
height: number
tokenData: TopToken
pricePercentChange: number | undefined | null
pricePercentChange?: number | null
sparklineMap: SparklineMap
}

View File

@@ -1,6 +1,5 @@
import styled, { DefaultTheme } from 'styled-components/macro'
type Gap = keyof DefaultTheme['grids']
import styled from 'styled-components/macro'
import { Gap } from 'theme'
export const Column = styled.div<{
gap?: Gap

View File

@@ -14,7 +14,7 @@ const ContentWrapper = styled(Column)`
font-size: 12px;
`
interface ConnectedAccountBlockedProps {
account: string | null | undefined
account?: string | null
isOpen: boolean
}

View File

@@ -1,15 +1,13 @@
import { Trans } from '@lingui/macro'
// eslint-disable-next-line no-restricted-imports
import { t } from '@lingui/macro'
import { formatNumber, formatPriceImpact, NumberType } from '@uniswap/conedison/format'
import { Percent } from '@uniswap/sdk-core'
import Row from 'components/Row'
import { LoadingBubble } from 'components/Tokens/loading'
import { MouseoverTooltip } from 'components/Tooltip'
import { useMemo } from 'react'
import styled, { useTheme } from 'styled-components/macro'
import { ThemedText } from '../../theme'
import { warningSeverity } from '../../utils/prices'
import styled from 'styled-components/macro'
import { ThemedText } from 'theme'
import { warningSeverity } from 'utils/prices'
const FiatLoadingBubble = styled(LoadingBubble)`
border-radius: 4px;
@@ -21,36 +19,40 @@ export function FiatValue({
fiatValue,
priceImpact,
}: {
fiatValue?: { data?: number; isLoading: boolean }
fiatValue: { data?: number; isLoading: boolean }
priceImpact?: Percent
}) {
const theme = useTheme()
const priceImpactColor = useMemo(() => {
if (!priceImpact) return undefined
if (priceImpact.lessThan('0')) return theme.accentSuccess
if (priceImpact.lessThan('0')) return 'accentSuccess'
const severity = warningSeverity(priceImpact)
if (severity < 1) return theme.textTertiary
if (severity < 3) return theme.deprecated_yellow1
return theme.accentFailure
}, [priceImpact, theme.accentSuccess, theme.accentFailure, theme.textTertiary, theme.deprecated_yellow1])
if (severity < 1) return 'textTertiary'
if (severity < 3) return 'deprecated_yellow1'
return 'accentFailure'
}, [priceImpact])
if (fiatValue.isLoading) {
return <FiatLoadingBubble />
}
return (
<ThemedText.DeprecatedBody fontSize={14} color={theme.textSecondary}>
{fiatValue?.isLoading ? (
<FiatLoadingBubble />
) : (
<div>
{fiatValue?.data ? formatNumber(fiatValue.data, NumberType.FiatTokenPrice) : undefined}
{priceImpact && (
<span style={{ color: priceImpactColor }}>
{' '}
<MouseoverTooltip text={t`The estimated difference between the USD values of input and output amounts.`}>
(<Trans>{formatPriceImpact(priceImpact)}</Trans>)
</MouseoverTooltip>
</span>
)}
</div>
<Row gap="sm">
<ThemedText.BodySmall>
{fiatValue.data ? (
formatNumber(fiatValue.data, NumberType.FiatTokenPrice)
) : (
<MouseoverTooltip text={<Trans>Not enough liquidity to show accurate USD value.</Trans>}>-</MouseoverTooltip>
)}
</ThemedText.BodySmall>
{priceImpact && (
<ThemedText.BodySmall color={priceImpactColor}>
<MouseoverTooltip
text={<Trans>The estimated difference between the USD values of input and output amounts.</Trans>}
>
(<Trans>{formatPriceImpact(priceImpact)}</Trans>)
</MouseoverTooltip>
</ThemedText.BodySmall>
)}
</ThemedText.DeprecatedBody>
</Row>
)
}

View File

@@ -195,7 +195,7 @@ interface SwapCurrencyInputPanelProps {
pair?: Pair | null
hideInput?: boolean
otherCurrency?: Currency | null
fiatValue: { data?: number; isLoading: boolean }
fiatValue?: { data?: number; isLoading: boolean }
priceImpact?: Percent
id: string
showCommonBases?: boolean
@@ -204,6 +204,7 @@ interface SwapCurrencyInputPanelProps {
renderBalance?: (amount: CurrencyAmount<Currency>) => ReactNode
locked?: boolean
loading?: boolean
disabled?: boolean
}
export default function SwapCurrencyInputPanel({
@@ -226,6 +227,7 @@ export default function SwapCurrencyInputPanel({
hideInput = false,
locked = false,
loading = false,
disabled = false,
...rest
}: SwapCurrencyInputPanelProps) {
const [modalOpen, setModalOpen] = useState(false)
@@ -258,13 +260,13 @@ export default function SwapCurrencyInputPanel({
className="token-amount-input"
value={value}
onUserInput={onUserInput}
disabled={!chainAllowed}
disabled={!chainAllowed || disabled}
$loading={loading}
/>
)}
<CurrencySelect
disabled={!chainAllowed}
disabled={!chainAllowed || disabled}
visible={currency !== undefined}
selected={!!currency}
hideInput={hideInput}
@@ -306,11 +308,12 @@ export default function SwapCurrencyInputPanel({
<FiatRow>
<RowBetween>
<LoadingOpacityContainer $loading={loading}>
<FiatValue fiatValue={fiatValue} priceImpact={priceImpact} />
{fiatValue && <FiatValue fiatValue={fiatValue} priceImpact={priceImpact} />}
</LoadingOpacityContainer>
{account ? (
<RowFixed style={{ height: '17px' }}>
<ThemedText.DeprecatedBody
data-testid="balance-text"
color={theme.textSecondary}
fontWeight={400}
fontSize={14}

View File

@@ -1,7 +1,7 @@
import { Trans } from '@lingui/macro'
import { TraceEvent } from '@uniswap/analytics'
import { BrowserEvent, InterfaceElementName, SwapEventName } from '@uniswap/analytics-events'
import { Currency, CurrencyAmount, Percent } from '@uniswap/sdk-core'
import { Currency, CurrencyAmount } from '@uniswap/sdk-core'
import { Pair } from '@uniswap/v2-sdk'
import { useWeb3React } from '@web3-react/core'
import { AutoColumn } from 'components/Column'
@@ -183,7 +183,6 @@ interface CurrencyInputPanelProps {
hideInput?: boolean
otherCurrency?: Currency | null
fiatValue?: { data?: number; isLoading: boolean }
priceImpact?: Percent
id: string
showCommonBases?: boolean
showCurrencyAmount?: boolean
@@ -207,7 +206,6 @@ export default function CurrencyInputPanel({
disableNonToken,
renderBalance,
fiatValue,
priceImpact,
hideBalance = false,
pair = null, // used for double token logo
hideInput = false,
@@ -293,7 +291,7 @@ export default function CurrencyInputPanel({
<FiatRow>
<RowBetween>
<LoadingOpacityContainer $loading={loading}>
<FiatValue fiatValue={fiatValue} priceImpact={priceImpact} />
{fiatValue && <FiatValue fiatValue={fiatValue} />}
</LoadingOpacityContainer>
{account ? (
<RowFixed style={{ height: '17px' }}>

View File

@@ -132,7 +132,7 @@ const Fallback = ({ error, eventId }: { error: Error; eventId: string | null })
</Column>
<CodeBlockWrapper>
<CodeTitle>
<ThemedText.SubHeader fontWeight={500}>
<ThemedText.SubHeader>
<Trans>Error ID: {eventId}</Trans>
</ThemedText.SubHeader>
<CopyToClipboard toCopy={eventId}>
@@ -164,7 +164,7 @@ const Fallback = ({ error, eventId }: { error: Error; eventId: string | null })
</Column>
<CodeBlockWrapper>
<CodeTitle>
<ThemedText.SubHeader fontWeight={500}>Error details</ThemedText.SubHeader>
<ThemedText.SubHeader>Error details</ThemedText.SubHeader>
<CopyToClipboard toCopy={errorDetails}>
<CopyIcon />
</CopyToClipboard>

View File

@@ -0,0 +1,38 @@
import { fireEvent, render, screen } from 'test-utils/render'
import noop from 'utils/noop'
import Expand from './index'
describe('Expand', () => {
it('does not render children when closed', () => {
render(
<Expand header={<span>Header</span>} isOpen={false} onToggle={noop} button={<span>Button</span>}>
Body
</Expand>
)
expect(screen.queryByText('Body')).not.toBeVisible()
})
it('renders children when open', () => {
render(
<Expand header={<span>Header</span>} isOpen={true} onToggle={noop} button={<span>Button</span>}>
Body
</Expand>
)
expect(screen.queryByText('Body')).toBeVisible()
})
it('calls `onToggle` when button is pressed', () => {
const onToggle = jest.fn()
render(
<Expand header={<span>Header</span>} isOpen={false} onToggle={onToggle} button={<span>Button</span>}>
Body
</Expand>
)
const button = screen.getByText('Button')
fireEvent.click(button)
expect(onToggle).toHaveBeenCalled()
})
})

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