Compare commits

...

154 Commits

Author SHA1 Message Date
Zach Pomerantz
a538bf0b69 build: target ESNext for dev (#6379)
* build: add caching to eslint

* build: add caching to jest

* build: add caching to tsc

* build: add caching to actions

* fix: upgrade upload-artifact to v3

* build: update craco eslint cacheLocation

* build: target ESNext for dev

* merge again
2023-04-17 14:26:00 -07:00
Zach Pomerantz
135cb8fb34 fix: translate "Get started" on landing page (#6371)
* fix: translate "Get started" on landing page

* lint: html
2023-04-17 14:03:38 -07:00
cartcrom
bf50582d38 test: local activity tests (#6341)
* refactor: move MP files into subfolders

* refactor: consolidate MP subfolder file-naming scheme

* test: add tests for parseLocal.ts

* refactor: update existing parseLocal tests
2023-04-17 13:03:43 -07:00
lynn
110e23d6eb fix: fix padding for transaction toast notifications (#6373)
* padding fixes, special casing for txn

* add drop shadow, change width to 348px, remove debug code

* opacity animation

* address comments

* one more change

* respond to tina comments

* name change

* add $ to padding
2023-04-17 14:39:38 -04:00
Zach Pomerantz
7ab6a17b42 build: utilize the node_modules/.cache (#6364)
* build: add caching to eslint

* build: add caching to jest

* build: add caching to tsc

* build: add caching to actions

* fix: upgrade upload-artifact to v3

* build: update craco eslint cacheLocation

* fix: pr nits
2023-04-17 09:28:00 -07:00
lynn
7ad13c96a8 fix: fix slow yarn start (#6369)
init
2023-04-14 11:45:33 -05:00
Vignesh Mohankumar
4e99cc4d93 build: send url without hash to sentry (#6352)
* build: send url without hash to sentry

* comments

* Update src/tracing/index.ts

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

* Update src/tracing/index.ts

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

* add test

* move files around
2023-04-13 17:49:29 -04:00
Vignesh Mohankumar
6d29815f59 fix: ignore error caused by Cloudflare HTML response (#6356)
* fix: ignore error caused by Cloudflare HTML response

* add test

* Update src/tracing/errors.test.ts

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

* Update src/tracing/errors.ts

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

* Update src/tracing/errors.ts

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

* multiline
2023-04-13 17:48:50 -04:00
Zach Pomerantz
4888fe23df test: add jest.asMock (#6310)
* test: add jest.asMock

* test: use mocked instead

* test: split test-utils to prevent interaction

* test: whoops missed one

* Merge but actually this time
2023-04-13 12:44:06 -07:00
Zach Pomerantz
ef9ecd9ce2 build: optimize build by splitting out typechecking and linting (#6323)
* build: move typecheck/lint out of build

* build: add typecheck to test action

* build: fix lint to use gitignore

* fix: correctly lint/check

* fix: simplify lint

* build: back out eslint array-ification

* test(lint): add comment RE config/typings

* build: clarify craco webpack plugin mods

* build: simplify craco webpack with functional methods

* build: rm unused IgnorePlugin

* test(lint): order imports
2023-04-13 12:43:47 -07:00
Jack Short
f5d0804c46 feat: creating feature flag for details v2 page (#6359)
* feat: creating feature flag for details v2 page

* eslint ignore

* moving details v2 under trace
2023-04-13 15:43:14 -04:00
Vignesh Mohankumar
0bac257254 fix: handle invalid chainId on position page (#6338)
* fix: handle invalid chainId on position page

* fix

* add test

* Revert "add test"

This reverts commit d18742aa50.

* pr comments

* rename

* fix
2023-04-13 15:40:31 -04:00
lynn
a77752ab83 fix: re-implement dark mode for connector icons (#6329)
* init

* init

* Update src/connection/index.test.tsx

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

* Update src/components/Identicon/StatusIcon.test.tsx

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

* Update src/components/Identicon/StatusIcon.test.tsx

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

* address comments

* unit test + remove _url in names

---------

Co-authored-by: Zach Pomerantz <zzmp@uniswap.org>
2023-04-13 11:17:48 -04:00
cartcrom
bf31ca4f06 feat: updated banner (#6355)
* feat: updated banner

* fix: linted

* feat: add download link to overflow menu

* feat: hover animation

* fix: update landing screen hide logic

* feat: added descriptor comment for stopPropogation

* fix: translations & responsiveness of button text

* fix: icon sizing / padding + word casing

* fix: Learn more casing
2023-04-12 22:00:05 -04:00
Charles Bachmeier
ed8afbd851 fix: App crashing when changing sorting or toggling usd price on trneding NFTs table (#6354)
* fix app breaking when changing sorting or toggling usd price

* compare floor change correctly

---------

Co-authored-by: Charles Bachmeier <charlie@genie.xyz>
2023-04-12 15:49:04 -07:00
Vignesh Mohankumar
47b6a7c4d5 build: add chainId as tag in sentry ErrorBoundary (#6345)
* build: add chainId to sentry tags

* set it in web3provider

* set before sending
2023-04-12 14:43:37 -04:00
cartcrom
086fc65457 feat: remove /wallet route (#6350)
* feat: replace internal microsite routes with links to external site

* feat: use updated analytics events from events repo

* fix: remove unnused empty wallet page
2023-04-12 13:55:53 -04:00
cartcrom
7df53f30a0 feat: uniwallet banner (#6344)
* feat: update wallet banner display logic

* fix: linted

* fix: learn more link
2023-04-12 12:03:00 -04:00
Jack Short
66497a0108 fix: last sale nan (#6337)
* fix: last sale nan

* correct formatted price

* responding to comments

* refactoring for readability
2023-04-12 11:45:48 -04:00
cartcrom
e0eb701bc0 feat: log connection activation/errors (#6333)
* feat: log connection activation/errors in console.debug

* fix: remove now-redundant comment
2023-04-12 10:36:01 -04:00
cartcrom
36cb0668a3 fix: use updated wallet links/language (#6343)
fix: use updated links/language
2023-04-11 18:36:34 -04:00
eddie
810f42136e fix: auto-dismiss MP bottom sheet after Buy Crypto click (#6298) 2023-04-11 14:46:28 -07:00
Vignesh Mohankumar
07b7d7f268 test: add test for position page states (#6342) 2023-04-11 17:10:38 -04:00
Charles Bachmeier
39b5bb37cd feat: hide looksrare until we can support their sdk (#6339)
* hide looksrare until we can support their sdk

* remove commented code

---------

Co-authored-by: Charles Bachmeier <charlie@genie.xyz>
2023-04-11 13:13:55 -07:00
cartcrom
feed63b1b3 refactor: reorganize mini-portfolio file names (#6336)
* refactor: move MP files into subfolders

* refactor: consolidate MP subfolder file-naming scheme
2023-04-11 15:26:27 -04:00
Vignesh Mohankumar
ee56382956 fix: ignores "user rejected transaction" errors (#6330)
* ignore user rejected errors

* test

* fix

* use util
2023-04-11 14:28:30 -04:00
lynn
64e396d9e0 fix: update empty state svg icons (#6326)
* update icons

* add snapshot test for empty states

* update failing snapshot
2023-04-11 13:37:05 -04:00
Tina
2ffc8a0bdf chore: Remove SWAP_TRANSACTION_COMPLETED event (#6328)
remove swap completed event
2023-04-10 17:27:02 -07:00
eddie
5ec9cdc5c4 fix: redo MP drawer layout changes with mobile fixed (#6280)
* fix: redo MP drawer layout changes with mobile fixed

* fix: mobile fix and another test

* fix: comments
2023-04-10 12:53:58 -07:00
eddie
4d85775d90 fix: update CurrencyList unit tests (#6321)
* fix: testing snapshot updates

* fix: remove inline style and update snapshots tests
2023-04-10 12:18:54 -07:00
eddie
c1c59ca692 refactor: rename WalletDropdown to AccountDrawer (#6313)
* feat: rename WalletDropdown to Portfolio

* fix: update after rebase

* feat: rename from Portfolio to AccountDrawer

* fix: fix test
2023-04-10 11:26:05 -07:00
Zach Pomerantz
f29d97413e build: use env node for craco lint (#6311) 2023-04-10 10:05:22 -07:00
Zach Pomerantz
a078d94a38 chore: update pr template (#6314) 2023-04-10 10:05:06 -07:00
lynn
c9c3329bc3 fix: switch back buttons in mini portfolio (#6327)
switch back header
2023-04-10 12:57:31 -04:00
eddie
13d0b70fa8 fix: remove "Received Swap Quote" field from Connect Wallet event (#6316) 2023-04-10 09:18:07 -07:00
lynn
b852e4e64a feat: adding analytics for fiat on ramp buy button feature (#6272)
* init

* testing if it works

* wip

* tooltip still not working correctly

* modal still not triggered after initial buy click

* remove invalid import

* region check fixed

* add disabled buy button treatment

* simplify and fix toggle twice bug

* no more state mgmt bugs finally

* rename vars for clarity and add todos

* add feature flag, remove toast

* keep wallet drawer open upon repeated buy clicks

* remove from feature flag modal for now

* unused vars

* first round respond to tina comments

* respond to tina padding comments, fix padding in response to cal feedback

* last round tina comments

* init pending element names being added to analytics events repo

* update event names

* add tooltip delay requested by fred and cal

* middle of revisions, fiat buy flow readability wip

* hook logic refactor done + added basic unit test

* rename enum and add todo for unit tests

* mouseover tooltip disable properly

* fix mouseover tooltip not working, ensure dot working as expected, rename buyFiatClicked to buyFiatFlowCompleted

* change developer doc comment

* respond comments

* update snapshot test

* lint fix

* remove unnecessary changes
2023-04-07 13:02:07 -04:00
lynn
55bd3555be feat: Web 2996 add fiat on ramp buy flow to swap modal on the interface (#6240)
* init

* testing if it works

* wip

* tooltip still not working correctly

* modal still not triggered after initial buy click

* remove invalid import

* region check fixed

* add disabled buy button treatment

* simplify and fix toggle twice bug

* no more state mgmt bugs finally

* rename vars for clarity and add todos

* add feature flag, remove toast

* keep wallet drawer open upon repeated buy clicks

* remove from feature flag modal for now

* unused vars

* first round respond to tina comments

* respond to tina padding comments, fix padding in response to cal feedback

* last round tina comments

* add tooltip delay requested by fred and cal

* middle of revisions, fiat buy flow readability wip

* hook logic refactor done + added basic unit test

* rename enum and add todo for unit tests

* mouseover tooltip disable properly

* fix mouseover tooltip not working, ensure dot working as expected, rename buyFiatClicked to buyFiatFlowCompleted

* change developer doc comment

* respond comments

* update snapshot test

* comments

* small changes + unit tests

* dedup

* remove enzyme

* Remove unecessary line

* simplify

* more cleanup

* add missing await

* more comments

* more comment responses

* more comment responses

* delay show fixes and respond to comments

* fix logic for show

* remove tooltip delay, unit test changes

* Update src/components/Popover/index.tsx

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

* remove delay on tooltip

* missed one

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

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

* comments

* .

* lint error

---------

Co-authored-by: Zach Pomerantz <zzmp@uniswap.org>
Co-authored-by: Tina <59578595+tinaszheng@users.noreply.github.com>
2023-04-06 17:06:50 -04:00
Zach Pomerantz
972a65066c fix: do not double-report i18n exception (#6308) 2023-04-06 13:18:01 -07:00
eddie
39a212f762 fix: catch json parse error in fetchTokenList (#6278)
* fix: catch json parse error in fetchTokenList

* fix: refactor fetchTokenList and add more tests

* fix: import in test

* fix: comments and names

* fix: comment format

* fix: comment formatting
2023-04-06 11:15:39 -07:00
Jack Short
c362f4fe39 chore: removing unnecessary data in assets query (#6301)
* chore: removing unnecessary data in assets query

* no smallimageurl

* disabling nft drawer loading initially on nft pages

* removed too much

* renaming

* fix missing rank
2023-04-06 14:04:16 -04:00
eddie
271ef580e1 fix: make token list version bump error quieter (#6271)
* fix: use console.debug for expected transient error

* fix: add tests

* fix: name and lints
2023-04-06 10:26:56 -07:00
Zach Pomerantz
81ced4cb8b test(e2e): configure cypress test retries for CI (#6305)
test: configure retries for cypress CI
2023-04-06 09:30:07 -07:00
cartcrom
ab214a8133 fix: only use local txs for current account (#6284)
* fix: only use local txs for current account

* refactor: remove unecessary try/catch

* fix: add back try/catch
2023-04-06 11:43:39 -04:00
eddie
1b2d86ae3a feat: remove amplitude swap error logging (#6306) 2023-04-05 16:09:07 -07:00
Zach Pomerantz
40cac44e07 docs: use comments for pull_request_template (#6304)
* docs: use comments for pull_request_template

* docs: update
2023-04-05 09:12:38 -07:00
eddie
4e6d28cff4 feat: Update pull_request_template.md (#6302)
Update pull_request_template.md

add notes about testing mobile layouts to the test plan prompts
2023-04-04 14:57:47 -07:00
eddie
709fad0804 test: add unit test coverage to some redux state files (#6285) 2023-04-04 09:46:24 -07:00
Zach Pomerantz
573f4c873a fix: omit failed eth_blockNumber calls from sentry (#6267)
* build: upgrade sentry

* fix: omit failed eth_blockNumber calls from sentry

* test: beforeSend

* fix: bring to parity with #6281

* docs: type filterKnownErrors to beforeSend
2023-04-03 15:12:33 -07:00
eddie
d300db669f fix: z index issue with socks icon (#6295) 2023-04-03 15:12:22 -07:00
eddie
fb8217ddea fix: dont block trade when price impact is favorable (#6261)
* fix: dont block trade when price impact is favorable

* fix: add comment
2023-04-03 14:13:27 -07:00
Jordan Frankfurt
7b9a23d920 feat: reduce severity of phishing filter to allow url token names (#6282)
* feat: reduce severity of phishing filter to allow url token names

* tests

* remove unused var from test

* test rendering mini portfolio pools list

* update owner

* update variable names to match cmcewen's suggestions

* checkStringForURL -> hasURL
2023-03-31 12:59:02 -05:00
Connor McEwen
120ad935fa revert: "fix: mini portfolio layout fixes" (#6279)
Revert "fix: mini portfolio layout fixes (#6260)"

This reverts commit fb05439d32.
2023-03-30 18:47:42 -04:00
cartcrom
4eaf16b624 fix: injection detection bug (#6276)
* fix: use functions to check injection status rather than static vars

* fix: unnused field

* fix: don't prompt mm install for generics

* fix: generic injector function

* fix: display name for MM on cb browser

* fix: re-add ios mobile check for uniswap wallet

* fix: reword comment

* fix: refactor delayed-injection test

* feat: added comments

* fix: revert to minimal changes

* fix: update tests
2023-03-30 17:50:20 -04:00
Connor McEwen
857e2915ab fix: put environment in the wrong place (#6277)
* fix: put environment in the wrong place

* move to proper step
2023-03-30 16:54:45 -04:00
Connor McEwen
7410c81b42 chore: update workflow release env (#6275) 2023-03-30 15:43:18 -04:00
eddie
fb05439d32 fix: mini portfolio layout fixes (#6260)
* fix: mini portfolio layout fixes

* feat: refactor MP drawer CSS
2023-03-30 12:07:02 -07:00
eddie
fb7eade70b fix: l2 icon borders in MP (#6254)
* fix: l2 icon borders in MP

* fix: bool logic

* fix: comments and add test

* fix: change variable name

* fix: split l2 icon into two components
2023-03-30 11:59:46 -07:00
eddie
bd2b2c487a fix: close MP drawer on nft nav (#6251)
* fix: close MP drawer on nft nav

* fix: make callbacks optional, rename props

* fix: improve card API

* fix: add e2e test
2023-03-29 15:08:30 -07:00
eddie
2f004ed1d9 fix: remove deprecated default imports from zustand (#6270)
* fix: replace default imports from zustand

* fix: add eslint rule

* fix: typo

o
2023-03-29 13:25:30 -07:00
Zach Pomerantz
db257c73f2 fix: improve chain id error (#6266)
* build: upgrade sentry

* fix: improve chain id error
2023-03-29 12:38:33 -07:00
Zach Pomerantz
7c37b9d00e build: upgrade sentry (#6264)
* build: upgrade sentry

* chore: comment nits
2023-03-29 11:05:13 -07:00
Connor McEwen
7688c527f0 chore: update codecov yaml (#6262)
chore: set patch coverage to 80%, lower tolerance threshold, remove unused keys
2023-03-29 11:31:27 -04:00
eddie
06dd41a9cd fix: getCurrency crash when token not found (#6263)
* fix: getCurrency crash when token not found

* fix: comments and add test

* fix: remove extra whitespace in unit test

* fix: make e2e test pass
2023-03-28 18:44:48 -07:00
eddie
850fec40a9 fix: remove price tag from nft cards in the side drawer (#6252)
* fix: remove price tag from nft cards in the side drawer

* fix: decouple price from display logic

* fix: missing file chaneg

* fix: add tests
2023-03-28 09:26:32 -07:00
yyip-dev
3c7eabc3d8 docs: Update PR template (#6235)
* Update PR template

* Add screen capture section and simplify descriptions

* Link out to PR title conventions & remove monitoring section
2023-03-28 11:31:11 -04:00
Jordan Frankfurt
048607080c feat: add query param to disable the nft sections of the app (#6225)
* feat: add queryparam to disable the nft sections of the app

* fix

* include mini portfolio

* add comments explaining nft disable atom usage and suggesting future work

* add subtitle exception to landing page and correct the bool

* update comment

* comment syntax nits
2023-03-27 20:54:39 -05:00
eddie
a0f20c54d8 feat: implement new designs for tx notifs (#6232)
* feat: re-add transaction activity popups

* feat: implement new designs for tx notifs

* fix: address comments

* fix: remove color from alert icon

* fix: nits

* fix: remove null check

* fix: fix
2023-03-27 13:56:52 -07:00
Zoltan Arvai
da79abbc0d feat: adds Ledger Connect to non-MetaMask wallet list (#6257)
feat: add Ledger Connect to non-MetaMask wallet list
2023-03-27 16:16:30 -04:00
Vignesh Mohankumar
5ac08e1142 fix: link to lowercase chain name from position page (#6195)
* fix: link to lowercase chain name from position page

* fix: use existing const
2023-03-27 14:41:03 -04:00
eddie
d330eea375 feat: re-add transaction activity popups (#6223) 2023-03-27 10:07:41 -07:00
eddie
35dace7bfe feat: cypress test coverage reporting (#6212)
* wip

* fix: only instrument thru babel in test and dev

* fix: remove unused deps

* fix: yarn dedup

* fix: remove nyc_output dir from git

* fix: remove comment from index.html
2023-03-27 09:48:02 -07:00
Vignesh Mohankumar
8ce8e17f62 fix: Revert to previous Nav menu (#6258)
* fix: revert nav

* lint

* fix

* fix
2023-03-27 12:32:01 -04:00
Vignesh Mohankumar
eb105b6ec7 fix: align /pools CTAs to the top (#6253)
* fix: align /pools CTAs to the top

* connor comments

* Revert "connor comments"

This reverts commit e8f25e8fa1.
2023-03-24 18:33:28 -04:00
eddie
267e7de2b6 feat: remove unicon tooltip entirely (#6250) 2023-03-24 13:11:21 -07:00
lynn
ab9f2af054 fix: modify mini portfolio pools row event properties (#6248)
* init

* use memo
2023-03-24 15:49:38 -04:00
Connor McEwen
67b405dd42 chore: update codeowners (#6247) 2023-03-24 12:18:07 -04:00
lynn
281dbf4305 feat: remove tax service toast, modal, and menu item (#6244)
* init

* fix
2023-03-24 11:50:07 -04:00
lynn
369f2d7dfa fix: update tax copy again (#6241)
init
2023-03-23 20:29:37 -04:00
Jordan Frankfurt
803c749b13 fix: remove backend name check from badge flag (#6236)
* fix: remove backend name check from badge flag

* drop undefined check

* drop bool cast
2023-03-23 17:01:09 -05:00
lynn
8818dadf24 fix: update tax services copy (#6239)
init
2023-03-23 17:39:22 -04:00
Jordan Frankfurt
d179fc6b84 fix: don't show the add liquidity warning when no position currently … (#6238)
fix: don't show the add liquidity warning when no position currently exists
2023-03-23 16:18:35 -05:00
Jordan Frankfurt
18ec675c52 feat: warn when adding liquidity to an unowned LP position (#6219)
* feat: warn when adding liquidity to an unowned LP position

* update boolean name

* use a better address equivalence check
2023-03-23 12:09:22 -05:00
lynn
1a79bac893 feat: add additional mini portfolio events (#6233)
* add events

* update nft logging

* fix lint

* mikes comments
2023-03-23 12:34:59 -04:00
Jordan Frankfurt
bbf49b0477 feat: use new consolidated signing endpoint (#6222)
feat: use new consolidate signing endpoint
2023-03-22 21:41:17 -05:00
Jordan Frankfurt
7ee41abd99 feat: design refresh on pool list items (#6230)
* update LP tags

* design feedback
2023-03-22 20:48:31 -05:00
Jordan Frankfurt
581c0982db fix: add url name filtering to mini-portfolio (#6231)
* fix: add url name filtering to mini-portfolio

* Update src/components/WalletDropdown/MiniPortfolio/Pools/index.tsx

Co-authored-by: cartcrom <39385577+cartcrom@users.noreply.github.com>

* fix: remove unused deps (#6227)

* init

* remove one more useless dep

* bump up test size

* bump up test size

* up size

* fix lint and add str == undefined case to hasURL

---------

Co-authored-by: cartcrom <39385577+cartcrom@users.noreply.github.com>
Co-authored-by: lynn <41491154+lynnshaoyu@users.noreply.github.com>
2023-03-22 20:47:54 -05:00
eddie
478a05a34f fix: crash in nft listing flow (#6234)
* fix: crash in nft listing flow

* fix: rename test
2023-03-22 16:45:39 -07:00
lynn
5d6664019e fix: remove unused deps (#6227)
* init

* remove one more useless dep

* bump up test size

* bump up test size

* up size
2023-03-22 18:32:27 -04:00
Jordan Frankfurt
0cc187a7cb feat: add 1bps pool to arbitrum (#6224) 2023-03-22 17:01:33 -05:00
cartcrom
f3e09a90e2 fix: use correct chain var when switching (#6229) 2023-03-22 17:06:06 -04:00
Jordan Frankfurt
17db102484 fix: update url regex to correctly match URLs (#6220)
* fix: update url regex to correctly match URLs

* add test

* more test cases and better comment explanation
2023-03-22 14:05:27 -05:00
Jordan Frankfurt
e8ca84ee07 fix: update theme gold400 value (#6213)
fix: update theme gold200 value
2023-03-22 12:23:12 -05:00
Vignesh Mohankumar
dae7314aa9 feat: show error state when on invalid position (#6214) 2023-03-22 12:02:18 -04:00
cartcrom
32e6237624 feat: 🤑 Portfolios 🖼 (#6216)
* feat: squash mgtm differences w/ public repo into new base commit to fix rebase issues going forward

* feat: new settings menu (#85)

* feat: new settings flow

* feat: add statsig geo gate for MGTM assets (#91)

* init
* feat: geo-gate mgtm features & wallet tab

* feat: new theme toggle (#86)

* feat: new theme toggle

* fix: import

* refactor: polish

* refactor: use enum instead of string union

* feat: mini portfolio tabs (#88)

* feat: mini portfolio tabs

* feat: feature flag

* feat: portfolio query (#89)

* feat: portfolio balance query

* polish

* fix: added todo for api key

* feat: tokens mini-portfolio tab (#93)

* feat: tokens tab

* fix: lint

* fix: pr comment polish

* fix: snapshot update

* feat: common portfolio row component (#99)

* feat: porfolio row component

* fix: updated layout

* feat: update token row design (#100)

* feat: porfolio row component

* fix: updated layout

* fix: updated tokens tab to latest design

* fix: unnused export

* feat: dropdown drawer (#95)

* inital drawer

* feat: animated drawer

* fix: attempt animation perf fix

* fix: lint

* feat: better animations

* fix: scrim

* refactor: const name

* test: update chain switcher test

* test: update chain switcher test id

* feat: Add NFT tab for mini porftolio (#104)

* add NFT tab

* add min width to verified icon size

* add keyArgs to nftBalances query so that different callers dont override query cache

* revert yarn node changes

* use flex shrink

* move styled components to top

* navigate to nft page

* update snapshot test after adding gap to Row

* Update src/components/WalletDropdown/MiniPortfolio/NFT.tsx

Co-authored-by: cartcrom <39385577+cartcrom@users.noreply.github.com>

---------

Co-authored-by: cartcrom <39385577+cartcrom@users.noreply.github.com>

* fix scroll behavior (#105)

* feat: portfolio loading state (#106)

* feat: loading state for token row

* fix: revert hardcoded loading states

* fix: removed unused component

* feat: activity feed tab (#103)

* init

* feat: ens avatars

* feat: etherscan api experiment

* fix: ignore error policy

* polish

* fix: query pageSize

* fix: revert redux change

* fix: small polish item

* fix: pr comments

* fix: translated activity titles

* fix: typing and todo comment

* todo comment

* fix: accidental chain name mismatch

* feat: remove dropdown chevron (#110)

* feat: small drawer UI updates (#109)

* feat: small changes

* fix: resize status icon w/ ens

* fix: commented css

* feat: activity loading state (#108)

* feat: activity loading state

* fix: unused wrapper

* fix: activity/nftbalance cache overlap

* fix: mp sidebar width adjustment (#112)

* fix: mp sidebar width adjustment

* fix: navbar breakpoints

* feat: nav bar MenuDropdown updates (#107)

* feat: nav bar MenuDropdown updates

* fix: pool breakpoints

* fix: pool in menu dropdown

* fix: lints

* fix: tests

* feat: swap click updates (#111)

* feat: swap click updates

* fix: updates

* fix: simplify

* fix: snapshots

* fix: snapshots

* feat: collapse button (#113)

* feat: collapse button

* fix: esc keypress and animation

* fix: MP scrollbar (#115)

* feat: hide small balances in token list (#116)

* feat: collapse button (#113)

* feat: collapse button

* fix: esc keypress and animation

* chore: merge

* fix: updates

* feat: show the hidden tokens at the bottom

* feat: empty balance state for tokens (#118)

* feat: empty balance state for tokens

* fix: balance change check

* fix: token inputs on TDP widget (#120)

* feat: remove quick swap button (#125)

* fix: nft activity descriptor (#119)

* fix: improper substring usage error

* pr comments + small fix

* feat: add mini portfolio events (#126)

* add events

* fix failing snapshot test

* incorporate eddie comment

* fix chain switching (#127)

* feat: pre-parse activity to catch errors (#129)

* feat: update wallet option icons (#128)

* feat: update wallet option icons

* feat: theme-aware injected logo, svg logos

* fix: rabby extension

* feat: uni icons avatars (#130)

* feat: update wallet option icons

* feat: theme-aware injected logo, svg logos

* fix: rabby extension

* feat: update avatar / icon logic

* fix: remove testing hardcoded socks balance

* fix: add isTrustWallet check

* feat: pools tab (#122)

* init

* feat: working cross-chain calls

* feat: inline range text

* feat: better multicall perf

* feat: loading state

* feat: pools persist between mounts

* feat: polish

* fix: small refactors

* remove stuff to split into sep pr

* fix: remove comment

* fix: judo PR comments

* fix: currencyKey case

* fix: eddie's comment

* fix (#134)

* feat: removed microsite content, updated responsiveness for wallet tab (#137)

* fix: injector unit tests (#136)

* fix: injector unit tests

* fix: lint

---------

Co-authored-by: cartcrom <cartergcromer@gmail.com>

* feat: toggle closed positions (#138)

* rename file

* refactored hidden row to use for closed positions

* fix: remove unnused atom

* fix: lint

* feat: MP tab empty states (#132)

* feat: squash mgtm differences w/ public repo into new base commit to fix rebase issues going forward

* feat: new settings menu (#85)

* feat: new settings flow

* feat: add statsig geo gate for MGTM assets (#91)

* init
* feat: geo-gate mgtm features & wallet tab

* feat: new theme toggle (#86)

* feat: new theme toggle

* fix: import

* refactor: polish

* refactor: use enum instead of string union

* feat: mini portfolio tabs (#88)

* feat: mini portfolio tabs

* feat: feature flag

* feat: portfolio query (#89)

* feat: portfolio balance query

* polish

* fix: added todo for api key

* feat: tokens mini-portfolio tab (#93)

* feat: tokens tab

* fix: lint

* fix: pr comment polish

* fix: snapshot update

* feat: common portfolio row component (#99)

* feat: porfolio row component

* fix: updated layout

* feat: update token row design (#100)

* feat: porfolio row component

* fix: updated layout

* fix: updated tokens tab to latest design

* fix: unnused export

* feat: dropdown drawer (#95)

* inital drawer

* feat: animated drawer

* fix: attempt animation perf fix

* fix: lint

* feat: better animations

* fix: scrim

* refactor: const name

* test: update chain switcher test

* test: update chain switcher test id

* feat: Add NFT tab for mini porftolio (#104)

* add NFT tab

* add min width to verified icon size

* add keyArgs to nftBalances query so that different callers dont override query cache

* revert yarn node changes

* use flex shrink

* move styled components to top

* navigate to nft page

* update snapshot test after adding gap to Row

* Update src/components/WalletDropdown/MiniPortfolio/NFT.tsx

Co-authored-by: cartcrom <39385577+cartcrom@users.noreply.github.com>

---------

Co-authored-by: cartcrom <39385577+cartcrom@users.noreply.github.com>

* fix scroll behavior (#105)

* feat: portfolio loading state (#106)

* feat: loading state for token row

* fix: revert hardcoded loading states

* fix: removed unused component

* feat: activity feed tab (#103)

* init

* feat: ens avatars

* feat: etherscan api experiment

* fix: ignore error policy

* polish

* fix: query pageSize

* fix: revert redux change

* fix: small polish item

* fix: pr comments

* fix: translated activity titles

* fix: typing and todo comment

* todo comment

* fix: accidental chain name mismatch

* feat: remove dropdown chevron (#110)

* feat: small drawer UI updates (#109)

* feat: small changes

* fix: resize status icon w/ ens

* fix: commented css

* init

* feat: working cross-chain calls

* feat: inline range text

* feat: better multicall perf

* feat: activity loading state (#108)

* feat: activity loading state

* fix: unused wrapper

* feat: loading state

* feat: pools persist between mounts

* fix: activity/nftbalance cache overlap

* fix: mp sidebar width adjustment (#112)

* fix: mp sidebar width adjustment

* fix: navbar breakpoints

* feat: nav bar MenuDropdown updates (#107)

* feat: nav bar MenuDropdown updates

* fix: pool breakpoints

* fix: pool in menu dropdown

* fix: lints

* fix: tests

* feat: swap click updates (#111)

* feat: swap click updates

* fix: updates

* fix: simplify

* fix: snapshots

* fix: snapshots

* feat: collapse button (#113)

* feat: collapse button

* fix: esc keypress and animation

* fix: MP scrollbar (#115)

* feat: hide small balances in token list (#116)

* feat: collapse button (#113)

* feat: collapse button

* fix: esc keypress and animation

* chore: merge

* fix: updates

* feat: show the hidden tokens at the bottom

* feat: empty balance state for tokens (#118)

* feat: empty balance state for tokens

* fix: balance change check

* fix: token inputs on TDP widget (#120)

* feat: polish

* fix: small refactors

* remove stuff to split into sep pr

* fix: remove comment

* feat: remove quick swap button (#125)

* fix: nft activity descriptor (#119)

* fix: improper substring usage error

* pr comments + small fix

* feat: add mini portfolio events (#126)

* add events

* fix failing snapshot test

* incorporate eddie comment

* fix chain switching (#127)

* feat: pre-parse activity to catch errors (#129)

* fix: activity/nftbalance cache overlap

* fix: mp sidebar width adjustment (#112)

* fix: mp sidebar width adjustment

* fix: navbar breakpoints

* feat: nav bar MenuDropdown updates (#107)

* feat: nav bar MenuDropdown updates

* fix: pool breakpoints

* fix: pool in menu dropdown

* fix: lints

* fix: tests

* feat: swap click updates (#111)

* feat: swap click updates

* fix: updates

* fix: simplify

* fix: snapshots

* fix: snapshots

* feat: collapse button (#113)

* feat: collapse button

* fix: esc keypress and animation

* fix: MP scrollbar (#115)

* feat: hide small balances in token list (#116)

* feat: collapse button (#113)

* feat: collapse button

* fix: esc keypress and animation

* chore: merge

* fix: updates

* feat: show the hidden tokens at the bottom

* feat: empty balance state for tokens (#118)

* feat: empty balance state for tokens

* fix: balance change check

* fix: token inputs on TDP widget (#120)

* feat: remove quick swap button (#125)

* fix: nft activity descriptor (#119)

* fix: improper substring usage error

* pr comments + small fix

* feat: add mini portfolio events (#126)

* add events

* fix failing snapshot test

* incorporate eddie comment

* fix chain switching (#127)

* feat: pre-parse activity to catch errors (#129)

* feat: start empty state updates

* feat: update wallet option icons (#128)

* feat: update wallet option icons

* feat: theme-aware injected logo, svg logos

* fix: rabby extension

* feat: empty wallet states w/ good positioning

* feat: finish empty states w/ theme aware icons

* fix: judo PR comments

* fix: currencyKey case

* feat: uni icons avatars (#130)

* feat: update wallet option icons

* feat: theme-aware injected logo, svg logos

* fix: rabby extension

* feat: update avatar / icon logic

* fix: remove testing hardcoded socks balance

* fix: add isTrustWallet check

* fix: eddie's comment

* feat: pools tab (#122)

* init

* feat: working cross-chain calls

* feat: inline range text

* feat: better multicall perf

* feat: loading state

* feat: pools persist between mounts

* feat: polish

* fix: small refactors

* remove stuff to split into sep pr

* fix: remove comment

* fix: judo PR comments

* fix: currencyKey case

* fix: eddie's comment

* fix (#134)

* feat: removed microsite content, updated responsiveness for wallet tab (#137)

* fix: injector unit tests (#136)

* fix: injector unit tests

* fix: lint

---------

Co-authored-by: cartcrom <cartergcromer@gmail.com>

* fix: pool loading state

* fix: simplify pools loading state

* fix: removed unused var

* feat: toggle closed positions (#138)

* rename file

* refactored hidden row to use for closed positions

* fix: remove unnused atom

* fix: lint

* fix: dry

* fix: lint

* fix: address review comments

* fix: lints

* fix: bad merge

---------

Co-authored-by: cartcrom <cartergcromer@gmail.com>
Co-authored-by: cartcrom <39385577+cartcrom@users.noreply.github.com>
Co-authored-by: Tina <59578595+tinaszheng@users.noreply.github.com>
Co-authored-by: lynn <41491154+lynnshaoyu@users.noreply.github.com>

* make sure mp is above tax service banner (#142)

* feat: add git version number to settings menu (#141)

* fix: remove unicon tooltip (#146)

* feat: remove unicon tooltip from mobile and timer prop

* fix: flip bool logic

* fix: small nits for mp from fred (#150)

* fixes

* fix

* feat: Local tx activity (#148)

* local swap working

* feat: cross-chain

* fix: revert query changes

* feat: local approvals

* feat: wrapped activity

* feat: local lp tx history

* fix: add doc comment

* fix: linted

* fix: no pools render error (#152)

* feat: mp activity feed design tweaks  (#145)

* feat: squash mgtm differences w/ public repo into new base commit to fix rebase issues going forward

* feat: new settings menu (#85)

* feat: new settings flow

* feat: add statsig geo gate for MGTM assets (#91)

* init
* feat: geo-gate mgtm features & wallet tab

* feat: new theme toggle (#86)

* feat: new theme toggle

* fix: import

* refactor: polish

* refactor: use enum instead of string union

* feat: mini portfolio tabs (#88)

* feat: mini portfolio tabs

* feat: feature flag

* feat: portfolio query (#89)

* feat: portfolio balance query

* polish

* fix: added todo for api key

* feat: tokens mini-portfolio tab (#93)

* feat: tokens tab

* fix: lint

* fix: pr comment polish

* fix: snapshot update

* feat: common portfolio row component (#99)

* feat: porfolio row component

* fix: updated layout

* feat: update token row design (#100)

* feat: porfolio row component

* fix: updated layout

* fix: updated tokens tab to latest design

* fix: unnused export

* feat: dropdown drawer (#95)

* inital drawer

* feat: animated drawer

* fix: attempt animation perf fix

* fix: lint

* feat: better animations

* fix: scrim

* refactor: const name

* test: update chain switcher test

* test: update chain switcher test id

* feat: Add NFT tab for mini porftolio (#104)

* add NFT tab

* add min width to verified icon size

* add keyArgs to nftBalances query so that different callers dont override query cache

* revert yarn node changes

* use flex shrink

* move styled components to top

* navigate to nft page

* update snapshot test after adding gap to Row

* Update src/components/WalletDropdown/MiniPortfolio/NFT.tsx

Co-authored-by: cartcrom <39385577+cartcrom@users.noreply.github.com>

---------

Co-authored-by: cartcrom <39385577+cartcrom@users.noreply.github.com>

* fix scroll behavior (#105)

* feat: portfolio loading state (#106)

* feat: loading state for token row

* fix: revert hardcoded loading states

* fix: removed unused component

* feat: activity feed tab (#103)

* init

* feat: ens avatars

* feat: etherscan api experiment

* fix: ignore error policy

* polish

* fix: query pageSize

* fix: revert redux change

* fix: small polish item

* fix: pr comments

* fix: translated activity titles

* fix: typing and todo comment

* todo comment

* fix: accidental chain name mismatch

* feat: remove dropdown chevron (#110)

* feat: small drawer UI updates (#109)

* feat: small changes

* fix: resize status icon w/ ens

* fix: commented css

* feat: activity loading state (#108)

* feat: activity loading state

* fix: unused wrapper

* fix: activity/nftbalance cache overlap

* fix: mp sidebar width adjustment (#112)

* fix: mp sidebar width adjustment

* fix: navbar breakpoints

* feat: nav bar MenuDropdown updates (#107)

* feat: nav bar MenuDropdown updates

* fix: pool breakpoints

* fix: pool in menu dropdown

* fix: lints

* fix: tests

* feat: swap click updates (#111)

* feat: swap click updates

* fix: updates

* fix: simplify

* fix: snapshots

* fix: snapshots

* feat: collapse button (#113)

* feat: collapse button

* fix: esc keypress and animation

* fix: MP scrollbar (#115)

* feat: hide small balances in token list (#116)

* feat: collapse button (#113)

* feat: collapse button

* fix: esc keypress and animation

* chore: merge

* fix: updates

* feat: show the hidden tokens at the bottom

* feat: empty balance state for tokens (#118)

* feat: empty balance state for tokens

* fix: balance change check

* fix: token inputs on TDP widget (#120)

* feat: remove quick swap button (#125)

* fix: nft activity descriptor (#119)

* fix: improper substring usage error

* pr comments + small fix

* feat: add mini portfolio events (#126)

* add events

* fix failing snapshot test

* incorporate eddie comment

* fix chain switching (#127)

* feat: pre-parse activity to catch errors (#129)

* feat: update wallet option icons (#128)

* feat: update wallet option icons

* feat: theme-aware injected logo, svg logos

* fix: rabby extension

* wip

* feat: uni icons avatars (#130)

* feat: update wallet option icons

* feat: theme-aware injected logo, svg logos

* fix: rabby extension

* feat: update avatar / icon logic

* fix: remove testing hardcoded socks balance

* fix: add isTrustWallet check

* feat: pools tab (#122)

* init

* feat: working cross-chain calls

* feat: inline range text

* feat: better multicall perf

* feat: loading state

* feat: pools persist between mounts

* feat: polish

* fix: small refactors

* remove stuff to split into sep pr

* fix: remove comment

* fix: judo PR comments

* fix: currencyKey case

* fix: eddie's comment

* fix (#134)

* wip with activity status icons

* feat: removed microsite content, updated responsiveness for wallet tab (#137)

* fix: injector unit tests (#136)

* fix: injector unit tests

* fix: lint

---------

Co-authored-by: cartcrom <cartergcromer@gmail.com>

* temp

* in progress

* text, and activity status changes working. missing logo changes

* fix lint issues

* refactor: square logo location

* feat: merge other activity changes

---------

Co-authored-by: cartcrom <cartergcromer@gmail.com>
Co-authored-by: cartcrom <39385577+cartcrom@users.noreply.github.com>
Co-authored-by: Tina <59578595+tinaszheng@users.noreply.github.com>
Co-authored-by: eddie <66155195+just-toby@users.noreply.github.com>

* feat: portfolio polling/refetching/performance (#154)

* init

* feat: implement asset polling & tx updating balances

* add refetching to activity tab

* fix: re-add error policy

* fix: add TODO

* fix: fix scroll + console warnings on mini portfolio nfts (#155)

* fixes

* rename

* init (#158)

* fix: browser wallet icons (#156)

* fix: moved border radius css (#157)

* feat: token details state for BNB (#151)

* fix: update token details missing flow

* refactor

* fix: lint

* fix: add bnb to queries

* feat: add comment explaining unsupported chains

* fix: remove buy crypto animation (#160)

* fix: price display pools tab (#159)

* fix: price display pools tab

* fix: lint

* feat: pools tab performance (#123)

* init

* fix: pr comments

* update cache return type

* refactor: rename type

* fix: further pr comments

* fix: remove stringify

* refactor: readability and caching

* fix: zach pr comments

* fix: removed hardcoded value

* feat: catch position errors for chains

* fix: add todo comment for followup ticket

* fix: build issue from merge conflict

* refactor: split up token caching function

* feat: separate array slicing into util with tests

* feat: close wallet drawer on wallet connection (#161)

* feat: close wallet drawer on wallet connection

* feat: added comment explaining fetchPolicy

* fix: only close if open

* refactor: add comment about ref

* fix: revert change to useAllTokens & rename with more descriptive name (#163)

* fix: square Arbitrum logo design changes  (#162)

* feat: close wallet drawer on wallet connection

* feat: added comment explaining fetchPolicy

* fix: only close if open

* init

* fixes

* fix border radius

---------

Co-authored-by: cartcrom <cartergcromer@gmail.com>

* fix: token loading state (#165)

* fix: remove unnused code and comments

* fix: privacy policy date

* fix: revert readme change

* fix: remove unnused FOR file

* fix: missed query id

* fix: add id to portfolios query

* fix: widget cypress test

---------

Co-authored-by: Tina <59578595+tinaszheng@users.noreply.github.com>
Co-authored-by: eddie <66155195+just-toby@users.noreply.github.com>
Co-authored-by: lynn <41491154+lynnshaoyu@users.noreply.github.com>
2023-03-22 11:29:26 -04:00
Jordan Frankfurt
cee19aebe7 fix: improve fetch-schema logging and platform support (#6191)
* fix: improve fetch-schema logging and platform support

* don't use async/await because linter doesn't like it
2023-03-22 10:14:07 -05:00
Vignesh Mohankumar
b2fbba13a2 fix: set height on vote and v2 liquidity buttons (#6217) 2023-03-22 00:11:01 -04:00
github-actions[bot]
a15a6108c1 chore(i18n): new Crowdin translations (#6196)
* chore(i18n): synchronize translations from crowdin [skip ci]

* merge in translations

---------

Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
Co-authored-by: Yannie Yip <yannie.yip@uniswap.org>
2023-03-21 20:01:47 -04:00
eddie
46724cd8f6 feat: preserve input currency on TDP when navigating (#6209)
* fix: NPE when connector is undefined

* feat: retain input token when switching Token Detail Page

* fix: remove logic from string template
2023-03-21 16:31:28 -07:00
Jordan Frankfurt
09e6d38f25 feat: bnb network notif badge in search (#6210)
* feat: bnb network notif badge in search

* add chainname check
2023-03-21 17:08:11 -05:00
Jordan Frankfurt
236a4dc145 feat: add tax service discount to help menu (#6211) 2023-03-21 17:07:57 -05:00
eddie
991d07ef39 fix: use weth in e2e test (#6207) 2023-03-21 09:50:02 -07:00
Jordan Frankfurt
7f5707e551 feat: add BNB Chain placeholder to token explore network menu (#6198)
* feat: add BNB Chain placeholder to token explore network menu

* don't make badge .6 opacity
2023-03-21 12:18:06 -04:00
lynn
4050b1b241 fix: dismiss tax service toast after 1 dismissal instead of 3 (#6200)
* init

* add todo
2023-03-20 18:26:47 -05:00
Charles Bachmeier
a19aa303d9 feat: list 1155s (#6193)
* undisable 1155s and add nftStandard to CollectionRow

* working OS 1155 listing

* amend OS listing to handle 0 creator fee

* handle no royalties set

* disable already listed protection for 1155s

* can list to LR

* stuck on x2

* working x2y2

* remove comment

* add listing issue finding to helper fn

---------

Co-authored-by: Charles Bachmeier <charlie@genie.xyz>
2023-03-20 12:31:56 -07:00
dependabot[bot]
c7f67437fd build(deps): bump @uniswap/widgets from 2.48.5 to 2.49.0 (#6189)
Bumps [@uniswap/widgets](https://github.com/Uniswap/widgets) from 2.48.5 to 2.49.0.
- [Release notes](https://github.com/Uniswap/widgets/releases)
- [Changelog](https://github.com/Uniswap/widgets/blob/main/.releaserc.json)
- [Commits](https://github.com/Uniswap/widgets/compare/v2.48.5...v2.49.0)

---
updated-dependencies:
- dependency-name: "@uniswap/widgets"
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-03-20 10:03:38 -07:00
Jordan Frankfurt
b5192a2b7d feat: update "Browser wallet" icons (#6194)
feat: update Browser wallet icons
2023-03-20 11:58:54 -05:00
Tina
cd92e4cf6f fix: Revert "feat: trace jsonrpc (#6159)" (#6190)
Revert "feat: trace jsonrpc (#6159)"

This reverts commit 6fee37c4b8.
2023-03-20 11:47:13 -04:00
Zach Pomerantz
089fba5dee test: parallelize cypress (again) with a consistent container (#6182)
* Revert "test: run cypress on one machine (#6181)"

This reverts commit a2812fcf79.

* test: use consistent container across cypress-test-matrix

* test: condition cypress-tests on cypress-test-matrix

* test: check cypress-test-matrix

* debug: cypress-test-matrix.result

* test: fix error

* test: fix error

* test: fix error

* test: fix error

* test: fix error

* test: fix error
2023-03-17 13:09:20 -07:00
cartcrom
503a33314f fix: use proper apollo client for ticks query (#6185)
fix: pass thegraph client to ticks query instead of default
2023-03-17 14:29:46 -04:00
Zach Pomerantz
6fee37c4b8 feat: trace jsonrpc (#6159)
* feat: maybeTrace

* feat: maybeTrace jsonrpc

* docs: maybeTrace

* test: fix test typing

* fix: pr feedback
2023-03-16 22:16:41 -07:00
Zach Pomerantz
a2812fcf79 test: run cypress on one machine (#6181)
* test: run cypress on one machine

* build: turn off parallel cypress
2023-03-16 21:10:36 -07:00
Vignesh Mohankumar
c42aeae96a fix: update useIsPoolsPage hook (#6179)
* fix: update `useIsPoolsPage` hook

* rename file
2023-03-16 19:36:07 -04:00
Vignesh Mohankumar
80ab000d34 fix: back button should lead to /pools not /pools/v2 (#6171) 2023-03-16 18:47:28 -04:00
eddie
81206f1eef test: widget integration tests (#6145)
* feat: upgrade widget

* test: swap widget integration tests

* test: handle conditional token safety warning

* fix: yarn dedup

* fix: try reformatting cy commands

* test: try waiting for page to load

* fix: update test

* fix: update test

* fix: test
2023-03-16 15:19:02 -07:00
Vignesh Mohankumar
568b05fda1 feat: support both /pool and /pools (#6173)
* more

* change locally
2023-03-16 18:08:10 -04:00
Vignesh Mohankumar
3d0ca21036 fix: remove Create a pool menu option (#6168) 2023-03-16 18:01:54 -04:00
Vignesh Mohankumar
8b743615d1 fix: link to providing liquidity help doc (#6169) 2023-03-16 18:01:46 -04:00
eddie
9e4fdabc34 feat: upgrade widget (#6176)
* feat: upgrade widget

* fix: yarn dedup
2023-03-16 14:05:52 -07:00
lynn
decb922d4b fix: handle undefined tax service banner counter in state (#6178)
* fix

* use ternary

* update
2023-03-16 16:42:43 -04:00
Vignesh Mohankumar
ac50555647 fix: equal width for CTA tiles on /pool (#6170) 2023-03-16 16:07:17 -04:00
Vignesh Mohankumar
f48356d0fb feat: link to token details from /pool/{} (#6162)
* feat: link to token details from /pool/{}

* use backend chain names

* use fn
2023-03-16 15:36:55 -04:00
Jack Short
a362f8797a chore: refactoring bag (#6039)
* chore: refactoring bag component to sub components (#6027)

* moving totalEthPrice to hook

* moving everything from bag to bag footer

* moving transaction state to sep hook

* explicit type

* itemsInBag

* fixing transaction tracking

* chore: refactor useFetchAssets to make it more readable (#6043)

* chore: refactoring useFetchAssets to make it more readable

* remvoing eslint stuff

* extracting what can be

* comments

* removing feature flag

* changing return type of useUsd hook

* zustand shallow
2023-03-16 14:39:50 -04:00
Jack Short
783f42abcc fix: cards floating point error (#6174) 2023-03-16 14:39:24 -04:00
Tina
0923cf4ac9 fix: Auto-slippage logic (#6167)
* range auto slippage from [.1% to 5%] and multiply gas by 110% instead of 10%

* remove newline

* no multiplier on gas, add comments

* consolidate constant variables
2023-03-16 12:46:59 -04:00
cartcrom
801958d0ae fix: skip when address undefined (#6172)
* fix: skip when address undefined

* fix: add back lowercase

* fix: use ms

* update loading var name
2023-03-16 09:33:47 -07:00
Zach Pomerantz
8392c29a1e feat: omit unnecessary sentry logs (#6166)
* fix: omit gracefully handled events from sentry

* fix: log locale load exception to sentry
2023-03-16 09:25:50 -07:00
Charles Bachmeier
8d36edf2b7 chore: remove no longer used anayltics query (#6165)
* chore: remove no longer used anayltics query

* Remove from index

---------

Co-authored-by: Charles Bachmeier <charlie@genie.xyz>
2023-03-15 18:30:28 -06:00
Charles Bachmeier
0f8d3fa506 fix: add more null checks for trending collections gql query (#6164)
add more null checks for trending collections gql query

Co-authored-by: Charles Bachmeier <charlie@genie.xyz>
2023-03-15 18:30:01 -06:00
Zach Pomerantz
5f64149f39 feat: trace quote (#6160)
* feat: trace quote

* fix: include more data
2023-03-15 17:09:22 -07:00
Zach Pomerantz
7115729e3e feat: trace swap.send (#6147)
* feat: trace swap.send

* docs: comments

* test: sentry transaction trace

* fix: tag as non-widget

* fix: nits

* refactor: brackets

* fix: type TraceTags

* docs: traceTransaction

* chore: transaction->span

* docs: even more docs

* fix: is_widget
2023-03-15 16:39:04 -07:00
Zach Pomerantz
6618135e7d feat: enable sentry via .env.production (#6163)
* build: alphabetize

* fix: rm SENTRY_ENABLED as it defaults to false

* feat: enable sentry via .env.production

* test: do not send events from e2e tests

* fix: negation
2023-03-15 11:59:48 -07:00
Jordan Frankfurt
d3085c1f3c feat: Add BNB Network support (#6036)
* Support BNB Chain

* Update Interface with BNB Chain

* use SupportedChainId from sdk

* yarn-deduplicate

* add rpc urls

* add support for arbitrum goerli

* add arbitrum_goerli to jsonrpc list

* remove eslint disables

* remove non-existent gql gen Chain type

* fix import issues

* fix test

* Update src/utils/getExplorerLink.ts

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

* add quicknode rpc as primary

* bump universal router sdk to support bnb chain

* add correct network name for asset repo indexing

* add v3 smart order router

* yarn-deduplicate --strategy=highest

* let ethersproject stuff update

* remove safety test that is no longer relevant

* update network info link

* fix block explorer stuff, fix light theme

* add subgraph support

* update quicknode rpc

* update the wrapped BNB token symbol to WBNB

* update useStablecoinPrice api to support loading states

* update useStablecoinPrice api to support loading states

* Revert "update useStablecoinPrice api to support loading states"

This reverts commit c1fa6297d0.

* Revert "update useStablecoinPrice api to support loading states"

This reverts commit 2e703552b5.

* fix copy and quote token for non-eth networks

* add coingecko bnb list

* fix: design treatment from @infredible for bnb support (#6052)

merging in current state--can merge again when the rest is ready

* fix info site links

* fix: don't show a price impact warning if the user is (un)wrapping

* finish switching to chains/constants

* fix chainId test

* fred's bnb background colors

* pr feedback from zzmp

* update sdk-core

* remove unused image

* update bnb chain asset url extension

* new addresses

* fix test and patch sdk-core version to use BNB instead of BSC

* checking in this weird schema file--maybe a mistake?

* remove temp file

* fix bases

* fix network bridge alert text color

* fix: widget defaultToken resetting on TDP

---------

Co-authored-by: ILIA.eth <4621066+ilyamk@users.noreply.github.com>
Co-authored-by: Tina <59578595+tinaszheng@users.noreply.github.com>
Co-authored-by: lynn <41491154+lynnshaoyu@users.noreply.github.com>
Co-authored-by: Tina Zheng <tina.s.zheng+github@gmail.com>
Co-authored-by: Eddie Dugan <eddie.dugan@uniswap.org>
2023-03-15 09:00:09 -05:00
Vignesh Mohankumar
813aeda012 feat: use tx formatting for PositionPage cards (#6142)
* tooltip

* add numberType

* try again

* eslint

* one more
2023-03-14 21:57:13 -04:00
Tina
13d3e75ada fix: Eth price fetch cache miss and use default fetch policy to remove extra network requests (#6157) 2023-03-14 20:43:08 -04:00
github-actions[bot]
8c55c70a1e chore(i18n): new Crowdin translations (#6000)
* chore(i18n): synchronize translations from crowdin [skip ci]

* merge in translations

---------

Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
Co-authored-by: Yannie Yip <yannie.yip@uniswap.org>
2023-03-14 18:25:40 -04:00
Charles Bachmeier
18939aa871 feat: Migrate NFT Activity to GraphQL (#6103)
* add new activity query

* add nftactivity hook and fix query

* converted activity type to accept optionals, hook should return activity type

* single asset activity

* relaystylepagination

* working on local endpoint

* working timestamps

* updated endpoint

* use correct env var

* undo asset testing

* forgot last

* undo more testing

* don't break on null address

* working loading state

* better loading states

* handle marketplace type update

* remove types

* properly format price and reduce redundancy

* handle null price

---------

Co-authored-by: Charles Bachmeier <charlie@genie.xyz>
2023-03-14 14:07:32 -07:00
Tina
bc251230da fix: Remove block number check for quotes (#6148)
* remove block number check

* add refetchOnMountOrArgChange
2023-03-14 16:30:31 -04:00
Charles Bachmeier
b0aea5f62a feat: add infringing collection to blocklist (#6151)
add infringing collection to blocklist

Co-authored-by: Charles Bachmeier <charlie@genie.xyz>
2023-03-14 13:06:38 -07:00
Jack Short
b963d3b27b feat: cards v2 (#6048)
* initial setup

* forgot border

* cta

* rough draft

* adding marketplace container and selector

* removing last sale

* details link

* marketplace icons

* removing hovered

* adding icons

* removing unused exports

* fixing hover

* not linking to details

* linting

* mobile

* moving cards to component

* linting

* profile cards

* deleting cards

* fixing imports

* actually fixing imports

* tryingn to fix linting errors

* disabling module export for this file

* fixing build

* seems to hate uppercase C

* fixing tests

* passing data-testid correctly

* tertiary info

* removing the extra times

* button states

* adjusting tertiary

* pointer-events

* border animation

* variance bug

* cryptopunks

* unavailable for listing

* disabled cta

* set heihgt

* animated slide up

* animation

* badge changes

* shadows

* ran yarn

* removing eslint comment

* removing types and hooks

* removing from cache

* small tweaks

* removing unused tertiary info

* initial comment addressing

* more comments

* translations

* refactoring file structure

* removing trans tag

* reverting to what it prev was

* text-shadow

* eslint ignore

* updating size test
2023-03-14 15:38:39 -04:00
Charles Bachmeier
799edfb493 fix: don't fire off a graphql query on an empty search (#6152)
don't fire off a graphql query on an empty search

Co-authored-by: Charles Bachmeier <charlie@genie.xyz>
2023-03-14 12:38:13 -07:00
Jordan Frankfurt
1e25436421 feat: injected -> browser wallet w/ icon updates (#6155)
* feat: injected -> browser wallet w/ icon updates

* update tests
2023-03-14 14:08:49 -05:00
Zach Pomerantz
ee2dc1ee17 feat: pageload performance tracing (#6146)
* build: add @sentry/tracing

* feat: turn on pageload tracing

* feat: simple trace abstractions

* fix: set sample rate in .env

* fix: rm unused trace

* fix: bump size for new deps

* fix: unnecessary line
2023-03-14 11:39:58 -07:00
cartcrom
4bdef2b601 fix: pricing sources (#6156)
* Revert "fix: "revert feat: Use ETH based pricing instead of USDC based pricing for quote USD values (#6132) (#6150)"

This reverts commit 645d92185a.

* Revert "fix: Use non-subgraph fields for calculating USD prices on explore and token details pages (#6134)"

This reverts commit 5c8b45c8e5.
2023-03-14 14:37:58 -04:00
eddie
acc6b0a740 fix: reset to defaultTokens when chain changes (#6154) 2023-03-14 11:18:50 -07:00
Jordan Frankfurt
645d92185a fix: "revert feat: Use ETH based pricing instead of USDC based pricing for quote USD values (#6132) (#6150)
* Revert "feat: Use ETH based pricing instead of USDC based pricing for quote USD values (#6132)"

This reverts commit a8864614c1.

* revert isLoading api

* fix lint errors

* fix loading state
2023-03-14 13:44:44 -04:00
lynn
145c96caa6 fix: tax promo banner updates (#6144)
* init

* update unit test

* fix hover states, fix landing pg behavior

* wip

* fix hook

* rename tax service hook

* fix typo

* remove left and right borders on mobile

* fix height of inner container on mobile
2023-03-14 11:50:43 -04:00
cartcrom
c3ae545b68 fix: fetch same tokens for topTokens and sparkline queries (#6135)
* fix: fetch same tokens for topTokens and sparkline queries

* fix: updated comment
2023-03-13 16:38:39 -04:00
Tina
2a04f0faca fix: loading states for USD prices (#6141)
* set loading state in usdc price fetch

* remove console log

* use 100 for polygon

* use 10k for polygon
2023-03-13 15:18:20 -04:00
Zach Pomerantz
1d2d1259e5 fix: set user properties before event properties (#6143) 2023-03-13 11:30:58 -07:00
Jordan Frankfurt
10eda002f5 fix: full range lp shortcut (#6136)
* fix: fix full range LP

* make a dedicated hook for syncing query parameters

* sync full range button to url

* add comment explaining lint rule disable
2023-03-13 12:36:45 -05:00
eddie
b5f665bc6e fix: allow defaultTokens to reset state in Widget (#6131)
fix: tdp widget bug
2023-03-13 10:31:15 -07:00
lynn
bf337f2865 fix: fix overflowing lp ui (#6138)
* fix

* incorporate comments from jordan + tina
2023-03-13 11:47:19 -04:00
Tina
5c8b45c8e5 fix: Use non-subgraph fields for calculating USD prices on explore and token details pages (#6134)
* use non subgraph fields for querying usd prices

* undefined check

* remove unuecessary logoUrl field

* remove outdated test

* fix: switch recently fetched tokens to token project market & update missing chart message

---------

Co-authored-by: cartcrom <cartergcromer@gmail.com>
2023-03-11 14:01:38 -05:00
Tina
2eb5ff3c5c fix: USD price for WETH + don't call gql for testnets (#6133)
fix weth + dont call gql for testnets
2023-03-11 12:04:28 -05:00
444 changed files with 16852 additions and 11047 deletions

12
.env
View File

@@ -1,13 +1,13 @@
# These API keys are intentionally public. Please do not report them - thank you for your concern.
ESLINT_NO_DEV_ERRORS=true
REACT_APP_AMPLITUDE_PROXY_URL="https://api.uniswap.org/v1/amplitude-proxy"
REACT_APP_STATSIG_PROXY_URL="https://api.uniswap.org/v1/statsig-proxy"
REACT_APP_AWS_API_REGION="us-east-2"
REACT_APP_AWS_API_ENDPOINT="https://beta.api.uniswap.org/v1/graphql"
REACT_APP_TEMP_API_URL="https://temp.api.uniswap.org/v1"
REACT_APP_SENTRY_DSN="https://a3c62e400b8748b5a8d007150e2f38b7@o1037921.ingest.sentry.io/4504255148851200"
REACT_APP_SENTRY_ENABLED=false
ESLINT_NO_DEV_ERRORS=true
REACT_APP_BNB_RPC_URL="https://rough-sleek-hill.bsc.quiknode.pro/413cc98cbc776cda8fdf1d0f47003583ff73d9bf"
REACT_APP_INFURA_KEY="4bf032f2d38a4ed6bb975b80d6340847"
REACT_APP_MOONPAY_API="https://api.moonpay.com"
REACT_APP_MOONPAY_LINK="https://us-central1-uniswap-mobile.cloudfunctions.net/signMoonpayLinkStaging?platform=web"
REACT_APP_MOONPAY_LINK="https://us-central1-uniswap-mobile.cloudfunctions.net/signMoonpayLinkV2?platform=web&env=staging"
REACT_APP_MOONPAY_PUBLISHABLE_KEY="pk_test_DycfESRid31UaSxhI5yWKe1r5E5kKSz"
REACT_APP_SENTRY_DSN="https://a3c62e400b8748b5a8d007150e2f38b7@o1037921.ingest.sentry.io/4504255148851200"
REACT_APP_STATSIG_PROXY_URL="https://api.uniswap.org/v1/statsig-proxy"
REACT_APP_TEMP_API_URL="https://temp.api.uniswap.org/v1"

View File

@@ -1,13 +1,15 @@
# These API keys are intentionally public. Please do not report them - thank you for your concern.
REACT_APP_AMPLITUDE_PROXY_URL="https://api.uniswap.org/v1/amplitude-proxy"
REACT_APP_STATSIG_PROXY_URL="https://api.uniswap.org/v1/statsig-proxy"
REACT_APP_AWS_API_ENDPOINT="https://api.uniswap.org/v1/graphql"
REACT_APP_BNB_RPC_URL="https://old-wispy-arrow.bsc.quiknode.pro/f5c060177236065c1058531a0615ab4f7a34a2fd"
REACT_APP_FIREBASE_KEY="AIzaSyBcZWwTcTJHj_R6ipZcrJkXdq05PuX0Rs0"
REACT_APP_FORTMATIC_KEY="pk_live_F937DF033A1666BF"
REACT_APP_GOOGLE_ANALYTICS_ID="G-KDP9B6W4H8"
REACT_APP_INFURA_KEY="099fc58e0de9451d80b18d7c74caa7c1"
REACT_APP_MOONPAY_API="https://api.moonpay.com"
REACT_APP_MOONPAY_LINK="https://us-central1-uniswap-mobile.cloudfunctions.net/signMoonpayLink?platform=web"
REACT_APP_MOONPAY_LINK="https://us-central1-uniswap-mobile.cloudfunctions.net/signMoonpayLinkV2?platform=web&env=production"
REACT_APP_MOONPAY_PUBLISHABLE_KEY="pk_live_uQG4BJC4w3cxnqpcSqAfohdBFDTsY6E"
REACT_APP_FIREBASE_KEY="AIzaSyBcZWwTcTJHj_R6ipZcrJkXdq05PuX0Rs0"
REACT_APP_SENTRY_ENABLED=true
REACT_APP_SENTRY_TRACES_SAMPLE_RATE=0.00003
REACT_APP_STATSIG_PROXY_URL="https://api.uniswap.org/v1/statsig-proxy"
THE_GRAPH_SCHEMA_ENDPOINT="https://api.thegraph.com/subgraphs/name/uniswap/uniswap-v3"
REACT_APP_SENTRY_ENABLED=false

View File

@@ -4,4 +4,31 @@ require('@uniswap/eslint-config/load')
module.exports = {
extends: '@uniswap/eslint-config/react',
overrides: [
{
// 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.
files: ['**/*.config.*', '**/*.d.ts'],
rules: {
'import/no-unused-modules': 'off',
},
},
{
files: ['**/*.ts', '**/*.tsx'],
rules: {
'no-restricted-imports': [
'error',
{
paths: [
{
name: 'zustand',
importNames: ['default'],
message: 'Default import from zustand is deprecated. Import `{ create }` instead.',
},
],
},
],
},
},
],
}

2
.github/CODEOWNERS vendored
View File

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

View File

@@ -1,4 +1,6 @@
name: Setup
description: checkout repo, setup node, and install node_modules
runs:
using: composite
steps:
@@ -10,12 +12,14 @@ runs:
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:
path: node_modules/
path: |
node_modules
!node_modules/.cache
key: ${{ runner.os }}-install-${{ hashFiles('**/yarn.lock') }}
- if: steps.install-cache.outputs.cache-hit != 'true'
run: yarn install --frozen-lockfile --ignore-scripts
shell: bash

View File

@@ -1,24 +1,45 @@
Your PR title must follow [conventional commits](https://www.conventionalcommits.org/en/v1.0.0/#summary), and should start with one of the following [types](https://github.com/angular/angular/blob/22b96b9/CONTRIBUTING.md#type):
<!-- Your PR title must follow conventional commits: https://github.com/Uniswap/interface#pr-title -->
- build: Changes that affect the build system or external dependencies (example scopes: yarn, eslint, typescript)
- ci: Changes to our CI configuration files and scripts (example scopes: vercel, github, cypress)
- docs: Documentation only changes
- feat: A new feature
- fix: A bug fix
- perf: A code change that improves performance
- refactor: A code change that neither fixes a bug nor adds a feature
- style: Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc)
- test: Adding missing tests or correcting existing tests
## Description
<!-- Summary of change, including motivation and context. -->
<!-- Use verb-driven language: "Fixes XYZ" instead of "This change fixes XYZ" -->
Example commit messages:
- feat: adds support for gnosis safe wallet
- fix: removes a polling memory leak
- chore: bumps redux version
<!-- Delete inapplicable lines: -->
_JIRA ticket:_
_Slack thread:_
_Relevant docs:_
Other things to note:
- Please describe the change using verb statements (ex: Removes X from Y)
- PRs with multiple changes should use a list of verb statements
- Add any relevant unit / integration tests
- Changes will be previewable via vercel. Non-obvious changes should include instructions for how to reproduce them
<!-- Delete this section if your change does not affect UI. -->
## Screen capture
| Before | After (Desktop) | After (Mobile) |
| ------------ |---------------- | -------------- |
| paste_before | past_after | paste_after |
## Test plan
<!-- Delete this section if your change is not a bug fix. -->
### Reproducing the error
<!-- Include steps to reproduce the bug. -->
1.
### QA (ie manual testing)
<!-- Include steps to test the change, ensuring no regression. -->
- [ ] N/A
#### Devices
<!-- If applicable, include different devices and screen sizes that may be affected, and how you've tested them. -->
### Automated testing
<!-- If N/A, check and note so it is obvious to your reviewers and does not show up as an incomplete task. -->
<!-- eg - [x] Unit test N/A -->
- [ ] Unit test
- [ ] Integration/E2E test

View File

@@ -42,6 +42,8 @@ jobs:
needs: tag
if: ${{ needs.tag.outputs.new_tag != null }}
runs-on: ubuntu-latest
environment:
name: release
steps:
- uses: actions/checkout@v3
- uses: ./.github/actions/setup

View File

@@ -1,5 +1,9 @@
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.
# See https://jongleberry.medium.com/speed-up-your-ci-and-dx-with-node-modules-cache-ac8df82b7bb0.
on:
push:
branches:
@@ -14,7 +18,27 @@ jobs:
steps:
- uses: actions/checkout@v3
- uses: ./.github/actions/setup
- uses: actions/cache@v3
id: eslint-cache
with:
path: node_modules/.cache
key: ${{ runner.os }}-eslint-${{ hashFiles('**/yarn.lock') }}-${{ github.run_id }}
restore-keys: ${{ runner.os }}-eslint-${{ hashFiles('**/yarn.lock') }}-
- run: yarn lint
typecheck:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: ./.github/actions/setup
- uses: actions/cache@v3
id: tsc-cache
with:
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
deps-tests:
runs-on: ubuntu-latest
@@ -28,6 +52,12 @@ jobs:
steps:
- uses: actions/checkout@v3
- uses: ./.github/actions/setup
- uses: actions/cache@v3
id: jest-cache
with:
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
- uses: codecov/codecov-action@v3
@@ -41,9 +71,15 @@ jobs:
steps:
- uses: actions/checkout@v3
- uses: ./.github/actions/setup
- uses: actions/cache@v3
id: build-cache
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@v2
- uses: actions/upload-artifact@v3
with:
name: build
path: build
@@ -55,29 +91,17 @@ jobs:
steps:
- uses: actions/checkout@v3
- uses: ./.github/actions/setup
- uses: actions/download-artifact@v2
- uses: actions/download-artifact@v3
with:
name: build
path: build
- run: yarn test:size
cypress-build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: ./.github/actions/setup
- uses: actions/cache@v3
id: cypress-cache
with:
path: /home/runner/.cache/Cypress
key: ${{ runner.os }}-cypress-${{ hashFiles('node_modules/cypress') }}
- if: steps.cypress-cache.outputs.cache-hit != 'true'
run: yarn cypress install
cypress-test-matrix:
needs: [build, cypress-build]
needs: [build]
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:
fail-fast: false
matrix:
@@ -86,18 +110,19 @@ jobs:
- uses: actions/checkout@v3
- uses: ./.github/actions/setup
- uses: actions/download-artifact@v2
with:
name: build
path: build
- uses: actions/cache@v3
id: cypress-cache
with:
path: /home/runner/.cache/Cypress
key: ${{ runner.os }}-cypress-${{ hashFiles('node_modules/cypress') }}
- if: steps.cypress-cache.outputs.cache-hit != 'true'
run: yarn cypress install
path: /root/.cache/Cypress
key: ${{ runner.os }}-cypress
- run: |
yarn cypress install
yarn cypress info
- uses: actions/download-artifact@v3
with:
name: build
path: build
- uses: cypress-io/github-action@v4
with:
@@ -111,9 +136,17 @@ jobs:
CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# Included as a single job to check against for cypress test success, as cypress runs in a matrix.
- uses: codecov/codecov-action@v3
with:
token: ${{ secrets.CODECOV_TOKEN }}
fail_ci_if_error: false
verbose: true
# Included as a single job to check for cypress-test-matrix success, as a matrix cannot be checked.
cypress-tests:
if: ${{ always() }}
needs: [cypress-test-matrix]
runs-on: ubuntu-latest
steps:
- run: echo 'Finished cypress tests https\://dashboard.cypress.io/projects/yp82ef'
- if: needs.cypress-test-matrix.result != 'success'
run: exit 1

4
.gitignore vendored
View File

@@ -29,6 +29,10 @@ schema.graphql
.env.test.local
.env.production.local
instrumented
.nyc_output
.nyc_output/**/*
/.netlify
npm-debug.log*

7
.nycrc Normal file
View File

@@ -0,0 +1,7 @@
{
"extends": "@istanbuljs/nyc-config-typescript",
"all": true,
"report-dir": "coverage",
"soureMap": false,
"instrument": false
}

View File

@@ -38,12 +38,39 @@ You can block an entire list of tokens by passing in a tokenlist like [here](./s
For steps on local deployment, development, and code contribution, please see [CONTRIBUTING](./CONTRIBUTING.md).
#### PR Title
Your PR title must follow [conventional commits](https://www.conventionalcommits.org/en/v1.0.0/#summary), and should start with one of the following [types](https://github.com/angular/angular/blob/22b96b9/CONTRIBUTING.md#type):
- build: Changes that affect the build system or external dependencies (example scopes: yarn, eslint, typescript)
- ci: Changes to our CI configuration files and scripts (example scopes: vercel, github, cypress)
- docs: Documentation only changes
- feat: A new feature
- fix: A bug fix
- perf: A code change that improves performance
- refactor: A code change that neither fixes a bug nor adds a feature
- style: Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc)
- test: Adding missing tests or correcting existing tests
Example commit messages:
- feat: adds support for gnosis safe wallet
- fix: removes a polling memory leak
- chore: bumps redux version
Other things to note:
- Please describe the change using verb statements (ex: Removes X from Y)
- PRs with multiple changes should use a list of verb statements
- Add any relevant unit / integration tests
- Changes will be previewable via vercel. Non-obvious changes should include instructions for how to reproduce them
## Accessing Uniswap V2
The Uniswap Interface supports swapping, adding liquidity, removing liquidity and migrating liquidity for Uniswap protocol V2.
- Swap on Uniswap V2: <https://app.uniswap.org/#/swap?use=v2>
- View V2 liquidity: <https://app.uniswap.org/#/pool/v2>
- View V2 liquidity: <https://app.uniswap.org/#/pools/v2>
- Add V2 liquidity: <https://app.uniswap.org/#/add/v2>
- Migrate V2 liquidity to V3: <https://app.uniswap.org/#/migrate/v2>

21
codecov.yml Normal file
View File

@@ -0,0 +1,21 @@
ignore:
- "**/generated/**/*"
- "**/generated/*"
- "**/cypress/**/*"
- "cypress/**/*"
- "**/instrumented/**/*"
- "**/styles/**/*"
- "styles/**/*"
- "**/constants/**/*"
- "constants/**/*"
coverage:
status:
project:
default:
target: auto
threshold: 1%
if_ci_failed: error
patch:
default:
target: 80%

View File

@@ -1,17 +1,44 @@
/* eslint-disable @typescript-eslint/no-var-requires */
/* eslint-env node */
const { VanillaExtractPlugin } = require('@vanilla-extract/webpack-plugin')
const { execSync } = require('child_process')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const { DefinePlugin } = require('webpack')
const commitHash = require('child_process').execSync('git rev-parse HEAD')
const commitHash = execSync('git rev-parse HEAD').toString().trim()
const isProduction = process.env.NODE_ENV === 'production'
// Linting and type checking are only necessary as part of development and testing.
// Omit them from production builds, as they slow down the feedback loop.
const shouldLintOrTypeCheck = !isProduction
module.exports = {
babel: {
plugins: ['@vanilla-extract/babel-plugin'],
env: {
test: {
plugins: ['istanbul'],
},
development: {
plugins: ['istanbul'],
},
},
},
eslint: {
enable: shouldLintOrTypeCheck,
pluginOptions(eslintConfig) {
return Object.assign(eslintConfig, {
cache: true,
cacheLocation: 'node_modules/.cache/eslint/',
})
},
},
typescript: {
enableTypeChecking: shouldLintOrTypeCheck,
},
jest: {
configure(jestConfig) {
return Object.assign({}, jestConfig, {
return Object.assign(jestConfig, {
cacheDirectory: 'node_modules/.cache/jest',
transformIgnorePatterns: ['@uniswap/conedison/format', '@uniswap/conedison/provider'],
moduleNameMapper: {
'@uniswap/conedison/format': '@uniswap/conedison/dist/format',
@@ -21,25 +48,29 @@ module.exports = {
},
},
webpack: {
plugins: [
new VanillaExtractPlugin({ identifiers: 'short' }),
new DefinePlugin({
'process.env.REACT_APP_GIT_COMMIT_HASH': JSON.stringify(commitHash.toString()),
}),
],
plugins: [new VanillaExtractPlugin({ identifiers: 'short' })],
configure: (webpackConfig) => {
const instanceOfMiniCssExtractPlugin = webpackConfig.plugins.find(
(plugin) => plugin instanceof MiniCssExtractPlugin
)
if (instanceOfMiniCssExtractPlugin !== undefined) instanceOfMiniCssExtractPlugin.options.ignoreOrder = true
webpackConfig.plugins = webpackConfig.plugins.map((plugin) => {
// Extend process.env with dynamic values (eg commit hash).
// This will make dynamic values available to JavaScript only, not to interpolated HTML (ie index.html).
if (plugin instanceof DefinePlugin) {
Object.assign(plugin.definitions['process.env'], {
REACT_APP_GIT_COMMIT_HASH: JSON.stringify(commitHash),
})
}
// We're currently on Webpack 4.x that doesn't support the `exports` field in package.json.
// CSS ordering is mitigated through scoping / naming conventions, so we can ignore order warnings.
// See https://webpack.js.org/plugins/mini-css-extract-plugin/#remove-order-warnings.
if (plugin instanceof MiniCssExtractPlugin) {
plugin.options.ignoreOrder = true
}
return plugin
})
// 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.
//
// In case you need to add more modules, make sure to remap them to the correct path.
//
// Map @uniswap/conedison to its dist folder.
// This is required because conedison uses * to redirect all imports to its dist.
webpackConfig.resolve.alias['@uniswap/conedison'] = '@uniswap/conedison/dist'
return webpackConfig

View File

@@ -1,3 +1,4 @@
import codeCoverageTask from '@cypress/code-coverage/task'
import { defineConfig } from 'cypress'
export default defineConfig({
@@ -5,8 +6,10 @@ export default defineConfig({
videoUploadOnPasses: false,
defaultCommandTimeout: 24000, // 2x average block time
chromeWebSecurity: false,
retries: { runMode: 2 },
e2e: {
setupNodeEvents(on, config) {
codeCoverageTask(on, config)
return {
...config,
// Only enable Chrome.

View File

@@ -25,7 +25,16 @@ describe('Landing Page', () => {
})
it('allows navigation to pool', () => {
cy.viewport(2000, 1600)
cy.visit('/swap')
cy.get(getTestSelector('pool-nav-link')).first().click()
cy.url().should('include', '/pool')
cy.url().should('include', '/pools')
})
it('allows navigation to pool on mobile', () => {
cy.viewport('iphone-6')
cy.visit('/swap')
cy.get(getTestSelector('pool-nav-link')).last().click()
cy.url().should('include', '/pools')
})
})

View File

@@ -1,6 +1,7 @@
// see https://github.com/Uniswap/interface/pull/4115
describe('Link', () => {
it('should update route', () => {
cy.viewport(2000, 1600)
cy.visit('/')
cy.contains('Pool').click()
cy.get('[data-cy="join-pool-button"]').should('exist')

View File

@@ -33,7 +33,7 @@ describe('Testing nfts', () => {
cy.visit(`/#/nfts/collection/${PUDGY_COLLECTION_ADDRESS}`)
cy.get(getTestSelector('nft-filter')).first().click()
cy.get(getTestSelector('nft-collection-filter-buy-now')).click()
cy.get(getTestSelector('nft-details-link')).first().click()
cy.get(getTestSelector('nft-collection-asset')).first().click()
cy.get(getTestSelector('nft-details-traits')).should('exist')
cy.get(getTestSelector('nft-details-activity')).should('exist')
cy.get(getTestSelector('nft-details-description')).should('exist')
@@ -48,4 +48,16 @@ describe('Testing nfts', () => {
cy.get(getTestSelector('nft-details-toggle-bag')).eq(1).click()
cy.get(getTestSelector('nft-bag')).should('exist')
})
it('should navigate to the owned nfts page', () => {
cy.get(getTestSelector('web3-status-connected')).click()
cy.get(getTestSelector('nft-view-self-nfts')).click()
})
it('should close the sidebar when navigating to NFT details', () => {
cy.get(getTestSelector('web3-status-connected')).click()
cy.get(getTestSelector('mini-portfolio-nav-nfts')).click()
cy.get(getTestSelector('mini-portfolio-nft')).first().click()
cy.contains('Buy crypto').should('not.be.visible')
})
})

View File

@@ -1,6 +1,6 @@
describe('Pool', () => {
beforeEach(() => {
cy.visit('/pool').then(() => {
cy.visit('/pools').then(() => {
cy.wait('@eth_blockNumber')
})
})

View File

@@ -0,0 +1,11 @@
describe('Position', () => {
it('shows an valid state on a supported network', () => {
cy.visit('/pools/1')
cy.contains('UNI / ETH')
})
it('shows an invalid state on a supported network', () => {
cy.visit('/pools/788893')
cy.contains('To view a position, you must be connected to the network it belongs to.')
})
})

View File

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

@@ -7,7 +7,7 @@ describe('Token explore', () => {
it('should load token leaderboard', () => {
cy.visit('/tokens/ethereum')
cy.get(getTestSelectorStartsWith('token-table')).its('length').should('be.eq', 100)
cy.get(getTestSelectorStartsWith('token-table')).its('length').should('be.greaterThan', 0)
// check sorted svg icon is present in volume cell, since tokens are sorted by volume by default
cy.get(getTestSelector('header-row')).find(getTestSelector('volume-cell')).find('svg').should('exist')
cy.get(getTestSelector('token-table-row-ETH')).find(getTestSelector('name-cell')).should('include.text', 'Ether')
@@ -69,6 +69,6 @@ describe('Token explore', () => {
cy.get(getTestSelector('tokens-network-filter-selected')).should('contain', 'Optimism')
cy.reload()
cy.get(getTestSelector('tokens-network-filter-selected')).should('contain', 'Optimism')
cy.get(getTestSelector('chain-selector')).last().should('contain', 'Ethereum')
cy.get(getTestSelector('chain-selector-logo')).invoke('attr', 'alt').should('eq', 'Ethereum')
})
})

View File

@@ -1,18 +1,46 @@
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('/pool')
cy.visit('/pools')
})
it('should change the theme', () => {
cy.get(getTestSelector('web3-status-connected')).click()
cy.get(getTestSelector('wallet-select-theme')).click()
cy.get(getTestSelector('wallet-select-theme')).contains('Light theme').should('exist')
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)')
})
it('should select a language', () => {
cy.get(getTestSelector('wallet-select-language')).click()
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 })
@@ -20,22 +48,13 @@ describe('Wallet Dropdown', () => {
cy.get(getTestSelector('wallet-back')).click()
})
it('should be able to view transactions', () => {
cy.get(getTestSelector('wallet-transactions')).click()
cy.get(getTestSelector('wallet-empty-transaction-text')).should('exist')
cy.get(getTestSelector('wallet-back')).click()
})
it('should change the theme when not connected', () => {
cy.get(getTestSelector('wallet-disconnect')).click()
cy.get(getTestSelector('wallet-select-theme')).click()
cy.get(getTestSelector('wallet-select-theme')).contains('Dark theme').should('exist')
cy.get(getTestSelector('wallet-select-theme')).click()
cy.get(getTestSelector('wallet-select-theme')).contains('Light theme').should('exist')
cy.get(getTestSelector('wallet-settings')).click()
cy.get(getTestSelector('theme-lightmode')).should('exist')
})
it('should select a language when not connected', () => {
cy.get(getTestSelector('wallet-select-language')).click()
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 })
@@ -43,9 +62,35 @@ describe('Wallet Dropdown', () => {
cy.get(getTestSelector('wallet-back')).click()
})
it('should open the wallet connect modal from the drop down when not connected', () => {
cy.get(getTestSelector('wallet-connect-wallet')).click()
cy.get(getTestSelector('wallet-modal')).should('exist')
cy.get(getTestSelector('wallet-modal-close')).click()
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')
})
})

View File

@@ -8,6 +8,7 @@
// Import commands.ts using ES2015 syntax:
import { injected } from './ethereum'
import assert = require('assert')
import '@cypress/code-coverage/support'
import { FeatureFlag } from '../../src/featureFlags/flags/featureFlags'

View File

@@ -1,3 +1,5 @@
export const getTestSelector = (selectorId: string) => `[data-testid=${selectorId}]`
export const getTestSelectorStartsWith = (selectorId: string) => `[data-testid^=${selectorId}]`
export const getClassContainsSelector = (selectorId: string) => `[class*=${selectorId}]`

View File

@@ -1,6 +1,6 @@
{
"name": "@uniswap/interface",
"version": "1.0.7",
"version": "1.1.0",
"description": "Uniswap Interface",
"homepage": ".",
"license": "GPL-3.0-or-later",
@@ -17,16 +17,17 @@
"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",
"start": "craco start",
"build": "craco build",
"serve": "serve build -l 3000",
"deduplicate": "yarn-deduplicate --strategy=highest",
"lint": "yarn eslint .",
"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",
"cypress:open": "cypress open --browser chrome --e2e",
"cypress:run": "cypress run --browser chrome --e2e",
"postinstall": "patch-package"
"deduplicate": "yarn-deduplicate --strategy=highest"
},
"jest": {
"collectCoverageFrom": [
@@ -37,8 +38,12 @@
"src/lib/utils/**/*.ts*",
"src/pages/**/*.ts*",
"src/state/**/*.ts*",
"src/tracing/**/*.ts*",
"src/utils/**/*.ts*"
],
"coveragePathIgnorePatterns": [
".snap"
],
"coverageThreshold": {
"global": {
"branches": 4,
@@ -66,6 +71,7 @@
"@lingui/cli": "^3.9.0",
"@testing-library/jest-dom": "^5.16.4",
"@testing-library/react": "^13.1",
"@testing-library/user-event": "^14.4.3",
"@typechain/ethers-v5": "^7.0.0",
"@types/array.prototype.flat": "^1.2.1",
"@types/array.prototype.flatmap": "^1.2.2",
@@ -94,7 +100,7 @@
"@uniswap/eslint-config": "^1.1.1",
"@vanilla-extract/babel-plugin": "^1.1.7",
"@vanilla-extract/webpack-plugin": "^2.1.11",
"cypress": "^10.3.1",
"cypress": "10.3.1",
"env-cmd": "^10.1.0",
"eslint": "^7.11.0",
"jest-fetch-mock": "^3.0.3",
@@ -113,6 +119,7 @@
"dependencies": {
"@apollo/client": "^3.7.2",
"@coinbase/wallet-sdk": "^3.6.4",
"@cypress/code-coverage": "^3.10.0",
"@fontsource/ibm-plex-mono": "^4.5.1",
"@fontsource/inter": "^4.5.1",
"@graphql-codegen/cli": "^2.15.0",
@@ -130,12 +137,12 @@
"@popperjs/core": "^2.4.4",
"@reach/dialog": "^0.10.3",
"@reach/portal": "^0.10.3",
"@react-hook/window-scroll": "^1.3.0",
"@reduxjs/toolkit": "^1.6.1",
"@sentry/react": "^7.29.0",
"@sentry/react": "^7.45.0",
"@sentry/tracing": "^7.45.0",
"@types/react-window-infinite-loader": "^1.0.6",
"@uniswap/analytics": "^1.3.1",
"@uniswap/analytics-events": "^2.6.0",
"@uniswap/analytics-events": "^2.10.0",
"@uniswap/conedison": "^1.4.0",
"@uniswap/governance": "^1.0.2",
"@uniswap/liquidity-staker": "^1.0.2",
@@ -143,17 +150,17 @@
"@uniswap/permit2-sdk": "1.2.0",
"@uniswap/redux-multicall": "^1.1.8",
"@uniswap/router-sdk": "^1.3.0",
"@uniswap/sdk-core": "^3.2.0",
"@uniswap/sdk-core": "^3.2.2",
"@uniswap/smart-order-router": "^3.6.1",
"@uniswap/token-lists": "^1.0.0-beta.30",
"@uniswap/universal-router-sdk": "^1.3.6",
"@uniswap/universal-router-sdk": "^1.3.8",
"@uniswap/v2-core": "1.0.0",
"@uniswap/v2-periphery": "^1.1.0-beta.0",
"@uniswap/v2-sdk": "^3.0.1",
"@uniswap/v3-core": "1.0.0",
"@uniswap/v3-periphery": "^1.1.1",
"@uniswap/v3-sdk": "^3.9.0",
"@uniswap/widgets": "^2.47.3",
"@uniswap/widgets": "^2.49.0",
"@vanilla-extract/css": "^1.7.2",
"@vanilla-extract/css-utils": "^0.1.2",
"@vanilla-extract/dynamic": "^2.0.2",
@@ -178,12 +185,12 @@
"@web3-react/walletconnect": "8.1.2-beta.0",
"array.prototype.flat": "^1.2.4",
"array.prototype.flatmap": "^1.2.4",
"babel-plugin-istanbul": "^6.1.1",
"cids": "^1.0.0",
"clsx": "^1.1.1",
"copy-to-clipboard": "^3.2.0",
"d3": "^7.6.1",
"ethers": "^5.7.2",
"firebase": "^9.1.3",
"focus-visible": "^5.2.0",
"get-graphql-schema": "^2.1.2",
"graphql": "^16.5.0",
@@ -199,7 +206,7 @@
"numbro": "^2.3.6",
"polished": "^3.3.2",
"polyfill-object.fromentries": "^1.0.1",
"popper-max-size-modifier": "^0.2.0",
"qrcode.react": "^3.1.0",
"qs": "^6.9.4",
"rc-slider": "^10.0.1",
"react": "^18.2.0",
@@ -222,12 +229,10 @@
"rebass": "^4.0.7",
"redux": "^4.1.2",
"redux-localstorage-simple": "^2.3.1",
"setimmediate": "^1.0.5",
"statsig-react": "^1.22.0",
"styled-components": "^5.3.5",
"tiny-invariant": "^1.2.0",
"ua-parser-js": "^0.7.28",
"use-count-up": "^2.2.5",
"use-resize-observer": "^9.0.2",
"uuid": "^8.3.2",
"video-extensions": "^1.2.0",

View File

@@ -1,22 +1,27 @@
/* eslint-env node */
require('dotenv').config({ path: '.env.production' })
const { exec } = require('child_process')
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 exec = promisify(child_process.exec)
function fetchSchema(url, outputFile) {
exec(
`get-graphql-schema --h Origin=https://app.uniswap.org ${url} | tee ${outputFile}.temp`,
(error, stdout, stderr) => {
if (error || stderr) {
console.log(`Failed to fetch schema from ${url}`)
} else if (stdout) {
exec(`mv ${outputFile}.temp ${outputFile}`)
exec(`npx get-graphql-schema --h Origin=https://app.uniswap.org ${url}`)
.then(({ stderr, stdout }) => {
if (stderr) {
throw new Error(stderr)
} else {
fs.writeFile(outputFile, stdout)
}
}
)
})
.catch((err) => {
console.error(err)
console.error(`Failed to fetch schema from ${url}`)
})
}
fetchSchema(process.env.THE_GRAPH_SCHEMA_ENDPOINT, thegraphConfig.schema)

View File

@@ -16,12 +16,12 @@ try {
}
// The last recorded size for these assets, as reported by `yarn build`.
const LAST_SIZE_MAIN_KB = 374
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 = 1417
const LAST_SIZE_ENTRY_KB = 1442
const SIZE_TOLERANCE_KB = 5
const SIZE_TOLERANCE_KB = 10
const jsEntrypoints = entrypoints.filter((entrypoint) => entrypoint.endsWith('js'))
assert(jsEntrypoints.length === 3)

View File

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

After

Width:  |  Height:  |  Size: 1.6 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 52 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 112 KiB

View File

@@ -0,0 +1,15 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="24" height="24" rx="8" fill="#F7F9FB"/>
<path d="M19.6128 4L13.2335 8.73803L14.4132 5.94266L19.6128 4Z" fill="#E2761B" stroke="#E2761B" stroke-width="0.0641141" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M4.88603 4L11.2141 8.78291L10.0921 5.94266L4.88603 4ZM17.3177 14.9827L15.6187 17.5858L19.254 18.586L20.2991 15.0404L17.3177 14.9827ZM4.21283 15.0404L5.25148 18.586L8.88675 17.5858L7.18772 14.9827L4.21283 15.0404Z" fill="#E4761B" stroke="#E4761B" stroke-width="0.0641141" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M8.68189 10.5847L7.66888 12.117L11.2785 12.2773L11.1503 8.3984L8.68189 10.5847ZM15.8178 10.5847L13.3173 8.35352L13.234 12.2773L16.8372 12.117L15.8178 10.5847ZM8.88705 17.5859L11.0541 16.5281L9.18198 15.0663L8.88705 17.5859ZM13.4456 16.5281L15.619 17.5859L15.3177 15.0663L13.4456 16.5281Z" fill="#E4761B" stroke="#E4761B" stroke-width="0.0641141" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M15.6191 17.5857L13.4456 16.5278L13.6187 17.9448L13.5995 18.541L15.6191 17.5857ZM8.88708 17.5857L10.9067 18.541L10.8939 17.9448L11.0541 16.5278L8.88708 17.5857Z" fill="#D7C1B3" stroke="#D7C1B3" stroke-width="0.0641141" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M10.9392 14.1297L9.13123 13.5976L10.4071 13.0142L10.9392 14.1297ZM13.5615 14.1297L14.0937 13.0142L15.3759 13.5976L13.5615 14.1297Z" fill="#233447" stroke="#233447" stroke-width="0.0641141" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M8.88652 17.5856L9.19427 14.9826L7.1875 15.0403L8.88652 17.5856ZM15.3108 14.9826L15.6185 17.5856L17.3175 15.0403L15.3108 14.9826ZM16.8367 12.1167L13.2335 12.277L13.5669 14.1299L14.099 13.0143L15.3813 13.5977L16.8367 12.1167ZM9.13016 13.5977L10.4124 13.0143L10.9382 14.1299L11.278 12.277L7.66836 12.1167L9.13016 13.5977Z" fill="#CD6116" stroke="#CD6116" stroke-width="0.0641141" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M7.66888 12.1167L9.18198 15.0659L9.13069 13.5977L7.66888 12.1167ZM15.3818 13.5977L15.3177 15.0659L16.8372 12.1167L15.3818 13.5977ZM11.2785 12.277L10.9387 14.1299L11.3619 16.3162L11.458 13.4374L11.2785 12.277ZM13.234 12.277L13.0609 13.431L13.1378 16.3162L13.5674 14.1299L13.234 12.277Z" fill="#E4751F" stroke="#E4751F" stroke-width="0.0641141" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M13.5679 14.1298L13.1384 16.3161L13.4461 16.5277L15.3182 15.0659L15.3824 13.5977L13.5679 14.1298ZM9.13123 13.5977L9.18252 15.0659L11.0546 16.5277L11.3624 16.3161L10.9392 14.1298L9.13123 13.5977Z" fill="#F6851B" stroke="#F6851B" stroke-width="0.0641141" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M13.5995 18.5412L13.6187 17.945L13.4584 17.8039H11.0413L10.8939 17.945L10.9067 18.5412L8.88708 17.5859L9.59234 18.163L11.0221 19.1567H13.4777L14.9138 18.163L15.6191 17.5859L13.5995 18.5412Z" fill="#C0AD9E" stroke="#C0AD9E" stroke-width="0.0641141" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M13.4455 16.528L13.1377 16.3164H11.3618L11.054 16.528L10.8937 17.9449L11.0412 17.8039H13.4583L13.6186 17.9449L13.4455 16.528Z" fill="#161616" stroke="#161616" stroke-width="0.0641141" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M19.8824 9.04578L20.4273 6.42992L19.6131 4L13.4453 8.57775L15.8175 10.5845L19.1707 11.5655L19.9144 10.6999L19.5939 10.4691L20.1068 10.0011L19.7093 9.69333L20.2222 9.30224L19.8824 9.04578ZM4.07825 6.42992L4.62322 9.04578L4.277 9.30224L4.78991 9.69333L4.39882 10.0011L4.91173 10.4691L4.59116 10.6999L5.32847 11.5655L8.68164 10.5845L11.0539 8.57775L4.88608 4L4.07825 6.42992Z" fill="#763D16" stroke="#763D16" stroke-width="0.0641141" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M19.1706 11.5657L15.8175 10.5847L16.8369 12.1171L15.3174 15.0663L17.3177 15.0407H20.2991L19.1706 11.5657ZM8.68158 10.5847L5.32841 11.5657L4.21283 15.0407H7.18772L9.18167 15.0663L7.66858 12.1171L8.68158 10.5847ZM13.2337 12.2773L13.4453 8.57796L14.4198 5.94287H10.0921L11.0538 8.57796L11.2782 12.2773L11.3551 13.4442L11.3616 16.3165H13.1375L13.1503 13.4442L13.2337 12.2773Z" fill="#F6851B" stroke="#F6851B" stroke-width="0.0641141" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

View File

@@ -0,0 +1,88 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_12510_20313)">
<rect width="24" height="24" rx="8" fill="white"/>
<g filter="url(#filter0_f_12510_20313)">
<circle cx="13.0526" cy="14.3159" r="14.7368" fill="url(#paint0_radial_12510_20313)"/>
</g>
<g filter="url(#filter1_d_12510_20313)">
<path d="M17.3968 5.20882C17.422 4.76813 17.482 4.47745 17.6027 4.21199C17.6505 4.10692 17.6952 4.02092 17.7021 4.02092C17.709 4.02092 17.6883 4.09848 17.656 4.19325C17.5682 4.45088 17.5538 4.80325 17.6143 5.21322C17.691 5.7334 17.7346 5.80845 18.2868 6.37036C18.5459 6.63392 18.8472 6.96633 18.9564 7.10904L19.155 7.36853L18.9564 7.18308C18.7135 6.9563 18.155 6.51402 18.0316 6.4508C17.9489 6.40839 17.9366 6.40913 17.8856 6.45969C17.8385 6.50629 17.8286 6.5763 17.8221 6.90727C17.812 7.4231 17.7413 7.7542 17.5709 8.08526C17.4787 8.26432 17.4642 8.2261 17.5476 8.02399C17.6099 7.87309 17.6162 7.80675 17.6157 7.30739C17.6148 6.30405 17.4951 6.06284 16.7935 5.64963C16.6157 5.54495 16.3229 5.39399 16.1427 5.31414C15.9625 5.23428 15.8193 5.16473 15.8245 5.15954C15.8444 5.13985 16.5287 5.33869 16.8041 5.44416C17.2138 5.60105 17.2814 5.62138 17.3311 5.60246C17.3645 5.58977 17.3806 5.49307 17.3968 5.20882Z" fill="url(#paint1_linear_12510_20313)"/>
<path d="M8.74107 3.74916C8.49804 3.7117 8.48779 3.7073 8.60216 3.68984C8.82133 3.65634 9.33884 3.70199 9.69547 3.78626C10.528 3.98291 11.2856 4.48669 12.0943 5.38141L12.3091 5.6191L12.6165 5.57C13.9112 5.36324 15.2284 5.52756 16.3301 6.0333C16.6331 6.17243 17.111 6.44939 17.1707 6.52056C17.1897 6.54324 17.2246 6.68925 17.2483 6.84505C17.3301 7.38407 17.2892 7.79723 17.1233 8.10582C17.033 8.27375 17.0279 8.32696 17.0886 8.47068C17.1371 8.58536 17.2722 8.67024 17.4059 8.67005C17.6797 8.66969 17.9744 8.2299 18.1109 7.61798L18.1651 7.37491L18.2726 7.49585C18.8619 8.15934 19.3248 9.06419 19.4043 9.70827L19.425 9.87619L19.326 9.72353C19.1555 9.46083 18.9842 9.28201 18.7648 9.13777C18.3694 8.87779 17.9514 8.78931 16.8441 8.73133C15.8441 8.67897 15.2782 8.59409 14.717 8.41224C13.7621 8.10289 13.2808 7.69089 12.1466 6.21216C11.6428 5.55535 11.3314 5.19195 11.0217 4.8993C10.3179 4.23435 9.62632 3.88561 8.74107 3.74916Z" fill="url(#paint2_linear_12510_20313)"/>
<path d="M9.21314 6.92984C8.71977 6.25487 8.41451 5.21998 8.4806 4.44636L8.50103 4.20696L8.61333 4.22732C8.82423 4.26555 9.18786 4.40004 9.35813 4.50281C9.82539 4.78481 10.0277 5.15608 10.2335 6.10945C10.2938 6.38869 10.3729 6.7047 10.4092 6.81168C10.4678 6.98388 10.6892 7.38612 10.8692 7.64735C10.9989 7.83549 10.9127 7.92464 10.6262 7.89893C10.1885 7.85967 9.59561 7.45307 9.21314 6.92984Z" fill="url(#paint3_linear_12510_20313)"/>
<path d="M16.7978 11.9533C14.4921 11.0308 13.6801 10.2301 13.6801 8.87901C13.6801 8.68019 13.687 8.51751 13.6954 8.51751C13.7038 8.51751 13.793 8.58313 13.8936 8.66334C14.3611 9.03598 14.8847 9.19514 16.3341 9.40526C17.187 9.52892 17.667 9.62879 18.1097 9.7747C19.517 10.2385 20.3877 11.1797 20.5953 12.4616C20.6557 12.8341 20.6203 13.5327 20.5225 13.9008C20.4452 14.1916 20.2096 14.7157 20.1471 14.7358C20.1298 14.7414 20.1128 14.6754 20.1083 14.5856C20.0846 14.1043 19.8398 13.6356 19.4287 13.2846C18.9613 12.8854 18.3333 12.5677 16.7978 11.9533Z" fill="url(#paint4_linear_12510_20313)"/>
<path d="M15.1791 12.3366C15.1503 12.1657 15.1002 11.9476 15.0678 11.8518L15.009 11.6776L15.1182 11.7995C15.2695 11.9681 15.389 12.184 15.4902 12.4714C15.5675 12.6908 15.5762 12.7561 15.5757 13.1126C15.5751 13.4626 15.5654 13.536 15.494 13.7335C15.3815 14.0449 15.2418 14.2657 15.0074 14.5027C14.5863 14.9286 14.0448 15.1645 13.2633 15.2623C13.1275 15.2793 12.7316 15.3079 12.3836 15.3259C11.5065 15.3712 10.9292 15.4649 10.4105 15.6459C10.3359 15.6719 10.2693 15.6877 10.2626 15.681C10.2416 15.6603 10.5947 15.4511 10.8864 15.3114C11.2978 15.1145 11.7072 15.0071 12.6246 14.8553C13.0778 14.7803 13.5458 14.6893 13.6646 14.6531C14.7869 14.3111 15.3638 13.4288 15.1791 12.3366Z" fill="url(#paint5_linear_12510_20313)"/>
<path d="M16.2361 14.2019C15.9298 13.5475 15.8594 12.9156 16.0272 12.3264C16.0452 12.2634 16.0741 12.2119 16.0914 12.2119C16.1088 12.2119 16.1811 12.2507 16.2521 12.2982C16.3932 12.3926 16.6763 12.5517 17.4306 12.9603C18.3719 13.4703 18.9085 13.8652 19.2735 14.3163C19.5931 14.7114 19.7909 15.1614 19.8861 15.7101C19.94 16.0209 19.9084 16.7688 19.8281 17.0818C19.5751 18.0686 18.9869 18.8438 18.148 19.2961C18.0251 19.3624 17.9147 19.4168 17.9028 19.4171C17.8908 19.4173 17.9356 19.3041 18.0023 19.1656C18.2845 18.5794 18.3167 18.0092 18.1033 17.3745C17.9727 16.9858 17.7063 16.5116 17.1685 15.7102C16.5432 14.7784 16.3899 14.5304 16.2361 14.2019Z" fill="url(#paint6_linear_12510_20313)"/>
<path d="M7.57574 17.7327C8.43136 17.0148 9.49596 16.5048 10.4657 16.3482C10.8836 16.2808 11.5799 16.3075 11.9669 16.406C12.5872 16.5637 13.1421 16.9171 13.4307 17.3381C13.7128 17.7495 13.8338 18.1081 13.9597 18.9058C14.0094 19.2205 14.0635 19.5365 14.0799 19.608C14.1745 20.0215 14.3586 20.3519 14.5867 20.5179C14.9491 20.7815 15.5731 20.7979 16.1869 20.5599C16.2911 20.5195 16.3815 20.4916 16.3879 20.4979C16.4101 20.5199 16.101 20.7255 15.883 20.8337C15.5896 20.9794 15.3563 21.0357 15.0463 21.0357C14.4841 21.0357 14.0173 20.7515 13.6279 20.172C13.5512 20.058 13.379 19.7164 13.2451 19.413C12.8339 18.481 12.6308 18.1971 12.1534 17.8863C11.7379 17.616 11.202 17.5675 10.7989 17.764C10.2694 18.0219 10.1216 18.6944 10.5009 19.1205C10.6516 19.2899 10.9327 19.436 11.1626 19.4644C11.5926 19.5175 11.9621 19.1925 11.9621 18.761C11.9621 18.4809 11.8538 18.3211 11.581 18.1988C11.2085 18.0317 10.808 18.227 10.81 18.5747C10.8108 18.723 10.8758 18.8162 11.0254 18.8834C11.1213 18.9266 11.1235 18.93 11.0453 18.9138C10.7035 18.8434 10.6234 18.4339 10.8982 18.1621C11.2282 17.8357 11.9105 17.9797 12.1448 18.4252C12.2432 18.6123 12.2546 18.9849 12.1688 19.2099C11.9768 19.7136 11.4167 19.9784 10.8486 19.8343C10.4618 19.7361 10.3044 19.6299 9.83802 19.1526C9.02768 18.3231 8.7131 18.1624 7.54487 17.9811L7.32101 17.9464L7.57574 17.7327Z" fill="url(#paint7_linear_12510_20313)"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M3.77354 2.88677C6.47968 6.15059 8.34354 7.49719 8.55067 7.78169C8.72168 8.0166 8.65732 8.2278 8.36434 8.39331C8.20142 8.48533 7.86646 8.57858 7.69875 8.57858C7.50906 8.57858 7.44394 8.50586 7.44394 8.50586C7.33395 8.40221 7.27201 8.42033 6.70721 7.42408C5.92308 6.215 5.26687 5.21202 5.24896 5.19523C5.20757 5.15639 5.20828 5.15771 6.62725 7.68067C6.85652 8.20652 6.67286 8.39954 6.67286 8.47443C6.67286 8.62679 6.63103 8.70687 6.4419 8.91651C6.1266 9.26604 5.98566 9.65877 5.88391 10.4716C5.76985 11.3827 5.44913 12.0263 4.56029 13.1279C4.04 13.7727 3.95486 13.8909 3.82358 14.1508C3.65821 14.478 3.61274 14.6613 3.59431 15.0746C3.57484 15.5116 3.61277 15.7938 3.74711 16.2116C3.86472 16.5774 3.98749 16.8189 4.30133 17.3019C4.57217 17.7188 4.72812 18.0285 4.72812 18.1497C4.72812 18.2461 4.74665 18.2463 5.16634 18.1521C6.17072 17.9267 6.98627 17.5303 7.44495 17.0446C7.72882 16.7439 7.79546 16.5778 7.79762 16.1658C7.79904 15.8963 7.7895 15.8399 7.71619 15.6848C7.59685 15.4325 7.3796 15.2227 6.90076 14.8974C6.27336 14.4712 6.00538 14.1281 5.93136 13.6562C5.87064 13.269 5.94108 12.9958 6.28817 12.273C6.64742 11.5247 6.73645 11.2059 6.79667 10.4517C6.83557 9.9644 6.88943 9.77223 7.03032 9.61798C7.17725 9.45714 7.30953 9.40267 7.67316 9.3533C8.266 9.27282 8.64349 9.1204 8.95378 8.83627C9.22296 8.58978 9.3356 8.35227 9.35288 7.99474L9.366 7.72374L9.21558 7.54946C8.67084 6.9183 3.40873 2.39062 3.37521 2.39062C3.36805 2.39062 3.5473 2.61391 3.77354 2.88677ZM5.03391 15.569C5.15708 15.3523 5.09164 15.0736 4.88562 14.9375C4.69096 14.8089 4.38857 14.8694 4.38857 15.0371C4.38857 15.0882 4.41703 15.1254 4.48117 15.1583C4.58918 15.2135 4.59702 15.2756 4.51204 15.4025C4.42598 15.531 4.43293 15.6441 4.53164 15.7209C4.69074 15.8447 4.91596 15.7766 5.03391 15.569Z" fill="url(#paint8_linear_12510_20313)"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M9.74013 9.49546C9.46182 9.5804 9.19129 9.87351 9.10754 10.1808C9.05645 10.3683 9.08544 10.6972 9.16196 10.7988C9.28559 10.9629 9.40515 11.0061 9.72887 11.0038C10.3627 10.9994 10.9136 10.7293 10.9777 10.3916C11.0302 10.1148 10.7882 9.73123 10.4549 9.56281C10.2829 9.47595 9.91711 9.44148 9.74013 9.49546ZM10.481 10.0712C10.5788 9.93317 10.536 9.78402 10.3698 9.68314C10.0532 9.49106 9.57452 9.65001 9.57452 9.94718C9.57452 10.0951 9.82416 10.2565 10.053 10.2565C10.2053 10.2565 10.4137 10.1662 10.481 10.0712Z" fill="url(#paint9_linear_12510_20313)"/>
</g>
</g>
<defs>
<filter id="filter0_f_12510_20313" x="-72.277" y="-71.0137" width="170.659" height="170.659" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<feGaussianBlur stdDeviation="35.2964" result="effect1_foregroundBlur_12510_20313"/>
</filter>
<filter id="filter1_d_12510_20313" x="-20.2828" y="-21.2671" width="64.5655" height="65.9605" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset/>
<feGaussianBlur stdDeviation="11.8289"/>
<feComposite in2="hardAlpha" operator="out"/>
<feColorMatrix type="matrix" values="0 0 0 0 1 0 0 0 0 0 0 0 0 0 0.605405 0 0 0 0.15 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_12510_20313"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_12510_20313" result="shape"/>
</filter>
<radialGradient id="paint0_radial_12510_20313" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(13.0719 14.2967) rotate(42.4693) scale(16.2421)">
<stop stop-color="white"/>
<stop offset="1" stop-color="#FFD8EF" stop-opacity="0.4"/>
</radialGradient>
<linearGradient id="paint1_linear_12510_20313" x1="19.7578" y1="18.1172" x2="4.89844" y2="3.67969" gradientUnits="userSpaceOnUse">
<stop stop-color="#FFCDF1"/>
<stop offset="0.380969" stop-color="#F436DA"/>
<stop offset="1" stop-color="#F356EF"/>
</linearGradient>
<linearGradient id="paint2_linear_12510_20313" x1="19.7578" y1="18.1172" x2="4.89844" y2="3.67969" gradientUnits="userSpaceOnUse">
<stop stop-color="#FFCDF1"/>
<stop offset="0.380969" stop-color="#F436DA"/>
<stop offset="1" stop-color="#F356EF"/>
</linearGradient>
<linearGradient id="paint3_linear_12510_20313" x1="19.7578" y1="18.1172" x2="4.89844" y2="3.67969" gradientUnits="userSpaceOnUse">
<stop stop-color="#FFCDF1"/>
<stop offset="0.380969" stop-color="#F436DA"/>
<stop offset="1" stop-color="#F356EF"/>
</linearGradient>
<linearGradient id="paint4_linear_12510_20313" x1="19.7578" y1="18.1172" x2="4.89844" y2="3.67969" gradientUnits="userSpaceOnUse">
<stop stop-color="#FFCDF1"/>
<stop offset="0.380969" stop-color="#F436DA"/>
<stop offset="1" stop-color="#F356EF"/>
</linearGradient>
<linearGradient id="paint5_linear_12510_20313" x1="19.7578" y1="18.1172" x2="4.89844" y2="3.67969" gradientUnits="userSpaceOnUse">
<stop stop-color="#FFCDF1"/>
<stop offset="0.380969" stop-color="#F436DA"/>
<stop offset="1" stop-color="#F356EF"/>
</linearGradient>
<linearGradient id="paint6_linear_12510_20313" x1="19.7578" y1="18.1172" x2="4.89844" y2="3.67969" gradientUnits="userSpaceOnUse">
<stop stop-color="#FFCDF1"/>
<stop offset="0.380969" stop-color="#F436DA"/>
<stop offset="1" stop-color="#F356EF"/>
</linearGradient>
<linearGradient id="paint7_linear_12510_20313" x1="19.7578" y1="18.1172" x2="4.89844" y2="3.67969" gradientUnits="userSpaceOnUse">
<stop stop-color="#FFCDF1"/>
<stop offset="0.380969" stop-color="#F436DA"/>
<stop offset="1" stop-color="#F356EF"/>
</linearGradient>
<linearGradient id="paint8_linear_12510_20313" x1="19.7578" y1="18.1172" x2="4.89844" y2="3.67969" gradientUnits="userSpaceOnUse">
<stop stop-color="#FFCDF1"/>
<stop offset="0.380969" stop-color="#F436DA"/>
<stop offset="1" stop-color="#F356EF"/>
</linearGradient>
<linearGradient id="paint9_linear_12510_20313" x1="19.7578" y1="18.1172" x2="4.89844" y2="3.67969" gradientUnits="userSpaceOnUse">
<stop stop-color="#FFCDF1"/>
<stop offset="0.380969" stop-color="#F436DA"/>
<stop offset="1" stop-color="#F356EF"/>
</linearGradient>
<clipPath id="clip0_12510_20313">
<rect width="24" height="24" rx="8" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 12 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -0,0 +1,77 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Generator: Adobe Illustrator 18.1.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 22.773 22.773" style="enable-background:new 0 0 22.773 22.773;" xml:space="preserve">
<g>
<g>
<path d="M15.769,0c0.053,0,0.106,0,0.162,0c0.13,1.606-0.483,2.806-1.228,3.675c-0.731,0.863-1.732,1.7-3.351,1.573
c-0.108-1.583,0.506-2.694,1.25-3.561C13.292,0.879,14.557,0.16,15.769,0z"/>
<path d="M20.67,16.716c0,0.016,0,0.03,0,0.045c-0.455,1.378-1.104,2.559-1.896,3.655c-0.723,0.995-1.609,2.334-3.191,2.334
c-1.367,0-2.275-0.879-3.676-0.903c-1.482-0.024-2.297,0.735-3.652,0.926c-0.155,0-0.31,0-0.462,0
c-0.995-0.144-1.798-0.932-2.383-1.642c-1.725-2.098-3.058-4.808-3.306-8.276c0-0.34,0-0.679,0-1.019
c0.105-2.482,1.311-4.5,2.914-5.478c0.846-0.52,2.009-0.963,3.304-0.765c0.555,0.086,1.122,0.276,1.619,0.464
c0.471,0.181,1.06,0.502,1.618,0.485c0.378-0.011,0.754-0.208,1.135-0.347c1.116-0.403,2.21-0.865,3.652-0.648
c1.733,0.262,2.963,1.032,3.723,2.22c-1.466,0.933-2.625,2.339-2.427,4.74C17.818,14.688,19.086,15.964,20.67,16.716z"/>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@@ -1,181 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 470.287 514.251" enable-background="new 0 0 470.287 514.251" xml:space="preserve">
<g id="Background">
</g>
<g id="Logos_and_symbols">
<g id="SYMBOL_VER_3">
</g>
<g id="SYMBOL_VER_3_3_">
</g>
<g id="SYMBOL_VER_4">
</g>
<g id="SYMBOL_VER_4_1_">
<g id="SYMBOL_VER_4_3_">
</g>
</g>
<g id="SYMBOL_VER_5_1_">
</g>
<g id="off_2_1_">
</g>
<g id="VER_3_1_">
<g id="SYMBOL_VER_2_1_">
</g>
</g>
<g id="VER_3">
<g id="SYMBOL_VER_2">
</g>
</g>
<g id="off_2">
</g>
<g id="SYMBOL_VER_5">
</g>
<g id="SYMBOL_VER_1">
</g>
<g id="SYMBOL_VER_1_1_">
</g>
<g id="SYMBOL_VER_1-1_3_">
</g>
<g id="SYMBOL_VER_1-1_2_">
</g>
<g id="SYMBOL_VER_1-1">
</g>
<g id="SYMBOL_VER_1-1_1_">
<g id="_x31_-3">
</g>
<g id="Symbol_-_Original_14_">
<path fill="#2D374B" d="M291.134,237.469l35.654-60.5l96.103,149.684l0.046,28.727l-0.313-197.672
c-0.228-4.832-2.794-9.252-6.887-11.859L242.715,46.324c-4.045-1.99-9.18-1.967-13.22,0.063c-0.546,0.272-1.06,0.57-1.548,0.895
l-0.604,0.379L59.399,144.983l-0.651,0.296c-0.838,0.385-1.686,0.875-2.48,1.444c-3.185,2.283-5.299,5.66-5.983,9.448
c-0.103,0.574-0.179,1.158-0.214,1.749l0.264,161.083l89.515-138.745c11.271-18.397,35.825-24.323,58.62-24.001l26.753,0.706
L67.588,409.765l18.582,10.697L245.692,157.22l70.51-0.256L157.091,426.849l66.306,38.138l7.922,4.556
c3.351,1.362,7.302,1.431,10.681,0.21l175.453-101.678l-33.544,19.438L291.134,237.469z M304.736,433.395l-66.969-105.108
l40.881-69.371l87.952,138.628L304.736,433.395z"/>
<polygon fill="#28A0F0" points="237.768,328.286 304.736,433.395 366.601,397.543 278.648,258.915 "/>
<path fill="#28A0F0" d="M422.937,355.379l-0.046-28.727l-96.103-149.684l-35.654,60.5l92.774,150.043l33.544-19.438
c3.29-2.673,5.281-6.594,5.49-10.825L422.937,355.379z"/>
<path fill="#FFFFFF" d="M20.219,382.469l47.369,27.296l157.634-252.801l-26.753-0.706c-22.795-0.322-47.35,5.604-58.62,24.001
L50.334,319.004l-30.115,46.271V382.469z"/>
<polygon fill="#FFFFFF" points="316.202,156.964 245.692,157.22 86.17,420.462 141.928,452.565 157.091,426.849 "/>
<path fill="#96BEDC" d="M452.65,156.601c-0.59-14.746-8.574-28.245-21.08-36.104L256.28,19.692
c-12.371-6.229-27.825-6.237-40.218-0.004c-1.465,0.739-170.465,98.752-170.465,98.752c-2.339,1.122-4.592,2.458-6.711,3.975
c-11.164,8.001-17.969,20.435-18.668,34.095v208.765l30.115-46.271L50.07,157.921c0.035-0.589,0.109-1.169,0.214-1.741
c0.681-3.79,2.797-7.171,5.983-9.456c0.795-0.569,172.682-100.064,173.228-100.337c4.04-2.029,9.175-2.053,13.22-0.063
l173.022,99.523c4.093,2.607,6.659,7.027,6.887,11.859v199.542c-0.209,4.231-1.882,8.152-5.172,10.825l-33.544,19.438
l-17.308,10.031l-61.864,35.852l-62.737,36.357c-3.379,1.221-7.33,1.152-10.681-0.21l-74.228-42.693l-15.163,25.717
l66.706,38.406c2.206,1.255,4.171,2.367,5.784,3.272c2.497,1.4,4.199,2.337,4.8,2.629c4.741,2.303,11.563,3.643,17.71,3.643
c5.636,0,11.132-1.035,16.332-3.072l182.225-105.531c10.459-8.104,16.612-20.325,17.166-33.564V156.601z"/>
</g>
<g id="Symbol_-_Original_13_">
</g>
<g id="Symbol_-_Original_6_">
</g>
<g id="Symbol_-_Original_4_">
</g>
<g id="One_color_version_-_White_3_">
<g id="Symbol_-_Original_15_">
</g>
</g>
<g id="One_color_version_-_White">
<g id="Symbol_-_Original">
</g>
</g>
<g id="Symbol_-_Monochromatic_3_">
<g id="_x33__7_">
</g>
</g>
<g id="Symbol_-_Monochromatic">
<g id="_x33__3_">
</g>
</g>
<g id="_x33__2_">
</g>
<g id="_x33__1_">
</g>
<g id="_x33_">
</g>
<g id="Symbol_-_Original_10_">
</g>
<g id="Symbol_-_Original_1_">
</g>
<g id="Symbol_-_Original_2_">
</g>
<g id="_x34__1_">
</g>
<g id="Symbol_-_Monochromatic_2_">
<g id="_x33__6_">
</g>
</g>
<g id="One_color_version_-_White_2_">
<g id="Symbol_-_Original_11_">
</g>
</g>
<g id="Symbol_-_Original_5_">
<g id="Symbol_-_Original_12_">
</g>
</g>
<g id="One_color_version_-_White_1_">
<g id="Symbol_-_Original_9_">
</g>
</g>
</g>
<g id="SYMBOL_VER_1_2_">
<g id="SYMBOL_VER_2_4_">
</g>
<g id="SYMBOL_VER_2-1-1_1_">
</g>
<g id="SYMBOL_VER_2-2-1_1_">
</g>
<g id="SYMBOL_VER_2-3-1_4_">
</g>
<g id="New_Symbol_1_">
<g id="SYMBOL_VER_2-3-1_3_">
</g>
</g>
<g id="New_Symbol">
<g id="SYMBOL_VER_2-3-1_1_">
</g>
</g>
</g>
<g id="SYMBOL_VER_2_2_">
</g>
<g id="SYMBOL_VER_4_2_">
</g>
<g id="SYMBOL_VER_3_2_">
</g>
<g id="SYMBOL_VER_3_1_">
</g>
<g id="SYMBOL_VER_1-1-1_1_">
</g>
<g id="SYMBOL_VER_1-1-1">
</g>
<g id="SYMBOL_VER_1-1-1_2_2_">
</g>
<g id="SYMBOL_VER_1-1-1_2">
</g>
<g id="SYMBOL_VER_1-1-1_2_1_">
</g>
<g id="Symbol_-_Original_7_">
</g>
<g id="Symbol_-_Original_8_">
</g>
<g id="SYMBOL_VER_2-1-1">
</g>
<g id="SYMBOL_VER_2-2-1">
</g>
<g id="SYMBOL_VER_2-3-1">
</g>
<g id="SYMBOL_VER_5-1_1_">
</g>
<g id="SYMBOL_VER_5-1">
</g>
<g id="SYMBOL_VER_5-2_1_">
</g>
<g id="SYMBOL_VER_5-2">
</g>
<g id="Symbol_-_Monochromatic_1_">
<g id="_x33__4_">
</g>
</g>
</g>
<svg width="14" height="16" viewBox="0 0 14 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0.53125 5.04465V10.9554C0.53125 11.3328 0.742546 11.6817 1.08477 11.8698L6.44755 14.8258C6.78977 15.0139 7.21107 15.0139 7.55329 14.8258L12.9161 11.8698C13.2583 11.6817 13.4696 11.3328 13.4696 10.9554V5.04465C13.4696 4.66726 13.2583 4.31833 12.9161 4.13026L7.55329 1.17426C7.21107 0.986184 6.78977 0.986184 6.44755 1.17426L1.08347 4.13026C0.74125 4.31833 0.53125 4.66726 0.53125 5.04465Z" fill="#213147"/>
<path d="M8.17051 9.14643L7.40569 11.1484C7.38495 11.2041 7.38495 11.2648 7.40569 11.3204L8.72143 14.7652L10.2433 13.9263L8.4168 9.14643C8.37532 9.03631 8.21199 9.03631 8.17051 9.14643Z" fill="#12AAFF"/>
<path d="M9.70391 5.77961C9.66243 5.66949 9.4991 5.66949 9.45762 5.77961L8.6928 7.78162C8.67206 7.83731 8.67206 7.89793 8.6928 7.95361L10.8485 13.5934L12.3704 12.7545L9.70391 5.77961Z" fill="#12AAFF"/>
<path d="M7 1.39574C7.03759 1.39574 7.07519 1.40564 7.10889 1.42296L12.9124 4.62147C12.9798 4.65859 13.0213 4.72789 13.0213 4.80089V11.1967C13.0213 11.2709 12.9798 11.339 12.9124 11.3761L7.10889 14.5746C7.07648 14.5932 7.03759 14.6018 7 14.6018C6.96241 14.6018 6.92482 14.5919 6.89111 14.5746L1.08759 11.3786C1.02019 11.3415 0.978704 11.2722 0.978704 11.1992V4.80213C0.978704 4.72789 1.02019 4.65983 1.08759 4.62271L6.89111 1.4242C6.92482 1.40564 6.96241 1.39574 7 1.39574ZM7 0.461548C6.79389 0.461548 6.58648 0.512279 6.40111 0.614978L0.598889 3.81226C0.228148 4.01642 0 4.3938 0 4.80213V11.1979C0 11.6062 0.228148 11.9836 0.598889 12.1878L6.40241 15.3863C6.58778 15.4878 6.79389 15.5397 7.0013 15.5397C7.20741 15.5397 7.41482 15.489 7.60019 15.3863L13.4037 12.1878C13.7744 11.9836 14.0026 11.6062 14.0026 11.1979V4.80213C14.0026 4.3938 13.7744 4.01642 13.4037 3.81226L7.59889 0.614978C7.41352 0.512279 7.20611 0.461548 7 0.461548Z" fill="#9DCCED"/>
<path d="M3.16162 13.6008L3.6957 12.2051L4.77033 13.0576L3.7657 13.9336L3.16162 13.6008Z" fill="#213147"/>
<path d="M6.51113 4.3443H5.03983C4.92965 4.3443 4.83113 4.40988 4.79354 4.50887L1.63965 12.7619L3.1615 13.6008L6.63428 4.51258C6.66669 4.43091 6.60317 4.3443 6.51113 4.3443Z" fill="white"/>
<path d="M9.08579 4.3443H7.6145C7.50431 4.3443 7.40579 4.40988 7.3682 4.50887L3.76709 13.9324L5.28894 14.7713L9.20894 4.51258C9.24005 4.43091 9.17653 4.3443 9.08579 4.3443Z" fill="white"/>
</svg>

Before

Width:  |  Height:  |  Size: 5.3 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

@@ -0,0 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<circle cx="8" cy="8" r="8" stroke-width="3" fill="#98A1C0"></circle>
</svg>

After

Width:  |  Height:  |  Size: 216 B

View File

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

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

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

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@@ -0,0 +1,77 @@
<svg width="42" height="42" viewBox="0 0 42 42" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="0.75" y="0.75" width="40.5" height="40.5" rx="8.25" fill="#293249" />
<g clip-path="url(#clip0_304_15139)">
<rect x="3" y="3" width="17" height="17" rx="6" fill="#3375BB" />
<g clip-path="url(#clip1_304_15139)">
<mask id="mask0_304_15139" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="3" y="2"
width="17" height="18">
<path d="M19.843 2.99976H3.10449V19.7382H19.843V2.99976Z" fill="white" />
</mask>
<g mask="url(#mask0_304_15139)">
<path
d="M11.4737 19.7382C16.0959 19.7382 19.843 15.9912 19.843 11.369C19.843 6.74679 16.0959 2.99976 11.4737 2.99976C6.85153 2.99976 3.10449 6.74679 3.10449 11.369C3.10449 15.9912 6.85153 19.7382 11.4737 19.7382Z"
fill="#3375BB" />
<path
d="M11.533 6.66125C13.187 8.0426 15.0837 7.95741 15.6257 7.95741C15.5071 15.8135 14.6039 14.2556 11.533 16.4585C8.46215 14.2556 7.5646 15.8135 7.44604 7.95741C7.98233 7.95741 9.87903 8.0426 11.533 6.66125Z"
stroke="white" stroke-width="1.04615" stroke-miterlimit="10" stroke-linecap="round"
stroke-linejoin="round" />
</g>
</g>
</g>
<rect x="3" y="22" width="17" height="17" rx="6" fill="white" />
<path
d="M17.9393 30.9431C18.4082 29.8912 16.0762 26.938 13.8455 25.7213C12.4386 24.7707 10.981 24.8975 10.6769 25.3157C10.0305 26.2283 12.8315 27.0141 14.7073 27.914C14.3017 28.0914 13.9215 28.4083 13.706 28.8012C13.0089 28.0407 11.4753 27.3816 9.67558 27.914C8.45883 28.2688 7.45756 29.118 7.06465 30.3855C6.97593 30.3474 6.86186 30.3221 6.76046 30.3221C6.34221 30.3221 6 30.6643 6 31.0826C6 31.5008 6.34221 31.843 6.76046 31.843C6.83651 31.843 7.07732 31.7923 7.07732 31.7923L10.981 31.8177C9.42209 34.3019 8.18 34.6567 8.18 35.0877C8.18 35.5186 9.35872 35.4045 9.80232 35.2398C11.9316 34.4793 14.213 32.0838 14.6059 31.3994C16.2536 31.6149 17.6351 31.6276 17.9393 30.9431Z"
fill="url(#paint0_linear_304_15139)" />
<path fill-rule="evenodd" clip-rule="evenodd"
d="M14.7201 27.9138C14.8088 27.8758 14.7961 27.7491 14.7708 27.6477C14.7201 27.4195 13.7695 26.4816 12.8823 26.0634C11.6656 25.493 10.7784 25.5184 10.6516 25.7845C10.8924 26.2915 12.0458 26.7605 13.2372 27.2674C13.7315 27.4702 14.2511 27.6857 14.7201 27.9138Z"
fill="url(#paint1_linear_304_15139)" />
<path fill-rule="evenodd" clip-rule="evenodd"
d="M13.1737 33.0091C12.9328 32.9204 12.654 32.8317 12.3371 32.7556C12.6667 32.1599 12.7427 31.26 12.4259 30.7024C11.9823 29.9165 11.4246 29.4983 10.1191 29.4983C9.40936 29.4983 7.48285 29.7391 7.44482 31.3488C7.44482 31.5135 7.44482 31.6656 7.4575 31.8177H10.981C10.512 32.5655 10.0684 33.1232 9.67552 33.5414C10.1445 33.6555 10.5247 33.7569 10.8796 33.8583C11.2091 33.947 11.526 34.023 11.8428 34.1118C12.3245 33.7569 12.7808 33.3767 13.1737 33.0091Z"
fill="url(#paint2_linear_304_15139)" />
<path
d="M7.01397 31.6276C7.15338 32.8443 7.85048 33.326 9.27001 33.4654C10.6895 33.6048 11.5007 33.5161 12.578 33.6048C13.4779 33.6809 14.2891 34.1498 14.5806 33.985C14.8468 33.8456 14.6947 33.326 14.3398 32.9964C13.8708 32.5655 13.2244 32.274 12.0964 32.1599C12.3245 31.5389 12.2612 30.6644 11.9063 30.1954C11.3993 29.511 10.4614 29.2068 9.27001 29.3335C8.02792 29.4856 6.83652 30.1067 7.01397 31.6276Z"
fill="url(#paint3_linear_304_15139)" />
<rect x="22" y="3" width="17" height="36" rx="6" fill="#4C82FB" fill-opacity="0.24" />
<g clip-path="url(#clip2_304_15139)">
<path
d="M30.5001 25.0833C33.0314 25.0833 35.0834 23.0313 35.0834 20.5C35.0834 17.9687 33.0314 15.9166 30.5001 15.9166C27.9688 15.9166 25.9167 17.9687 25.9167 20.5C25.9167 23.0313 27.9688 25.0833 30.5001 25.0833Z"
stroke="#4C82FB" stroke-width="0.825" stroke-linecap="round" stroke-linejoin="round" />
<path d="M25.9167 20.5H35.0834" stroke="#4C82FB" stroke-width="0.825" stroke-linecap="round"
stroke-linejoin="round" />
<path
d="M30.5001 15.9166C31.6465 17.1717 32.298 18.8005 32.3334 20.5C32.298 22.1994 31.6465 23.8282 30.5001 25.0833C29.3537 23.8282 28.7022 22.1994 28.6667 20.5C28.7022 18.8005 29.3537 17.1717 30.5001 15.9166V15.9166Z"
stroke="#4C82FB" stroke-width="0.825" stroke-linecap="round" stroke-linejoin="round" />
</g>
<rect x="0.75" y="0.75" width="40.5" height="40.5" rx="8.25" stroke="#1B2236" stroke-width="0.5" />
<defs>
<linearGradient id="paint0_linear_304_15139" x1="9.54664" y1="29.9616" x2="17.8542" y2="32.3175"
gradientUnits="userSpaceOnUse">
<stop stop-color="#8797FF" />
<stop offset="1" stop-color="#AAA8FF" />
</linearGradient>
<linearGradient id="paint1_linear_304_15139" x1="16.147" y1="30.1475" x2="10.1527" y2="24.1385"
gradientUnits="userSpaceOnUse">
<stop stop-color="#3B22A0" />
<stop offset="1" stop-color="#5156D8" stop-opacity="0" />
</linearGradient>
<linearGradient id="paint2_linear_304_15139" x1="13.3391" y1="33.2263" x2="7.58086" y2="29.9157"
gradientUnits="userSpaceOnUse">
<stop stop-color="#3B1E8F" />
<stop offset="1" stop-color="#6A6FFB" stop-opacity="0" />
</linearGradient>
<linearGradient id="paint3_linear_304_15139" x1="9.14535" y1="30.6449" x2="13.0375" y2="35.5903"
gradientUnits="userSpaceOnUse">
<stop stop-color="#8898FF" />
<stop offset="0.9839" stop-color="#5F47F1" />
</linearGradient>
<clipPath id="clip0_304_15139">
<rect x="3" y="3" width="17" height="17" rx="6" fill="white" />
</clipPath>
<clipPath id="clip1_304_15139">
<rect width="17" height="16.7385" fill="white" transform="translate(3 2.99988)" />
</clipPath>
<clipPath id="clip2_304_15139">
<rect width="11" height="11" fill="white" transform="translate(25 15)" />
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 5.7 KiB

View File

@@ -0,0 +1,78 @@
<svg width="42" height="42" viewBox="0 0 42 42" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="1" y="1" width="40" height="40" rx="8" fill="#E8ECFB" />
<g clip-path="url(#clip0_304_15192)">
<rect x="3" y="3" width="17" height="17" rx="6" fill="#3375BB" />
<g clip-path="url(#clip1_304_15192)">
<mask id="mask0_304_15192" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="3" y="2"
width="17" height="18">
<path d="M19.843 2.99976H3.10449V19.7382H19.843V2.99976Z" fill="white" />
</mask>
<g mask="url(#mask0_304_15192)">
<path
d="M11.4737 19.7382C16.0959 19.7382 19.843 15.9912 19.843 11.369C19.843 6.74679 16.0959 2.99976 11.4737 2.99976C6.85153 2.99976 3.10449 6.74679 3.10449 11.369C3.10449 15.9912 6.85153 19.7382 11.4737 19.7382Z"
fill="#3375BB" />
<path
d="M11.533 6.66125C13.187 8.0426 15.0837 7.95741 15.6257 7.95741C15.5071 15.8135 14.6039 14.2556 11.533 16.4585C8.46215 14.2556 7.5646 15.8135 7.44604 7.95741C7.98233 7.95741 9.87903 8.0426 11.533 6.66125Z"
stroke="white" stroke-width="1.04615" stroke-miterlimit="10" stroke-linecap="round"
stroke-linejoin="round" />
</g>
</g>
</g>
<rect x="3" y="22" width="17" height="17" rx="6" fill="white" />
<path
d="M17.9393 30.9431C18.4082 29.8912 16.0762 26.938 13.8455 25.7213C12.4386 24.7707 10.981 24.8975 10.6769 25.3157C10.0305 26.2283 12.8315 27.0141 14.7073 27.914C14.3017 28.0914 13.9215 28.4083 13.706 28.8012C13.0089 28.0407 11.4753 27.3816 9.67558 27.914C8.45883 28.2688 7.45756 29.118 7.06465 30.3855C6.97593 30.3474 6.86186 30.3221 6.76046 30.3221C6.34221 30.3221 6 30.6643 6 31.0826C6 31.5008 6.34221 31.843 6.76046 31.843C6.83651 31.843 7.07732 31.7923 7.07732 31.7923L10.981 31.8177C9.42209 34.3019 8.18 34.6567 8.18 35.0877C8.18 35.5186 9.35872 35.4045 9.80232 35.2398C11.9316 34.4793 14.213 32.0838 14.6059 31.3994C16.2536 31.6149 17.6351 31.6276 17.9393 30.9431Z"
fill="url(#paint0_linear_304_15192)" />
<path fill-rule="evenodd" clip-rule="evenodd"
d="M14.7201 27.9138C14.8088 27.8758 14.7961 27.7491 14.7708 27.6477C14.7201 27.4195 13.7695 26.4816 12.8823 26.0634C11.6656 25.493 10.7784 25.5184 10.6516 25.7845C10.8924 26.2915 12.0458 26.7605 13.2372 27.2674C13.7315 27.4702 14.2511 27.6857 14.7201 27.9138Z"
fill="url(#paint1_linear_304_15192)" />
<path fill-rule="evenodd" clip-rule="evenodd"
d="M13.1737 33.0091C12.9328 32.9204 12.654 32.8317 12.3371 32.7556C12.6667 32.1599 12.7427 31.26 12.4259 30.7024C11.9823 29.9165 11.4246 29.4983 10.1191 29.4983C9.40936 29.4983 7.48285 29.7391 7.44482 31.3488C7.44482 31.5135 7.44482 31.6656 7.4575 31.8177H10.981C10.512 32.5655 10.0684 33.1232 9.67552 33.5414C10.1445 33.6555 10.5247 33.7569 10.8796 33.8583C11.2091 33.947 11.526 34.023 11.8428 34.1118C12.3245 33.7569 12.7808 33.3767 13.1737 33.0091Z"
fill="url(#paint2_linear_304_15192)" />
<path
d="M7.01397 31.6276C7.15338 32.8443 7.85048 33.326 9.27001 33.4654C10.6895 33.6048 11.5007 33.5161 12.578 33.6048C13.4779 33.6809 14.2891 34.1498 14.5806 33.985C14.8468 33.8456 14.6947 33.326 14.3398 32.9964C13.8708 32.5655 13.2244 32.274 12.0964 32.1599C12.3245 31.5389 12.2612 30.6644 11.9063 30.1954C11.3993 29.511 10.4614 29.2068 9.27001 29.3335C8.02792 29.4856 6.83652 30.1067 7.01397 31.6276Z"
fill="url(#paint3_linear_304_15192)" />
<rect x="22" y="3" width="17" height="36" rx="6" fill="#FB118E" fill-opacity="0.12" />
<g clip-path="url(#clip2_304_15192)">
<path
d="M30.5001 25.0833C33.0314 25.0833 35.0834 23.0313 35.0834 20.5C35.0834 17.9687 33.0314 15.9166 30.5001 15.9166C27.9688 15.9166 25.9167 17.9687 25.9167 20.5C25.9167 23.0313 27.9688 25.0833 30.5001 25.0833Z"
stroke="#FB118E" stroke-width="0.825" stroke-linecap="round" stroke-linejoin="round" />
<path d="M25.9167 20.5H35.0834" stroke="#FB118E" stroke-width="0.825" stroke-linecap="round"
stroke-linejoin="round" />
<path
d="M30.5001 15.9166C31.6465 17.1717 32.298 18.8005 32.3334 20.5C32.298 22.1994 31.6465 23.8282 30.5001 25.0833C29.3537 23.8282 28.7022 22.1994 28.6667 20.5C28.7022 18.8005 29.3537 17.1717 30.5001 15.9166V15.9166Z"
stroke="#FB118E" stroke-width="0.825" stroke-linecap="round" stroke-linejoin="round" />
</g>
<rect x="0.75" y="0.75" width="40.5" height="40.5" rx="8.25" stroke="#5D6785"
stroke-opacity="0.24" stroke-width="0.5" />
<defs>
<linearGradient id="paint0_linear_304_15192" x1="9.54664" y1="29.9616" x2="17.8542" y2="32.3175"
gradientUnits="userSpaceOnUse">
<stop stop-color="#8797FF" />
<stop offset="1" stop-color="#AAA8FF" />
</linearGradient>
<linearGradient id="paint1_linear_304_15192" x1="16.147" y1="30.1475" x2="10.1527" y2="24.1385"
gradientUnits="userSpaceOnUse">
<stop stop-color="#3B22A0" />
<stop offset="1" stop-color="#5156D8" stop-opacity="0" />
</linearGradient>
<linearGradient id="paint2_linear_304_15192" x1="13.3391" y1="33.2263" x2="7.58086" y2="29.9157"
gradientUnits="userSpaceOnUse">
<stop stop-color="#3B1E8F" />
<stop offset="1" stop-color="#6A6FFB" stop-opacity="0" />
</linearGradient>
<linearGradient id="paint3_linear_304_15192" x1="9.14535" y1="30.6449" x2="13.0375" y2="35.5903"
gradientUnits="userSpaceOnUse">
<stop stop-color="#8898FF" />
<stop offset="0.9839" stop-color="#5F47F1" />
</linearGradient>
<clipPath id="clip0_304_15192">
<rect x="3" y="3" width="17" height="17" rx="6" fill="white" />
</clipPath>
<clipPath id="clip1_304_15192">
<rect width="17" height="16.7385" fill="white" transform="translate(3 2.99988)" />
</clipPath>
<clipPath id="clip2_304_15192">
<rect width="11" height="11" fill="white" transform="translate(25 15)" />
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 5.7 KiB

View File

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

After

Width:  |  Height:  |  Size: 398 B

View File

@@ -0,0 +1,13 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M17 3H7C5 3 4 4 4 6V18C4 20 5 21 7 21H17C19 21 20 20 20 18V6C20 4 19 3 17 3ZM8 16.75C7.586 16.75 7.25 16.414 7.25 16C7.25 15.586 7.586 15.25 8 15.25C8.414 15.25 8.75 15.586 8.75 16C8.75 16.414 8.414 16.75 8 16.75ZM8 12.75C7.586 12.75 7.25 12.414 7.25 12C7.25 11.586 7.586 11.25 8 11.25C8.414 11.25 8.75 11.586 8.75 12C8.75 12.414 8.414 12.75 8 12.75ZM8 8.75C7.586 8.75 7.25 8.414 7.25 8C7.25 7.586 7.586 7.25 8 7.25C8.414 7.25 8.75 7.586 8.75 8C8.75 8.414 8.414 8.75 8 8.75ZM16 16.75H11C10.586 16.75 10.25 16.414 10.25 16C10.25 15.586 10.586 15.25 11 15.25H16C16.414 15.25 16.75 15.586 16.75 16C16.75 16.414 16.414 16.75 16 16.75ZM16 12.75H11C10.586 12.75 10.25 12.414 10.25 12C10.25 11.586 10.586 11.25 11 11.25H16C16.414 11.25 16.75 11.586 16.75 12C16.75 12.414 16.414 12.75 16 12.75ZM16 8.75H11C10.586 8.75 10.25 8.414 10.25 8C10.25 7.586 10.586 7.25 11 7.25H16C16.414 7.25 16.75 7.586 16.75 8C16.75 8.414 16.414 8.75 16 8.75Z" fill="currentColor"/>
</svg>
<!-- <svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M17.63 3H6.38C4.13 3 3 4.12 3 6.37V17.62C3 19.87 4.13 21 6.38 21H17.63C19.88 21 21 19.87 21 17.62V6.37C21 4.12 19.88 3 17.63 3ZM9.75 13.69C10.05 13.98 10.05 14.46 9.75 14.75C9.61 14.9 9.41 14.97 9.22 14.97C9.03 14.97 8.84 14.9 8.69 14.75L6.47 12.53C6.18 12.24 6.18 11.76 6.47 11.47L8.69 9.25C8.98 8.95 9.46 8.95 9.75 9.25C10.05 9.54 10.05 10.02 9.75 10.31L8.06 12L9.75 13.69ZM13.73 8.17999L11.73 16.18C11.64 16.52 11.34 16.75 11 16.75C10.94 16.75 10.88 16.74 10.82 16.73C10.42 16.63 10.17 16.22 10.27 15.82L12.27 7.82001C12.37 7.42001 12.78 7.16999 13.18 7.26999C13.58 7.36999 13.83 7.77999 13.73 8.17999ZM17.53 12.53L15.31 14.75C15.16 14.9 14.97 14.97 14.78 14.97C14.59 14.97 14.39 14.9 14.25 14.75C13.95 14.46 13.95 13.98 14.25 13.69L15.94 12L14.25 10.31C13.95 10.02 13.95 9.54 14.25 9.25C14.54 8.95 15.02 8.95 15.31 9.25L17.53 11.47C17.82 11.76 17.82 12.24 17.53 12.53Z" fill="currentColor"/>
</svg> -->
<!-- <svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M14.75 6V3.75L19.25 8.25H17C15.42 8.25 14.75 7.58 14.75 6ZM20 9.75V18C20 20 19 21 17 21H8C6 21 5 20 5 18V6C5 4 6 3 8 3H13.25V6C13.25 8.42 14.58 9.75 17 9.75H20ZM9.06104 16L10.531 14.53C10.824 14.237 10.824 13.762 10.531 13.469C10.238 13.176 9.76297 13.176 9.46997 13.469L7.46997 15.469C7.17697 15.762 7.17697 16.237 7.46997 16.53L9.46997 18.53C9.61597 18.676 9.808 18.75 10 18.75C10.192 18.75 10.384 18.677 10.53 18.53C10.823 18.237 10.823 17.762 10.53 17.469L9.06104 16ZM15.53 15.47L13.53 13.47C13.237 13.177 12.762 13.177 12.469 13.47C12.176 13.763 12.176 14.238 12.469 14.531L13.939 16.001L12.469 17.471C12.176 17.764 12.176 18.239 12.469 18.532C12.615 18.678 12.807 18.752 12.999 18.752C13.191 18.752 13.3831 18.679 13.5291 18.532L15.5291 16.532C15.8231 16.238 15.823 15.762 15.53 15.47Z" fill="currentColor"/>
</svg> -->

After

Width:  |  Height:  |  Size: 3.0 KiB

View File

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

After

Width:  |  Height:  |  Size: 5.4 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 32 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 26 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 990 KiB

View File

@@ -1,8 +1,8 @@
import { TraceEvent } from '@uniswap/analytics'
import { BrowserEvent, InterfaceElementName, SharedEventName } from '@uniswap/analytics-events'
import { useIsDarkMode } from 'state/user/hooks'
import styled from 'styled-components/macro'
import { BREAKPOINTS, ExternalLink, StyledRouterLink } from 'theme'
import { useIsDarkMode } from 'theme/components/ThemeToggle'
import { DiscordIcon, GithubIcon, TwitterIcon } from './Icons'
import darkUnicornImgSrc from './images/unicornEmbossDark.png'
@@ -149,7 +149,7 @@ export const AboutFooter = () => {
<TextLink to="/swap">Swap</TextLink>
<TextLink to="/tokens">Tokens</TextLink>
<TextLink to="/nfts">NFTs</TextLink>
<TextLink to="/pool">Pools</TextLink>
<TextLink to="/pools">Pools</TextLink>
</LinkGroup>
<LinkGroup>
<LinkGroupTitle>Protocol</LinkGroupTitle>

View File

@@ -1,9 +1,9 @@
import { TraceEvent } from '@uniswap/analytics'
import { BrowserEvent, SharedEventName } from '@uniswap/analytics-events'
import { Link } from 'react-router-dom'
import { useIsDarkMode } from 'state/user/hooks'
import styled, { DefaultTheme } from 'styled-components/macro'
import { BREAKPOINTS } from 'theme'
import { useIsDarkMode } from 'theme/components/ThemeToggle'
export enum CardType {
Primary = 'Primary',

View File

@@ -1,7 +1,7 @@
import { ButtonEmpty } from 'components/Button'
import { useIsDarkMode } from 'state/user/hooks'
import styled from 'styled-components/macro'
import { BREAKPOINTS } from 'theme'
import { useIsDarkMode } from 'theme/components/ThemeToggle'
import meshSrc from './images/Mesh.png'

View File

@@ -50,7 +50,7 @@ export const MORE_CARDS = [
elementName: InterfaceElementName.ABOUT_PAGE_BUY_CRYPTO_CARD,
},
{
to: '/pool',
to: '/pools',
title: 'Earn',
description: 'Provide liquidity to pools on Uniswap and earn fees on swaps.',
lightIcon: <StyledCardLogo src={lightArrowImgSrc} alt="Analytics" />,

View File

@@ -1,67 +0,0 @@
import { useWeb3React } from '@web3-react/core'
import { CheckCircle, Triangle } from 'react-feather'
import styled from 'styled-components/macro'
import { useAllTransactions } from '../../state/transactions/hooks'
import { ExternalLink } from '../../theme'
import { ExplorerDataType, getExplorerLink } from '../../utils/getExplorerLink'
import Loader from '../Loader'
import { RowFixed } from '../Row'
import { TransactionSummary } from './TransactionSummary'
const TransactionStatusText = styled.div`
margin-right: 0.5rem;
display: flex;
align-items: center;
:hover {
text-decoration: underline;
}
`
const TransactionState = styled(ExternalLink)<{ pending: boolean; success?: boolean }>`
display: flex;
justify-content: space-between;
align-items: center;
text-decoration: none !important;
border-radius: 0.5rem;
padding: 0.25rem 0rem;
font-weight: 500;
font-size: 0.825rem;
color: ${({ theme }) => theme.accentAction};
`
const IconWrapper = styled.div<{ pending: boolean; success?: boolean }>`
color: ${({ pending, success, theme }) =>
pending ? theme.accentAction : success ? theme.accentSuccess : theme.accentFailure};
`
export default function Transaction({ hash }: { hash: string }) {
const { chainId } = useWeb3React()
const allTransactions = useAllTransactions()
const tx = allTransactions?.[hash]
const info = tx?.info
const pending = !tx?.receipt
const success = !pending && tx && (tx.receipt?.status === 1 || typeof tx.receipt?.status === 'undefined')
if (!chainId) return null
return (
<div>
<TransactionState
href={getExplorerLink(chainId, hash, ExplorerDataType.TRANSACTION)}
pending={pending}
success={success}
>
<RowFixed>
<TransactionStatusText>
<TransactionSummary info={info} />
</TransactionStatusText>
</RowFixed>
<IconWrapper pending={pending} success={success}>
{pending ? <Loader /> : success ? <CheckCircle size="16" /> : <Triangle size="16" />}
</IconWrapper>
</TransactionState>
</div>
)
}

View File

@@ -1,326 +0,0 @@
import { Trans } from '@lingui/macro'
import { useWeb3React } from '@web3-react/core'
import { getConnection, getConnectionName, getIsCoinbaseWallet, getIsMetaMaskWallet } from 'connection/utils'
import { useCallback } from 'react'
import { ExternalLink as LinkIcon } from 'react-feather'
import { useAppDispatch } from 'state/hooks'
import { updateSelectedWallet } from 'state/user/reducer'
import { removeConnectedWallet } from 'state/wallets/reducer'
import styled, { useTheme } from 'styled-components/macro'
import { flexColumnNoWrap, flexRowNoWrap } from 'theme/styles'
import { isMobile } from 'utils/userAgent'
import { ReactComponent as Close } from '../../assets/images/x.svg'
import { clearAllTransactions } from '../../state/transactions/reducer'
import { CopyHelper, ExternalLink, LinkStyledButton, ThemedText } from '../../theme'
import { shortenAddress } from '../../utils'
import { ExplorerDataType, getExplorerLink } from '../../utils/getExplorerLink'
import { ButtonSecondary } from '../Button'
import StatusIcon from '../Identicon/StatusIcon'
import { AutoRow } from '../Row'
import Transaction from './Transaction'
const HeaderRow = styled.div`
${flexRowNoWrap};
padding: 1rem 1rem;
font-weight: 500;
color: ${(props) => (props.color === 'blue' ? ({ theme }) => theme.accentAction : 'inherit')};
${({ theme }) => theme.deprecated_mediaWidth.deprecated_upToMedium`
padding: 1rem;
`};
`
const UpperSection = styled.div`
position: relative;
h5 {
margin: 0;
margin-bottom: 0.5rem;
font-size: 1rem;
font-weight: 400;
}
h5:last-child {
margin-bottom: 0px;
}
h4 {
margin-top: 0;
font-weight: 500;
}
`
const InfoCard = styled.div`
padding: 1rem;
border: 1px solid ${({ theme }) => theme.deprecated_bg3};
border-radius: 20px;
position: relative;
display: grid;
grid-row-gap: 12px;
margin-bottom: 20px;
`
const AccountGroupingRow = styled.div`
${flexColumnNoWrap};
justify-content: space-between;
align-items: center;
font-weight: 400;
color: ${({ theme }) => theme.textPrimary};
div {
${flexColumnNoWrap};
align-items: center;
}
`
const AccountSection = styled.div`
padding: 0rem 1rem;
${({ theme }) => theme.deprecated_mediaWidth.deprecated_upToMedium`padding: 0rem 1rem 1.5rem 1rem;`};
`
const YourAccount = styled.div`
h5 {
margin: 0 0 1rem 0;
font-weight: 400;
}
h4 {
margin: 0;
font-weight: 500;
}
`
const LowerSection = styled.div`
${flexColumnNoWrap};
padding: 1.5rem;
flex-grow: 1;
overflow: auto;
background-color: ${({ theme }) => theme.backgroundInteractive};
border-bottom-left-radius: 20px;
border-bottom-right-radius: 20px;
h5 {
margin: 0;
font-weight: 400;
color: ${({ theme }) => theme.textTertiary};
}
`
const AccountControl = styled.div`
display: flex;
justify-content: space-between;
min-width: 0;
width: 100%;
font-weight: 500;
font-size: 1.25rem;
a:hover {
text-decoration: underline;
}
p {
min-width: 0;
margin: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
`
const AddressLink = styled(ExternalLink)`
color: ${({ theme }) => theme.textTertiary};
margin-left: 1rem;
font-size: 0.825rem;
display: flex;
gap: 6px;
text-decoration: none !important;
:hover {
color: ${({ theme }) => theme.textSecondary};
}
`
const CloseIcon = styled.div`
position: absolute;
right: 1rem;
top: 14px;
&:hover {
cursor: pointer;
opacity: ${({ theme }) => theme.opacity.hover};
}
`
const CloseColor = styled(Close)`
path {
stroke: ${({ theme }) => theme.deprecated_text4};
}
`
const WalletName = styled.div`
width: initial;
font-size: 0.825rem;
font-weight: 500;
color: ${({ theme }) => theme.textTertiary};
`
const TransactionListWrapper = styled.div`
${flexColumnNoWrap};
`
const WalletAction = styled(ButtonSecondary)`
width: fit-content;
font-weight: 400;
margin-left: 8px;
font-size: 0.825rem;
padding: 4px 6px;
:hover {
cursor: pointer;
text-decoration: underline;
}
`
function renderTransactions(transactions: string[]) {
return (
<TransactionListWrapper>
{transactions.map((hash, i) => {
return <Transaction key={i} hash={hash} />
})}
</TransactionListWrapper>
)
}
interface AccountDetailsProps {
toggleWalletModal: () => void
pendingTransactions: string[]
confirmedTransactions: string[]
ENSName?: string
openOptions: () => void
}
export default function AccountDetails({
toggleWalletModal,
pendingTransactions,
confirmedTransactions,
ENSName,
openOptions,
}: AccountDetailsProps) {
const { chainId, account, connector } = useWeb3React()
const connectionType = getConnection(connector).type
const theme = useTheme()
const dispatch = useAppDispatch()
const hasMetaMaskExtension = getIsMetaMaskWallet()
const hasCoinbaseExtension = getIsCoinbaseWallet()
const isInjectedMobileBrowser = (hasMetaMaskExtension || hasCoinbaseExtension) && isMobile
function formatConnectorName() {
return (
<WalletName>
<Trans>Connected with</Trans> {getConnectionName(connectionType, hasMetaMaskExtension)}
</WalletName>
)
}
const clearAllTransactionsCallback = useCallback(() => {
if (chainId) dispatch(clearAllTransactions({ chainId }))
}, [dispatch, chainId])
return (
<>
<UpperSection>
<CloseIcon onClick={toggleWalletModal}>
<CloseColor />
</CloseIcon>
<HeaderRow>
<Trans>Account</Trans>
</HeaderRow>
<AccountSection>
<YourAccount>
<InfoCard>
<AccountGroupingRow>
{formatConnectorName()}
<div>
{!isInjectedMobileBrowser && (
<>
<WalletAction
style={{ fontSize: '.825rem', fontWeight: 400, marginRight: '8px' }}
onClick={() => {
const walletType = getConnectionName(getConnection(connector).type)
if (connector.deactivate) {
connector.deactivate()
} else {
connector.resetState()
}
dispatch(updateSelectedWallet({ wallet: undefined }))
dispatch(removeConnectedWallet({ account, walletType }))
openOptions()
}}
>
<Trans>Disconnect</Trans>
</WalletAction>
<WalletAction
style={{ fontSize: '.825rem', fontWeight: 400 }}
onClick={() => {
openOptions()
}}
>
<Trans>Change</Trans>
</WalletAction>
</>
)}
</div>
</AccountGroupingRow>
<AccountGroupingRow data-testid="web3-account-identifier-row">
<AccountControl>
<div>
<StatusIcon connectionType={connectionType} />
<p>{ENSName ? ENSName : account && shortenAddress(account)}</p>
</div>
</AccountControl>
</AccountGroupingRow>
<AccountGroupingRow>
<AccountControl>
<div>
{account && (
<CopyHelper toCopy={account} gap={6} iconSize={16} fontSize={14}>
<Trans>Copy Address</Trans>
</CopyHelper>
)}
{chainId && account && (
<AddressLink href={getExplorerLink(chainId, ENSName ?? account, ExplorerDataType.ADDRESS)}>
<LinkIcon size={16} />
<Trans>View on Explorer</Trans>
</AddressLink>
)}
</div>
</AccountControl>
</AccountGroupingRow>
</InfoCard>
</YourAccount>
</AccountSection>
</UpperSection>
{!!pendingTransactions.length || !!confirmedTransactions.length ? (
<LowerSection>
<AutoRow mb="1rem" style={{ justifyContent: 'space-between' }}>
<ThemedText.DeprecatedBody>
<Trans>Recent Transactions</Trans>
</ThemedText.DeprecatedBody>
<LinkStyledButton onClick={clearAllTransactionsCallback}>
<Trans>(clear all)</Trans>
</LinkStyledButton>
</AutoRow>
{renderTransactions(pendingTransactions)}
{renderTransactions(confirmedTransactions)}
</LowerSection>
) : (
<LowerSection>
<ThemedText.DeprecatedBody color={theme.textPrimary}>
<Trans>Your transactions will appear here...</Trans>
</ThemedText.DeprecatedBody>
</LowerSection>
)}
</>
)
}

View File

@@ -1,94 +0,0 @@
import { useWeb3React } from '@web3-react/core'
import { UNI_ADDRESS } from 'constants/addresses'
import { TransactionInfo, TransactionType } from 'state/transactions/types'
import styled, { css } from 'styled-components/macro'
import { nativeOnChain } from '../../constants/tokens'
import { useCurrency } from '../../hooks/Tokens'
import CurrencyLogo from '../Logo/CurrencyLogo'
const CurrencyWrap = styled.div`
position: relative;
width: 36px;
height: 36px;
`
const CurrencyWrapStyles = css`
position: absolute;
height: 24px;
`
const CurrencyLogoWrap = styled.span<{ isCentered: boolean }>`
${CurrencyWrapStyles};
left: ${({ isCentered }) => (isCentered ? '50%' : '0')};
top: ${({ isCentered }) => (isCentered ? '50%' : '0')};
transform: ${({ isCentered }) => isCentered && 'translate(-50%, -50%)'};
`
const CurrencyLogoWrapTwo = styled.span`
${CurrencyWrapStyles};
bottom: 0px;
right: 0px;
`
interface CurrencyPair {
currencyId0: string | undefined
currencyId1: string | undefined
}
const getCurrency = ({ info, chainId }: { info: TransactionInfo; chainId: number | undefined }): CurrencyPair => {
switch (info.type) {
case TransactionType.ADD_LIQUIDITY_V3_POOL:
case TransactionType.REMOVE_LIQUIDITY_V3:
case TransactionType.CREATE_V3_POOL: {
const { baseCurrencyId, quoteCurrencyId } = info
return { currencyId0: baseCurrencyId, currencyId1: quoteCurrencyId }
}
case TransactionType.SWAP: {
const { inputCurrencyId, outputCurrencyId } = info
return { currencyId0: inputCurrencyId, currencyId1: outputCurrencyId }
}
case TransactionType.WRAP: {
const { unwrapped } = info
const native = info.chainId ? nativeOnChain(info.chainId) : undefined
const base = 'ETH'
const wrappedCurrency = native?.wrapped.address ?? 'WETH'
return { currencyId0: unwrapped ? wrappedCurrency : base, currencyId1: unwrapped ? base : wrappedCurrency }
}
case TransactionType.COLLECT_FEES: {
const { currencyId0, currencyId1 } = info
return { currencyId0, currencyId1 }
}
case TransactionType.APPROVAL: {
return { currencyId0: info.tokenAddress, currencyId1: undefined }
}
case TransactionType.CLAIM: {
const uniAddress = chainId ? UNI_ADDRESS[chainId] : undefined
return { currencyId0: uniAddress, currencyId1: undefined }
}
default:
return { currencyId0: undefined, currencyId1: undefined }
}
}
const LogoView = ({ info }: { info: TransactionInfo }) => {
const { chainId } = useWeb3React()
const { currencyId0, currencyId1 } = getCurrency({ info, chainId })
const currency0 = useCurrency(currencyId0)
const currency1 = useCurrency(currencyId1)
const isCentered = !(currency0 && currency1)
return (
<CurrencyWrap>
<CurrencyLogoWrap isCentered={isCentered}>
<CurrencyLogo size="24px" currency={currency0} />
</CurrencyLogoWrap>
{!isCentered && (
<CurrencyLogoWrapTwo>
<CurrencyLogo size="24px" currency={currency1} />
</CurrencyLogoWrapTwo>
)}
</CurrencyWrap>
)
}
export default LogoView

View File

@@ -1,336 +0,0 @@
import { Trans } from '@lingui/macro'
import { Fraction, TradeType } from '@uniswap/sdk-core'
import JSBI from 'jsbi'
import {
AddLiquidityV3PoolTransactionInfo,
ApproveTransactionInfo,
ClaimTransactionInfo,
CollectFeesTransactionInfo,
ExactInputSwapTransactionInfo,
ExactOutputSwapTransactionInfo,
RemoveLiquidityV3TransactionInfo,
TransactionInfo,
TransactionType,
WrapTransactionInfo,
} from 'state/transactions/types'
import styled from 'styled-components/macro'
import { nativeOnChain } from '../../constants/tokens'
import { useCurrency, useToken } from '../../hooks/Tokens'
import useENSName from '../../hooks/useENSName'
import { shortenAddress } from '../../utils'
import { TransactionState } from './index'
const HighlightText = styled.span`
color: ${({ theme }) => theme.textPrimary};
font-weight: 600;
`
const BodyWrap = styled.div`
line-height: 20px;
`
interface ActionProps {
pending: JSX.Element
success: JSX.Element
failed: JSX.Element
transactionState: TransactionState
}
const Action = ({ pending, success, failed, transactionState }: ActionProps) => {
switch (transactionState) {
case TransactionState.Failed:
return failed
case TransactionState.Success:
return success
default:
return pending
}
}
const formatAmount = (amountRaw: string, decimals: number, sigFigs: number): string =>
new Fraction(amountRaw, JSBI.exponentiate(JSBI.BigInt(10), JSBI.BigInt(decimals))).toSignificant(sigFigs)
const FailedText = ({ transactionState }: { transactionState: TransactionState }) =>
transactionState === TransactionState.Failed ? <Trans>failed</Trans> : <span />
const FormattedCurrencyAmount = ({
rawAmount,
currencyId,
}: {
rawAmount: string
currencyId: string
sigFigs: number
}) => {
const currency = useCurrency(currencyId)
return currency ? (
<HighlightText>
{formatAmount(rawAmount, currency.decimals, /* sigFigs= */ 6)} {currency.symbol}
</HighlightText>
) : null
}
const getRawAmounts = (
info: ExactInputSwapTransactionInfo | ExactOutputSwapTransactionInfo
): { rawAmountFrom: string; rawAmountTo: string } => {
return info.tradeType === TradeType.EXACT_INPUT
? { rawAmountFrom: info.inputCurrencyAmountRaw, rawAmountTo: info.expectedOutputCurrencyAmountRaw }
: { rawAmountFrom: info.expectedInputCurrencyAmountRaw, rawAmountTo: info.outputCurrencyAmountRaw }
}
const SwapSummary = ({
info,
transactionState,
}: {
info: ExactInputSwapTransactionInfo | ExactOutputSwapTransactionInfo
transactionState: TransactionState
}) => {
const actionProps = {
transactionState,
pending: <Trans>Swapping</Trans>,
success: <Trans>Swapped</Trans>,
failed: <Trans>Swap</Trans>,
}
const { rawAmountFrom, rawAmountTo } = getRawAmounts(info)
return (
<BodyWrap>
<Action {...actionProps} />{' '}
<FormattedCurrencyAmount rawAmount={rawAmountFrom} currencyId={info.inputCurrencyId} sigFigs={2} />{' '}
<Trans>for </Trans>{' '}
<FormattedCurrencyAmount rawAmount={rawAmountTo} currencyId={info.outputCurrencyId} sigFigs={2} />{' '}
<FailedText transactionState={transactionState} />
</BodyWrap>
)
}
const AddLiquidityV3PoolSummary = ({
info,
transactionState,
}: {
info: AddLiquidityV3PoolTransactionInfo
transactionState: TransactionState
}) => {
const { createPool, quoteCurrencyId, baseCurrencyId } = info
const actionProps = {
transactionState,
pending: <Trans>Adding</Trans>,
success: <Trans>Added</Trans>,
failed: <Trans>Add</Trans>,
}
return (
<BodyWrap>
{createPool ? (
<CreateV3PoolSummary info={info} transactionState={transactionState} />
) : (
<>
<Action {...actionProps} />{' '}
<FormattedCurrencyAmount rawAmount={info.expectedAmountBaseRaw} currencyId={baseCurrencyId} sigFigs={2} />{' '}
<Trans>and</Trans>{' '}
<FormattedCurrencyAmount rawAmount={info.expectedAmountQuoteRaw} currencyId={quoteCurrencyId} sigFigs={2} />
</>
)}{' '}
<FailedText transactionState={transactionState} />
</BodyWrap>
)
}
const RemoveLiquidityV3Summary = ({
info: { baseCurrencyId, quoteCurrencyId, expectedAmountBaseRaw, expectedAmountQuoteRaw },
transactionState,
}: {
info: RemoveLiquidityV3TransactionInfo
transactionState: TransactionState
}) => {
const actionProps = {
transactionState,
pending: <Trans>Removing</Trans>,
success: <Trans>Removed</Trans>,
failed: <Trans>Remove</Trans>,
}
return (
<BodyWrap>
<Action {...actionProps} />{' '}
<FormattedCurrencyAmount rawAmount={expectedAmountBaseRaw} currencyId={baseCurrencyId} sigFigs={2} />{' '}
<Trans>and</Trans>{' '}
<FormattedCurrencyAmount rawAmount={expectedAmountQuoteRaw} currencyId={quoteCurrencyId} sigFigs={2} />{' '}
<FailedText transactionState={transactionState} />
</BodyWrap>
)
}
const CreateV3PoolSummary = ({
info: { baseCurrencyId, quoteCurrencyId },
transactionState,
}: {
info: AddLiquidityV3PoolTransactionInfo
transactionState: TransactionState
}) => {
const baseCurrency = useCurrency(baseCurrencyId)
const quoteCurrency = useCurrency(quoteCurrencyId)
const actionProps = {
transactionState,
pending: <Trans>Creating</Trans>,
success: <Trans>Created</Trans>,
failed: <Trans>Create</Trans>,
}
return (
<BodyWrap>
<Action {...actionProps} />{' '}
<HighlightText>
{baseCurrency?.symbol}/{quoteCurrency?.symbol}{' '}
</HighlightText>
<Trans>Pool</Trans> <FailedText transactionState={transactionState} />
</BodyWrap>
)
}
const CollectFeesSummary = ({
info,
transactionState,
}: {
info: CollectFeesTransactionInfo
transactionState: TransactionState
}) => {
const { currencyId0, expectedCurrencyOwed0 = '0', expectedCurrencyOwed1 = '0', currencyId1 } = info
const actionProps = {
transactionState,
pending: <Trans>Collecting</Trans>,
success: <Trans>Collected</Trans>,
failed: <Trans>Collect</Trans>,
}
return (
<BodyWrap>
<Action {...actionProps} />{' '}
<FormattedCurrencyAmount rawAmount={expectedCurrencyOwed0} currencyId={currencyId0} sigFigs={2} />{' '}
<Trans>and</Trans>{' '}
<FormattedCurrencyAmount rawAmount={expectedCurrencyOwed1} currencyId={currencyId1} sigFigs={2} />{' '}
<Trans>fees</Trans> <FailedText transactionState={transactionState} />
</BodyWrap>
)
}
const ApprovalSummary = ({
info,
transactionState,
}: {
info: ApproveTransactionInfo
transactionState: TransactionState
}) => {
const token = useToken(info.tokenAddress)
const actionProps = {
transactionState,
pending: <Trans>Approving</Trans>,
success: <Trans>Approved</Trans>,
failed: <Trans>Approve</Trans>,
}
return (
<BodyWrap>
<Action {...actionProps} /> <HighlightText>{token?.symbol}</HighlightText>{' '}
<FailedText transactionState={transactionState} />
</BodyWrap>
)
}
const ClaimSummary = ({
info: { recipient, uniAmountRaw },
transactionState,
}: {
info: ClaimTransactionInfo
transactionState: TransactionState
}) => {
const { ENSName } = useENSName()
const actionProps = {
transactionState,
pending: <Trans>Claiming</Trans>,
success: <Trans>Claimed</Trans>,
failed: <Trans>Claim</Trans>,
}
return (
<BodyWrap>
{uniAmountRaw && (
<>
<Action {...actionProps} />{' '}
<HighlightText>
{formatAmount(uniAmountRaw, 18, 4)}
UNI{' '}
</HighlightText>{' '}
<Trans>for</Trans> <HighlightText>{ENSName ?? shortenAddress(recipient)}</HighlightText>
</>
)}{' '}
<FailedText transactionState={transactionState} />
</BodyWrap>
)
}
const WrapSummary = ({
info: { chainId, currencyAmountRaw, unwrapped },
transactionState,
}: {
info: WrapTransactionInfo
transactionState: TransactionState
}) => {
const native = chainId ? nativeOnChain(chainId) : undefined
const from = unwrapped ? native?.wrapped.symbol ?? 'WETH' : native?.symbol ?? 'ETH'
const to = unwrapped ? native?.symbol ?? 'ETH' : native?.wrapped.symbol ?? 'WETH'
const amount = formatAmount(currencyAmountRaw, 18, 6)
const actionProps = unwrapped
? {
transactionState,
pending: <Trans>Unwrapping</Trans>,
success: <Trans>Unwrapped</Trans>,
failed: <Trans>Unwrap</Trans>,
}
: {
transactionState,
pending: <Trans>Wrapping</Trans>,
success: <Trans>Wrapped</Trans>,
failed: <Trans>Wrap</Trans>,
}
return (
<BodyWrap>
<Action {...actionProps} />{' '}
<HighlightText>
{amount} {from}
</HighlightText>{' '}
<Trans>to</Trans>{' '}
<HighlightText>
{amount} {to}
</HighlightText>{' '}
<FailedText transactionState={transactionState} />
</BodyWrap>
)
}
const TransactionBody = ({ info, transactionState }: { info: TransactionInfo; transactionState: TransactionState }) => {
switch (info.type) {
case TransactionType.SWAP:
return <SwapSummary info={info} transactionState={transactionState} />
case TransactionType.ADD_LIQUIDITY_V3_POOL:
return <AddLiquidityV3PoolSummary info={info} transactionState={transactionState} />
case TransactionType.REMOVE_LIQUIDITY_V3:
return <RemoveLiquidityV3Summary info={info} transactionState={transactionState} />
case TransactionType.WRAP:
return <WrapSummary info={info} transactionState={transactionState} />
case TransactionType.COLLECT_FEES:
return <CollectFeesSummary info={info} transactionState={transactionState} />
case TransactionType.APPROVAL:
return <ApprovalSummary info={info} transactionState={transactionState} />
case TransactionType.CLAIM:
return <ClaimSummary info={info} transactionState={transactionState} />
default:
return <span />
}
}
export default TransactionBody

View File

@@ -1,98 +0,0 @@
import { useWeb3React } from '@web3-react/core'
import { getChainInfoOrDefault } from 'constants/chainInfo'
import { SupportedChainId } from 'constants/chains'
import { useMemo } from 'react'
import { AlertTriangle, CheckCircle } from 'react-feather'
import styled from 'styled-components/macro'
import { ExternalLink } from 'theme'
import { colors } from 'theme/colors'
import { TransactionDetails } from '../../state/transactions/types'
import Loader from '../Loader'
import LogoView from './LogoView'
import TransactionBody from './TransactionBody'
export enum TransactionState {
Pending,
Success,
Failed,
}
const Grid = styled(ExternalLink)<{ isLastTransactionInList?: boolean }>`
cursor: pointer;
display: grid;
grid-template-columns: 44px auto 24px;
width: 100%;
text-decoration: none;
border-bottom: ${({ theme, isLastTransactionInList }) =>
isLastTransactionInList ? 'none' : `1px solid ${theme.backgroundOutline}`};
padding: 12px;
&:hover {
background-color: ${({ theme }) => theme.backgroundModule};
transition: 250ms background-color ease;
}
`
const TextContainer = styled.span`
font-size: 14px;
margin-top: auto;
margin-bottom: auto;
color: ${({ theme }) => theme.textTertiary};
`
const IconStyleWrap = styled.span`
margin-top: auto;
margin-bottom: auto;
margin-left: auto;
height: 16px;
`
export const TransactionSummary = ({
transactionDetails,
isLastTransactionInList = false,
}: {
transactionDetails: TransactionDetails
isLastTransactionInList?: boolean
}) => {
const { chainId = 1 } = useWeb3React()
const tx = transactionDetails
const { explorer } = getChainInfoOrDefault(chainId ? chainId : SupportedChainId.MAINNET)
const { info, receipt, hash } = tx
const transactionState = useMemo(() => {
const pending = !receipt
const success = !pending && tx && (receipt?.status === 1 || typeof receipt?.status === 'undefined')
const transactionState = pending
? TransactionState.Pending
: success
? TransactionState.Success
: TransactionState.Failed
return transactionState
}, [receipt, tx])
const link = `${explorer}tx/${hash}`
return chainId ? (
<Grid href={link} target="_blank" isLastTransactionInList={isLastTransactionInList}>
<LogoView info={info} />
<TextContainer as="span">
<TransactionBody info={info} transactionState={transactionState} />
</TextContainer>
{transactionState === TransactionState.Pending ? (
<IconStyleWrap>
<Loader />
</IconStyleWrap>
) : transactionState === TransactionState.Success ? (
<IconStyleWrap>
<CheckCircle color={colors.green200} size="16px" />
</IconStyleWrap>
) : (
<IconStyleWrap>
<AlertTriangle color={colors.gold200} size="16px" />
</IconStyleWrap>
)}
</Grid>
) : null
}

View File

@@ -1,65 +1,56 @@
import { Trans } from '@lingui/macro'
import { sendAnalyticsEvent } from '@uniswap/analytics'
import { InterfaceEventName } from '@uniswap/analytics-events'
import { formatUSDPrice } from '@uniswap/conedison/format'
import { sendAnalyticsEvent, TraceEvent } from '@uniswap/analytics'
import { BrowserEvent, InterfaceElementName, InterfaceEventName, SharedEventName } from '@uniswap/analytics-events'
import { formatNumber, NumberType } from '@uniswap/conedison/format'
import { CurrencyAmount, Token } from '@uniswap/sdk-core'
import { useWeb3React } from '@web3-react/core'
import { ButtonEmphasis, ButtonSize, LoadingButtonSpinner, ThemeButton } from 'components/Button'
import Column from 'components/Column'
import { AutoRow } from 'components/Row'
import { LoadingBubble } from 'components/Tokens/loading'
import { formatDelta } from 'components/Tokens/TokenDetails/PriceChart'
import Tooltip from 'components/Tooltip'
import { getConnection } from 'connection/utils'
import { getChainInfoOrDefault } from 'constants/chainInfo'
import { SupportedChainId } from 'constants/chains'
import useCopyClipboard from 'hooks/useCopyClipboard'
import useStablecoinPrice from 'hooks/useStablecoinPrice'
import useNativeCurrency from 'lib/hooks/useNativeCurrency'
import { useGetConnection } from 'connection'
import { usePortfolioBalancesQuery } from 'graphql/data/__generated__/types-and-hooks'
import { useAtomValue } from 'jotai/utils'
import { useProfilePageState, useSellAsset, useWalletCollections } from 'nft/hooks'
import { useIsNftClaimAvailable } from 'nft/hooks/useIsNftClaimAvailable'
import { ProfilePageStateType } from 'nft/types'
import { useCallback, useMemo, useState } from 'react'
import { Copy, CreditCard, ExternalLink as ExternalLinkIcon, Info, Power } from 'react-feather'
import { useCallback, useState } from 'react'
import { ArrowDownRight, ArrowUpRight, Copy, CreditCard, IconProps, Info, Power, Settings } from 'react-feather'
import { useNavigate } from 'react-router-dom'
import { useCurrencyBalanceString } from 'state/connection/hooks'
import { shouldDisableNFTRoutesAtom } from 'state/application/atoms'
import { useAppDispatch } from 'state/hooks'
import { updateSelectedWallet } from 'state/user/reducer'
import styled, { css, keyframes } from 'styled-components/macro'
import { ExternalLink, ThemedText } from 'theme'
import styled, { useTheme } from 'styled-components/macro'
import { CopyHelper, ExternalLink, ThemedText } from 'theme'
import { shortenAddress } from '../../nft/utils/address'
import { useCloseModal, useFiatOnrampAvailability, useOpenModal, useToggleModal } from '../../state/application/hooks'
import { ApplicationModal } from '../../state/application/reducer'
import { useUserHasAvailableClaim, useUserUnclaimedAmount } from '../../state/claim/hooks'
import StatusIcon from '../Identicon/StatusIcon'
import { useToggleAccountDrawer } from '.'
import IconButton, { IconHoverText } from './IconButton'
import MiniPortfolio from './MiniPortfolio'
import { portfolioFadeInAnimation } from './MiniPortfolio/PortfolioRow'
const BuyCryptoButtonBorderKeyframes = keyframes`
0% {
border-color: transparent;
}
33% {
border-color: hsla(225, 95%, 63%, 1);
}
66% {
border-color: hsla(267, 95%, 63%, 1);
}
100% {
border-color: transparent;
}
const AuthenticatedHeaderWrapper = styled.div`
padding: 20px 16px;
display: flex;
flex-direction: column;
flex: 1;
`
const BuyCryptoButton = styled(ThemeButton)`
const HeaderButton = styled(ThemeButton)`
border-color: transparent;
border-radius: 12px;
border-style: solid;
border-width: 1px;
height: 40px;
margin-top: 8px;
animation-direction: alternate;
animation-duration: ${({ theme }) => theme.transition.duration.slow};
animation-fill-mode: none;
animation-iteration-count: 2;
animation-name: ${BuyCryptoButtonBorderKeyframes};
animation-timing-function: ${({ theme }) => theme.transition.timing.inOut};
`
const WalletButton = styled(ThemeButton)`
border-radius: 12px;
padding-top: 10px;
@@ -69,22 +60,16 @@ const WalletButton = styled(ThemeButton)`
border: none;
`
const ProfileButton = styled(WalletButton)`
background: ${({ theme }) => theme.accentAction};
transition: ${({ theme }) => theme.transition.duration.fast} ${({ theme }) => theme.transition.timing.ease}
background-color;
`
const UNIButton = styled(WalletButton)`
border-radius: 12px;
padding-top: 10px;
padding-bottom: 10px;
margin-top: 4px;
color: white;
border: none;
background: linear-gradient(to right, #9139b0 0%, #4261d6 100%);
`
const Column = styled.div`
display: flex;
flex-direction: column;
text-align: center;
`
const IconContainer = styled.div`
display: flex;
align-items: center;
@@ -100,13 +85,6 @@ const IconContainer = styled.div`
}
}
`
const USDText = styled.div`
font-size: 16px;
font-weight: 500;
color: ${({ theme }) => theme.textSecondary};
margin-top: 8px;
`
const FiatOnrampNotAvailableText = styled(ThemedText.Caption)`
align-items: center;
color: ${({ theme }) => theme.textSecondary};
@@ -122,34 +100,23 @@ const FiatOnrampAvailabilityExternalLink = styled(ExternalLink)`
width: 14px;
`
const TruncatedTextStyle = css`
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
`
const FlexContainer = styled.div`
${TruncatedTextStyle}
const StatusWrapper = styled.div`
display: inline-block;
width: 70%;
padding-right: 4px;
display: inline-flex;
`
const AccountNamesWrapper = styled.div`
min-width: 0;
margin-right: 8px;
overflow: hidden;
white-space: nowrap;
display: flex;
width: 100%;
flex-direction: column;
justify-content: center;
gap: 4px;
`
const ENSNameContainer = styled(ThemedText.SubHeader)`
${TruncatedTextStyle}
color: ${({ theme }) => theme.textPrimary};
margin-top: 2.5px;
`
const AccountContainer = styled(ThemedText.BodySmall)`
${TruncatedTextStyle}
color: ${({ theme }) => theme.textSecondary};
margin-top: 2.5px;
`
const StyledInfoIcon = styled(Info)`
height: 12px;
width: 12px;
@@ -158,31 +125,42 @@ const StyledInfoIcon = styled(Info)`
const StyledLoadingButtonSpinner = styled(LoadingButtonSpinner)`
fill: ${({ theme }) => theme.accentAction};
`
const BalanceWrapper = styled.div`
display: flex;
flex-direction: column;
gap: 8px;
padding: 16px 0;
`
const HeaderWrapper = styled.div`
margin-bottom: 12px;
margin-bottom: 20px;
display: flex;
justify-content: space-between;
align-items: flex-start;
`
const AuthenticatedHeader = () => {
const { account, chainId, connector, ENSName } = useWeb3React()
const [isCopied, setCopied] = useCopyClipboard()
const copy = useCallback(() => {
setCopied(account || '')
}, [account, setCopied])
const CopyText = styled(CopyHelper).attrs({
InitialIcon: Copy,
CopiedIcon: Copy,
gap: 4,
iconSize: 14,
iconPosition: 'right',
})``
const FadeInColumn = styled(Column)`
${portfolioFadeInAnimation}
`
const PortfolioDrawerContainer = styled(Column)`
flex: 1;
`
export function PortfolioArrow({ change, ...rest }: { change: number } & IconProps) {
const theme = useTheme()
return change < 0 ? (
<ArrowDownRight color={theme.accentCritical} size={20} {...rest} />
) : (
<ArrowUpRight color={theme.accentSuccess} size={20} {...rest} />
)
}
export default function AuthenticatedHeader({ account, openSettings }: { account: string; openSettings: () => void }) {
const { connector, ENSName } = useWeb3React()
const dispatch = useAppDispatch()
const balanceString = useCurrencyBalanceString(account ?? '')
const {
nativeCurrency: { symbol: nativeCurrencySymbol },
explorer,
} = getChainInfoOrDefault(chainId ? chainId : SupportedChainId.MAINNET)
const navigate = useNavigate()
const closeModal = useCloseModal()
const setSellPageState = useProfilePageState((state) => state.setProfilePageState)
@@ -190,11 +168,12 @@ const AuthenticatedHeader = () => {
const clearCollectionFilters = useWalletCollections((state) => state.clearCollectionFilters)
const isClaimAvailable = useIsNftClaimAvailable((state) => state.isClaimAvailable)
const shouldDisableNFTRoutes = useAtomValue(shouldDisableNFTRoutesAtom)
const unclaimedAmount: CurrencyAmount<Token> | undefined = useUserUnclaimedAmount(account)
const isUnclaimed = useUserHasAvailableClaim(account)
const connectionType = getConnection(connector).type
const nativeCurrency = useNativeCurrency()
const nativeCurrencyPrice = useStablecoinPrice(nativeCurrency ?? undefined)
const getConnection = useGetConnection()
const connection = getConnection(connector)
const openClaimModal = useToggleModal(ApplicationModal.ADDRESS_CLAIM)
const openNftModal = useToggleModal(ApplicationModal.UNISWAP_NFT_AIRDROP_CLAIM)
const disconnect = useCallback(() => {
@@ -205,26 +184,23 @@ const AuthenticatedHeader = () => {
dispatch(updateSelectedWallet({ wallet: undefined }))
}, [connector, dispatch])
const amountUSD = useMemo(() => {
if (!nativeCurrencyPrice || !balanceString) return undefined
const price = parseFloat(nativeCurrencyPrice.toFixed(5))
const balance = parseFloat(balanceString)
return price * balance
}, [balanceString, nativeCurrencyPrice])
const toggleWalletDrawer = useToggleAccountDrawer()
const navigateToProfile = useCallback(() => {
toggleWalletDrawer()
resetSellAssets()
setSellPageState(ProfilePageStateType.VIEWING)
clearCollectionFilters()
navigate('/nfts/profile')
closeModal()
}, [clearCollectionFilters, closeModal, navigate, resetSellAssets, setSellPageState])
}, [clearCollectionFilters, closeModal, navigate, resetSellAssets, setSellPageState, toggleWalletDrawer])
const openFiatOnrampModal = useOpenModal(ApplicationModal.FIAT_ONRAMP)
const openFoRModalWithAnalytics = useCallback(() => {
toggleWalletDrawer()
sendAnalyticsEvent(InterfaceEventName.FIAT_ONRAMP_WIDGET_OPENED)
openFiatOnrampModal()
}, [openFiatOnrampModal])
}, [openFiatOnrampModal, toggleWalletDrawer])
const [shouldCheck, setShouldCheck] = useState(false)
const {
@@ -248,53 +224,86 @@ const AuthenticatedHeader = () => {
const openFiatOnrampUnavailableTooltip = useCallback(() => setShow(true), [setShow])
const closeFiatOnrampUnavailableTooltip = useCallback(() => setShow(false), [setShow])
const { data: portfolioBalances } = usePortfolioBalancesQuery({
variables: { ownerAddress: account ?? '' },
fetchPolicy: 'cache-only', // PrefetchBalancesWrapper handles balance fetching/staleness; this component only reads from cache
})
const portfolio = portfolioBalances?.portfolios?.[0]
const totalBalance = portfolio?.tokensTotalDenominatedValue?.value
const absoluteChange = portfolio?.tokensTotalDenominatedValueChange?.absolute?.value
const percentChange = portfolio?.tokensTotalDenominatedValueChange?.percentage?.value
return (
<>
<AuthenticatedHeaderWrapper>
<HeaderWrapper>
<FlexContainer>
<StatusIcon connectionType={connectionType} size={24} />
{ENSName ? (
<StatusWrapper>
<StatusIcon connection={connection} size={40} />
{account && (
<AccountNamesWrapper>
<ENSNameContainer>{ENSName}</ENSNameContainer>
<AccountContainer>{account && shortenAddress(account, 2, 4)}</AccountContainer>
<ThemedText.SubHeader color="textPrimary" fontWeight={500}>
<CopyText toCopy={ENSName ?? account}>{ENSName ?? shortenAddress(account, 4, 4)}</CopyText>
</ThemedText.SubHeader>
{/* Displays smaller view of account if ENS name was rendered above */}
{ENSName && (
<ThemedText.BodySmall color="textTertiary">
<CopyText toCopy={account}>{shortenAddress(account, 4, 4)}</CopyText>
</ThemedText.BodySmall>
)}
</AccountNamesWrapper>
) : (
<ThemedText.SubHeader marginTop="2.5px">{account && shortenAddress(account, 2, 4)}</ThemedText.SubHeader>
)}
</FlexContainer>
</StatusWrapper>
<IconContainer>
<IconButton onClick={copy} Icon={Copy}>
{isCopied ? <Trans>Copied!</Trans> : <Trans>Copy</Trans>}
</IconButton>
<IconButton href={`${explorer}address/${account}`} target="_blank" Icon={ExternalLinkIcon}>
<Trans>Explore</Trans>
</IconButton>
<IconButton data-testid="wallet-disconnect" onClick={disconnect} Icon={Power}>
<Trans>Disconnect</Trans>
</IconButton>
<IconButton data-testid="wallet-settings" onClick={openSettings} Icon={Settings} />
<TraceEvent
events={[BrowserEvent.onClick]}
name={SharedEventName.ELEMENT_CLICKED}
element={InterfaceElementName.DISCONNECT_WALLET_BUTTON}
>
<IconButton data-testid="wallet-disconnect" onClick={disconnect} Icon={Power} />
</TraceEvent>
</IconContainer>
</HeaderWrapper>
<Column>
<BalanceWrapper>
<ThemedText.SubHeaderSmall>ETH Balance</ThemedText.SubHeaderSmall>
<ThemedText.HeadlineLarge fontSize={36} fontWeight={400}>
{balanceString} {nativeCurrencySymbol}
</ThemedText.HeadlineLarge>
{amountUSD !== undefined && <USDText>{formatUSDPrice(amountUSD)} USD</USDText>}
</BalanceWrapper>
<ProfileButton
data-testid="nft-view-self-nfts"
onClick={navigateToProfile}
size={ButtonSize.medium}
emphasis={ButtonEmphasis.medium}
>
<Trans>View and sell NFTs</Trans>
</ProfileButton>
<BuyCryptoButton
<PortfolioDrawerContainer>
{totalBalance !== undefined ? (
<FadeInColumn gap="xs">
<ThemedText.HeadlineLarge fontWeight={500}>
{formatNumber(totalBalance, NumberType.PortfolioBalance)}
</ThemedText.HeadlineLarge>
<AutoRow marginBottom="20px">
{absoluteChange !== 0 && percentChange && (
<>
<PortfolioArrow change={absoluteChange as number} />
<ThemedText.BodySecondary>
{`${formatNumber(Math.abs(absoluteChange as number), NumberType.PortfolioBalance)} (${formatDelta(
percentChange
)})`}
</ThemedText.BodySecondary>
</>
)}
</AutoRow>
</FadeInColumn>
) : (
<Column gap="xs">
<LoadingBubble height="44px" width="170px" />
<LoadingBubble height="16px" width="100px" margin="4px 0 20px 0" />
</Column>
)}
{!shouldDisableNFTRoutes && (
<HeaderButton
data-testid="nft-view-self-nfts"
onClick={navigateToProfile}
size={ButtonSize.medium}
emphasis={ButtonEmphasis.medium}
>
<Trans>View and sell NFTs</Trans>
</HeaderButton>
)}
<HeaderButton
size={ButtonSize.medium}
emphasis={ButtonEmphasis.medium}
onClick={handleBuyCryptoClick}
disabled={disableBuyCryptoButton}
data-testid="wallet-buy-crypto"
>
{error ? (
<ThemedText.BodyPrimary>{error}</ThemedText.BodyPrimary>
@@ -308,7 +317,7 @@ const AuthenticatedHeader = () => {
<Trans>Buy crypto</Trans>
</>
)}
</BuyCryptoButton>
</HeaderButton>
{Boolean(!fiatOnrampAvailable && fiatOnrampAvailabilityChecked) && (
<FiatOnrampNotAvailableText marginTop="8px">
<Trans>Not available in your region</Trans>
@@ -327,6 +336,7 @@ const AuthenticatedHeader = () => {
</Tooltip>
</FiatOnrampNotAvailableText>
)}
<MiniPortfolio account={account} />
{isUnclaimed && (
<UNIButton onClick={openClaimModal} size={ButtonSize.medium} emphasis={ButtonEmphasis.medium}>
<Trans>Claim</Trans> {unclaimedAmount?.toFixed(0, { groupSeparator: ',' } ?? '-')} <Trans>reward</Trans>
@@ -337,9 +347,7 @@ const AuthenticatedHeader = () => {
<Trans>Claim Uniswap NFT Airdrop</Trans>
</UNIButton>
)}
</Column>
</>
</PortfolioDrawerContainer>
</AuthenticatedHeaderWrapper>
)
}
export default AuthenticatedHeader

View File

@@ -0,0 +1,41 @@
import { useWeb3React } from '@web3-react/core'
import Column from 'components/Column'
import WalletModal from 'components/WalletModal'
import { useCallback, useState } from 'react'
import styled from 'styled-components/macro'
import AuthenticatedHeader from './AuthenticatedHeader'
import SettingsMenu from './SettingsMenu'
const DefaultMenuWrap = styled(Column)`
width: 100%;
height: 100%;
`
enum MenuState {
DEFAULT,
SETTINGS,
}
function DefaultMenu() {
const { account } = useWeb3React()
const isAuthenticated = !!account
const [menu, setMenu] = useState<MenuState>(MenuState.DEFAULT)
const openSettings = useCallback(() => setMenu(MenuState.SETTINGS), [])
const closeSettings = useCallback(() => setMenu(MenuState.DEFAULT), [])
return (
<DefaultMenuWrap>
{menu === MenuState.DEFAULT &&
(isAuthenticated ? (
<AuthenticatedHeader account={account} openSettings={openSettings} />
) : (
<WalletModal openSettings={openSettings} />
))}
{menu === MenuState.SETTINGS && <SettingsMenu onClose={closeSettings} />}
</DefaultMenuWrap>
)
}
export default DefaultMenu

View File

@@ -0,0 +1,73 @@
import { sendAnalyticsEvent } from '@uniswap/analytics'
import { InterfaceElementName, InterfaceEventName, SharedEventName } from '@uniswap/analytics-events'
import { PropsWithChildren, useCallback } from 'react'
import styled from 'styled-components/macro'
import { ClickableStyle } from 'theme'
import { isIOS } from 'utils/userAgent'
const StyledButton = styled.button<{ padded?: boolean; branded?: boolean }>`
${ClickableStyle}
width: 100%;
display: flex;
justify-content: center;
flex-direction: row;
gap: 6px;
padding: 8px 24px;
border: none;
white-space: nowrap;
background: ${({ theme, branded }) => (branded ? theme.promotionalGradient : theme.backgroundInteractive)};
border-radius: 12px;
font-weight: 600;
font-size: 14px;
line-height: 16px;
color: ${({ theme, branded }) => (branded ? theme.accentTextLightPrimary : theme.textPrimary)};
`
function BaseButton({ onClick, branded, children }: PropsWithChildren<{ onClick?: () => void; branded?: boolean }>) {
return (
<StyledButton branded={branded} onClick={onClick}>
{children}
</StyledButton>
)
}
const APP_STORE_LINK = 'https://apps.apple.com/us/app/uniswap-wallet/id6443944476'
const MICROSITE_LINK = 'https://wallet.uniswap.org/'
const openAppStore = () => {
window.open(APP_STORE_LINK, /* target = */ 'uniswap_wallet_appstore')
}
export const openWalletMicrosite = () => {
sendAnalyticsEvent(InterfaceEventName.UNISWAP_WALLET_MICROSITE_OPENED)
window.open(MICROSITE_LINK, /* target = */ 'uniswap_wallet_microsite')
}
export function openDownloadApp(element: InterfaceElementName) {
sendAnalyticsEvent(SharedEventName.ELEMENT_CLICKED, { element })
if (isIOS) openAppStore()
else openWalletMicrosite()
}
// Launches App Store if on an iOS device, else navigates to Uniswap Wallet microsite
export function DownloadButton({
onClick,
text = 'Download',
element,
}: {
onClick?: () => void
text?: string
element: InterfaceElementName
}) {
const onButtonClick = useCallback(() => {
// handles any actions required by the parent, i.e. cancelling wallet connection attempt or dismissing an ad
onClick?.()
openDownloadApp(element)
}, [element, onClick])
return (
<BaseButton branded onClick={onButtonClick}>
{text}
</BaseButton>
)
}

View File

@@ -0,0 +1,28 @@
import { Trans } from '@lingui/macro'
import Tooltip from 'components/Tooltip'
import useCopyClipboard from 'hooks/useCopyClipboard'
import styled from 'styled-components/macro'
import { ThemedText } from 'theme'
const Container = styled.div`
width: 100%;
cursor: pointer;
`
export function GitVersionRow() {
const [isCopied, staticCopy] = useCopyClipboard()
return process.env.REACT_APP_GIT_COMMIT_HASH ? (
<Container
onClick={() => {
staticCopy(process.env.REACT_APP_GIT_COMMIT_HASH as string)
}}
>
<Tooltip text="Copied" show={isCopied}>
<ThemedText.BodySmall color="textTertiary">
<Trans>Version: </Trans>
{' ' + process.env.REACT_APP_GIT_COMMIT_HASH.substring(0, 6)}
</ThemedText.BodySmall>
</Tooltip>
</Container>
) : null
}

View File

@@ -73,11 +73,10 @@ const IconBlock = (props: React.ComponentPropsWithoutRef<'a' | 'button'>) => {
return <IconBlockButton {...props} />
}
const IconButton = ({ Icon, children, ...rest }: IconButtonProps | IconLinkProps) => (
const IconButton = ({ Icon, ...rest }: IconButtonProps | IconLinkProps) => (
<IconBlock {...rest}>
<IconWrapper>
<Icon strokeWidth={1.5} size={16} />
<IconHoverText>{children}</IconHoverText>
</IconWrapper>
</IconBlock>
)

View File

@@ -0,0 +1,71 @@
import { TraceEvent } from '@uniswap/analytics'
import { BrowserEvent, InterfaceElementName, SharedEventName } from '@uniswap/analytics-events'
import Column from 'components/Column'
import AlertTriangleFilled from 'components/Icons/AlertTriangleFilled'
import { LoaderV2 } from 'components/Icons/LoadingSpinner'
import { TransactionStatus } from 'graphql/data/__generated__/types-and-hooks'
import useENSName from 'hooks/useENSName'
import styled from 'styled-components/macro'
import { EllipsisStyle, ThemedText } from 'theme'
import { ExplorerDataType, getExplorerLink } from 'utils/getExplorerLink'
import { PortfolioLogo } from '../PortfolioLogo'
import PortfolioRow from '../PortfolioRow'
import { useTimeSince } from './parseRemote'
import { Activity } from './types'
const ActivityRowDescriptor = styled(ThemedText.BodySmall)`
color: ${({ theme }) => theme.textSecondary};
${EllipsisStyle}
`
const StyledTimestamp = styled(ThemedText.Caption)`
color: ${({ theme }) => theme.textSecondary};
font-variant: small;
font-feature-settings: 'tnum' on, 'lnum' on, 'ss02' on;
`
export function ActivityRow({
activity: { chainId, status, title, descriptor, logos, otherAccount, currencies, timestamp, hash },
}: {
activity: Activity
}) {
const { ENSName } = useENSName(otherAccount)
const timeSince = useTimeSince(timestamp)
const explorerUrl = getExplorerLink(chainId, hash, ExplorerDataType.TRANSACTION)
return (
<TraceEvent
events={[BrowserEvent.onClick]}
name={SharedEventName.ELEMENT_CLICKED}
element={InterfaceElementName.MINI_PORTFOLIO_ACTIVITY_ROW}
properties={{ hash, chain_id: chainId, explorer_url: explorerUrl }}
>
<PortfolioRow
left={
<Column>
<PortfolioLogo chainId={chainId} currencies={currencies} images={logos} accountAddress={otherAccount} />
</Column>
}
title={<ThemedText.SubHeader fontWeight={500}>{title}</ThemedText.SubHeader>}
descriptor={
<ActivityRowDescriptor color="textSecondary">
{descriptor}
{ENSName ?? otherAccount}
</ActivityRowDescriptor>
}
right={
status === TransactionStatus.Pending ? (
<LoaderV2 />
) : status === TransactionStatus.Confirmed ? (
<StyledTimestamp>{timeSince}</StyledTimestamp>
) : (
<AlertTriangleFilled />
)
}
onClick={() => window.open(explorerUrl, '_blank')}
/>
</TraceEvent>
)
}

View File

@@ -0,0 +1,156 @@
import { t } from '@lingui/macro'
import { useAccountDrawer } from 'components/AccountDrawer'
import Column from 'components/Column'
import { LoadingBubble } from 'components/Tokens/loading'
import { getYear, isSameDay, isSameMonth, isSameWeek, isSameYear } from 'date-fns'
import { TransactionStatus, useTransactionListQuery } from 'graphql/data/__generated__/types-and-hooks'
import { PollingInterval } from 'graphql/data/util'
import { atom, useAtom } from 'jotai'
import { EmptyWalletModule } from 'nft/components/profile/view/EmptyWalletContent'
import { useEffect, useMemo } from 'react'
import styled from 'styled-components/macro'
import { ThemedText } from 'theme'
import { PortfolioSkeleton, PortfolioTabWrapper } from '../PortfolioRow'
import { ActivityRow } from './ActivityRow'
import { useLocalActivities } from './parseLocal'
import { parseRemoteActivities } from './parseRemote'
import { Activity, ActivityMap } from './types'
interface ActivityGroup {
title: string
transactions: Array<Activity>
}
const sortActivities = (a: Activity, b: Activity) => b.timestamp - a.timestamp
const createGroups = (activities?: Array<Activity>) => {
if (!activities || !activities.length) return []
const now = Date.now()
const pending: Array<Activity> = []
const today: Array<Activity> = []
const currentWeek: Array<Activity> = []
const last30Days: Array<Activity> = []
const currentYear: Array<Activity> = []
const yearMap: { [key: string]: Array<Activity> } = {}
// TODO(cartcrom): create different time bucket system for activities to fall in based on design wants
activities.forEach((activity) => {
if (activity.status === TransactionStatus.Pending) {
pending.push(activity)
return
}
const addedTime = activity.timestamp * 1000
if (isSameDay(now, addedTime)) {
today.push(activity)
} else if (isSameWeek(addedTime, now)) {
currentWeek.push(activity)
} else if (isSameMonth(addedTime, now)) {
last30Days.push(activity)
} else if (isSameYear(addedTime, now)) {
currentYear.push(activity)
} else {
const year = getYear(addedTime)
if (!yearMap[year]) {
yearMap[year] = [activity]
} else {
yearMap[year].push(activity)
}
}
})
const sortedYears = Object.keys(yearMap)
.sort((a, b) => parseInt(b) - parseInt(a))
.map((year) => ({ title: year, transactions: yearMap[year] }))
const transactionGroups: Array<ActivityGroup> = [
{ title: t`Pending`, transactions: pending.sort(sortActivities) },
{ title: t`Today`, transactions: today.sort(sortActivities) },
{ title: t`This week`, transactions: currentWeek.sort(sortActivities) },
{ title: t`This month`, transactions: last30Days.sort(sortActivities) },
{ title: t`This year`, transactions: currentYear.sort(sortActivities) },
...sortedYears,
]
return transactionGroups.filter((transactionInformation) => transactionInformation.transactions.length > 0)
}
const ActivityGroupWrapper = styled(Column)`
margin-top: 16px;
gap: 8px;
`
function combineActivities(localMap: ActivityMap = {}, remoteMap: ActivityMap = {}): 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)
return acc
}, [])
}
const lastFetchedAtom = atom<number | undefined>(0)
export function ActivityTab({ account }: { account: string }) {
const [drawerOpen, toggleWalletDrawer] = useAccountDrawer()
const [lastFetched, setLastFetched] = useAtom(lastFetchedAtom)
const localMap = useLocalActivities(account)
const { data, loading, refetch } = useTransactionListQuery({
variables: { account },
errorPolicy: 'all',
fetchPolicy: 'cache-first',
})
// We only refetch remote activity if the user renavigates to the activity tab by changing tabs or opening the drawer
useEffect(() => {
const currentTime = Date.now()
if (!lastFetched) {
setLastFetched(currentTime)
} else if (drawerOpen && lastFetched && currentTime - lastFetched > PollingInterval.Slow) {
refetch()
setLastFetched(currentTime)
}
}, [drawerOpen, lastFetched, refetch, setLastFetched])
const activityGroups = useMemo(() => {
const remoteMap = parseRemoteActivities(data?.portfolios?.[0].assetActivities)
const allActivities = combineActivities(localMap, remoteMap)
return createGroups(allActivities)
}, [data?.portfolios, localMap])
if (!data && loading)
return (
<>
<LoadingBubble height="16px" width="80px" margin="16px 16px 8px" />
<PortfolioSkeleton shrinkRight />
</>
)
else if (activityGroups.length === 0) {
return <EmptyWalletModule type="activity" onNavigateClick={toggleWalletDrawer} />
} else {
return (
<PortfolioTabWrapper>
{activityGroups.map((activityGroup) => (
<ActivityGroupWrapper key={activityGroup.title}>
<ThemedText.SubHeader color="textSecondary" fontWeight={500} marginLeft="16px">
{activityGroup.title}
</ThemedText.SubHeader>
<Column>
{activityGroup.transactions.map((activity) => (
<ActivityRow key={activity.hash} activity={activity} />
))}
</Column>
</ActivityGroupWrapper>
))}
</PortfolioTabWrapper>
)
}
}

View File

@@ -0,0 +1,512 @@
import { SupportedChainId, Token, TradeType as MockTradeType } from '@uniswap/sdk-core'
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 {
ExactInputSwapTransactionInfo,
ExactOutputSwapTransactionInfo,
TransactionDetails,
TransactionInfo,
TransactionType as MockTxType,
} from 'state/transactions/types'
import { renderHook } from 'test-utils/render'
import { parseLocalActivity, useLocalActivities } from './parseLocal'
function mockSwapInfo(
type: MockTradeType,
inputCurrency: Token,
inputCurrencyAmountRaw: string,
outputCurrency: Token,
outputCurrencyAmountRaw: string
): ExactInputSwapTransactionInfo | ExactOutputSwapTransactionInfo {
if (type === MockTradeType.EXACT_INPUT) {
return {
type: MockTxType.SWAP,
tradeType: MockTradeType.EXACT_INPUT,
inputCurrencyId: inputCurrency.address,
inputCurrencyAmountRaw,
outputCurrencyId: outputCurrency.address,
expectedOutputCurrencyAmountRaw: outputCurrencyAmountRaw,
minimumOutputCurrencyAmountRaw: outputCurrencyAmountRaw,
}
} else {
return {
type: MockTxType.SWAP,
tradeType: MockTradeType.EXACT_OUTPUT,
inputCurrencyId: inputCurrency.address,
expectedInputCurrencyAmountRaw: inputCurrencyAmountRaw,
maximumInputCurrencyAmountRaw: inputCurrencyAmountRaw,
outputCurrencyId: outputCurrency.address,
outputCurrencyAmountRaw,
}
}
}
const mockAccount1 = '0x000000000000000000000000000000000000000001'
const mockAccount2 = '0x000000000000000000000000000000000000000002'
const mockChainId = SupportedChainId.MAINNET
const mockSpenderAddress = PERMIT2_ADDRESS[mockChainId]
const mockCurrencyAmountRaw = '1000000000000000000'
const mockCurrencyAmountRawUSDC = '1000000'
function mockHash(id: string, status: MockTxStatus = MockTxStatus.Confirmed) {
return id + status
}
function mockCommonFields(id: string, account = mockAccount2, status: MockTxStatus) {
const hash = mockHash(id, status)
return {
hash,
from: account,
receipt:
status === MockTxStatus.Pending
? undefined
: {
transactionHash: hash,
status: status === MockTxStatus.Confirmed ? 1 : 0,
},
addedTime: 0,
}
}
function mockMultiStatus(info: TransactionInfo, id: string): [TransactionDetails, number][] {
// Mocks a transaction with multiple statuses
return [
[
{ info, ...mockCommonFields(id, mockAccount2, MockTxStatus.Pending) } as unknown as TransactionDetails,
mockChainId,
],
[
{ info, ...mockCommonFields(id, mockAccount2, MockTxStatus.Confirmed) } as unknown as TransactionDetails,
mockChainId,
],
[
{ info, ...mockCommonFields(id, mockAccount2, MockTxStatus.Failed) } as unknown as TransactionDetails,
mockChainId,
],
]
}
const mockTokenAddressMap: TokenAddressMap = {
[mockChainId]: {
[MockDAI.address]: { token: MockDAI },
[MockUSDC_MAINNET.address]: { token: MockUSDC_MAINNET },
} as TokenAddressMap[number],
}
jest.mock('../../../../state/lists/hooks', () => ({
useCombinedActiveList: () => mockTokenAddressMap,
}))
jest.mock('../../../../state/transactions/hooks', () => {
return {
useMultichainTransactions: (): [TransactionDetails, number][] => {
return [
[
{
info: mockSwapInfo(
MockTradeType.EXACT_INPUT,
MockUSDC_MAINNET,
mockCurrencyAmountRawUSDC,
MockDAI,
mockCurrencyAmountRaw
),
...mockCommonFields('0x123', mockAccount1, MockTxStatus.Confirmed),
} as TransactionDetails,
mockChainId,
],
...mockMultiStatus(
mockSwapInfo(
MockTradeType.EXACT_OUTPUT,
MockUSDC_MAINNET,
mockCurrencyAmountRawUSDC,
MockDAI,
mockCurrencyAmountRaw
),
'0xswap_exact_input'
),
...mockMultiStatus(
mockSwapInfo(
MockTradeType.EXACT_INPUT,
MockUSDC_MAINNET,
mockCurrencyAmountRawUSDC,
MockDAI,
mockCurrencyAmountRaw
),
'0xswap_exact_output'
),
...mockMultiStatus(
{
type: MockTxType.APPROVAL,
tokenAddress: MockDAI.address,
spender: mockSpenderAddress,
},
'0xapproval'
),
...mockMultiStatus(
{
type: MockTxType.WRAP,
unwrapped: false,
currencyAmountRaw: mockCurrencyAmountRaw,
chainId: mockChainId,
},
'0xwrap'
),
...mockMultiStatus(
{
type: MockTxType.WRAP,
unwrapped: true,
currencyAmountRaw: mockCurrencyAmountRaw,
chainId: mockChainId,
},
'0xunwrap'
),
...mockMultiStatus(
{
type: MockTxType.ADD_LIQUIDITY_V3_POOL,
createPool: false,
baseCurrencyId: MockUSDC_MAINNET.address,
quoteCurrencyId: MockDAI.address,
feeAmount: 500,
expectedAmountBaseRaw: mockCurrencyAmountRawUSDC,
expectedAmountQuoteRaw: mockCurrencyAmountRaw,
},
'0xadd_liquidity_v3'
),
...mockMultiStatus(
{
type: MockTxType.REMOVE_LIQUIDITY_V3,
baseCurrencyId: MockUSDC_MAINNET.address,
quoteCurrencyId: MockDAI.address,
expectedAmountBaseRaw: mockCurrencyAmountRawUSDC,
expectedAmountQuoteRaw: mockCurrencyAmountRaw,
},
'0xremove_liquidity_v3'
),
...mockMultiStatus(
{
type: MockTxType.ADD_LIQUIDITY_V2_POOL,
baseCurrencyId: MockUSDC_MAINNET.address,
quoteCurrencyId: MockDAI.address,
expectedAmountBaseRaw: mockCurrencyAmountRawUSDC,
expectedAmountQuoteRaw: mockCurrencyAmountRaw,
},
'0xadd_liquidity_v2'
),
...mockMultiStatus(
{
type: MockTxType.COLLECT_FEES,
currencyId0: MockUSDC_MAINNET.address,
currencyId1: MockDAI.address,
expectedCurrencyOwed0: mockCurrencyAmountRawUSDC,
expectedCurrencyOwed1: mockCurrencyAmountRaw,
},
'0xcollect_fees'
),
...mockMultiStatus(
{
type: MockTxType.MIGRATE_LIQUIDITY_V3,
baseCurrencyId: MockUSDC_MAINNET.address,
quoteCurrencyId: MockDAI.address,
isFork: false,
},
'0xmigrate_v3_liquidity'
),
]
},
}
})
describe('parseLocalActivity', () => {
it('returns swap activity fields with known tokens, exact input', () => {
const details = {
info: mockSwapInfo(
MockTradeType.EXACT_INPUT,
MockUSDC_MAINNET,
mockCurrencyAmountRawUSDC,
MockDAI,
mockCurrencyAmountRaw
),
receipt: {
transactionHash: '0x123',
status: 1,
},
} as TransactionDetails
const chainId = SupportedChainId.MAINNET
expect(parseLocalActivity(details, chainId, mockTokenAddressMap)).toEqual({
chainId: 1,
currencies: [MockUSDC_MAINNET, MockDAI],
descriptor: '1.00 USDC for 1.00 DAI',
hash: undefined,
receipt: {
id: '0x123',
info: {
type: 1,
tradeType: MockTradeType.EXACT_INPUT,
inputCurrencyId: MockUSDC_MAINNET.address,
inputCurrencyAmountRaw: mockCurrencyAmountRawUSDC,
outputCurrencyId: MockDAI.address,
expectedOutputCurrencyAmountRaw: mockCurrencyAmountRaw,
minimumOutputCurrencyAmountRaw: mockCurrencyAmountRaw,
},
receipt: { status: 1, transactionHash: '0x123' },
status: 'CONFIRMED',
transactionHash: '0x123',
},
status: 'CONFIRMED',
timestamp: NaN,
title: 'Swapped',
})
})
it('returns swap activity fields with known tokens, exact output', () => {
const details = {
info: mockSwapInfo(
MockTradeType.EXACT_OUTPUT,
MockUSDC_MAINNET,
mockCurrencyAmountRawUSDC,
MockDAI,
mockCurrencyAmountRaw
),
receipt: {
transactionHash: '0x123',
status: 1,
},
} as TransactionDetails
const chainId = SupportedChainId.MAINNET
expect(parseLocalActivity(details, chainId, mockTokenAddressMap)).toMatchObject({
chainId: 1,
currencies: [MockUSDC_MAINNET, MockDAI],
descriptor: '1.00 USDC for 1.00 DAI',
status: 'CONFIRMED',
title: 'Swapped',
})
})
it('returns swap activity fields with unknown tokens', () => {
const details = {
info: mockSwapInfo(
MockTradeType.EXACT_INPUT,
MockUSDC_MAINNET,
mockCurrencyAmountRawUSDC,
MockDAI,
mockCurrencyAmountRaw
),
receipt: {
transactionHash: '0x123',
status: 1,
},
} as TransactionDetails
const chainId = SupportedChainId.MAINNET
const tokens = {} as TokenAddressMap
expect(parseLocalActivity(details, chainId, tokens)).toMatchObject({
chainId: 1,
currencies: [undefined, undefined],
descriptor: 'Unknown for Unknown',
status: 'CONFIRMED',
title: 'Swapped',
})
})
it('only returns activity for the current account', () => {
const account1Activites = renderHook(() => useLocalActivities(mockAccount1)).result.current
const account2Activites = renderHook(() => useLocalActivities(mockAccount2)).result.current
expect(Object.values(account1Activites)).toHaveLength(1)
expect(Object.values(account2Activites)).toHaveLength(30)
})
it('Properly uses correct tense of activity title based on tx status', () => {
const activities = renderHook(() => useLocalActivities(mockAccount2)).result.current
expect(activities[mockHash('0xswap_exact_input', MockTxStatus.Pending)]?.title).toEqual('Swapping')
expect(activities[mockHash('0xswap_exact_input', MockTxStatus.Confirmed)]?.title).toEqual('Swapped')
expect(activities[mockHash('0xswap_exact_input', MockTxStatus.Failed)]?.title).toEqual('Swap failed')
})
it('Adapts Swap exact input to Activity type', () => {
const hash = mockHash('0xswap_exact_input')
const activity = renderHook(() => useLocalActivities(mockAccount2)).result.current[hash]
expect(activity).toMatchObject({
chainId: mockChainId,
currencies: [MockUSDC_MAINNET, MockDAI],
title: 'Swapped',
descriptor: `1.00 ${MockUSDC_MAINNET.symbol} for 1.00 ${MockDAI.symbol}`,
hash,
status: MockTxStatus.Confirmed,
receipt: {
id: hash,
status: MockTxStatus.Confirmed,
},
})
})
it('Adapts Swap exact output to Activity type', () => {
const hash = mockHash('0xswap_exact_output')
const activity = renderHook(() => useLocalActivities(mockAccount2)).result.current[hash]
expect(activity).toMatchObject({
chainId: mockChainId,
currencies: [MockUSDC_MAINNET, MockDAI],
title: 'Swapped',
descriptor: `1.00 ${MockUSDC_MAINNET.symbol} for 1.00 ${MockDAI.symbol}`,
hash,
status: MockTxStatus.Confirmed,
receipt: {
id: hash,
status: MockTxStatus.Confirmed,
},
})
})
it('Adapts Approval to Activity type', () => {
const hash = mockHash('0xapproval')
const activity = renderHook(() => useLocalActivities(mockAccount2)).result.current[hash]
expect(activity).toMatchObject({
chainId: mockChainId,
currencies: [MockDAI],
title: 'Approved',
descriptor: MockDAI.symbol,
hash,
status: MockTxStatus.Confirmed,
receipt: {
id: hash,
status: MockTxStatus.Confirmed,
},
})
})
it('Adapts Wrap to Activity type', () => {
const hash = mockHash('0xwrap')
const activity = renderHook(() => useLocalActivities(mockAccount2)).result.current[hash]
const native = nativeOnChain(mockChainId)
expect(activity).toMatchObject({
chainId: mockChainId,
currencies: [native, native.wrapped],
title: 'Wrapped',
descriptor: `1.00 ${native.symbol} for 1.00 ${native.wrapped.symbol}`,
hash,
status: MockTxStatus.Confirmed,
receipt: {
id: hash,
status: MockTxStatus.Confirmed,
},
})
})
it('Adapts Unwrap to Activity type', () => {
const hash = mockHash('0xunwrap')
const activity = renderHook(() => useLocalActivities(mockAccount2)).result.current[hash]
const native = nativeOnChain(mockChainId)
expect(activity).toMatchObject({
chainId: mockChainId,
currencies: [native.wrapped, native],
title: 'Unwrapped',
descriptor: `1.00 ${native.wrapped.symbol} for 1.00 ${native.symbol}`,
hash,
status: MockTxStatus.Confirmed,
receipt: {
id: hash,
status: MockTxStatus.Confirmed,
},
})
})
it('Adapts AddLiquidityV3 to Activity type', () => {
const hash = mockHash('0xadd_liquidity_v3')
const activity = renderHook(() => useLocalActivities(mockAccount2)).result.current[hash]
expect(activity).toMatchObject({
chainId: mockChainId,
currencies: [MockUSDC_MAINNET, MockDAI],
title: 'Added liquidity',
descriptor: `1.00 ${MockUSDC_MAINNET.symbol} and 1.00 ${MockDAI.symbol}`,
hash,
status: MockTxStatus.Confirmed,
receipt: {
id: hash,
status: MockTxStatus.Confirmed,
},
})
})
it('Adapts RemoveLiquidityV3 to Activity type', () => {
const hash = mockHash('0xremove_liquidity_v3')
const activity = renderHook(() => useLocalActivities(mockAccount2)).result.current[hash]
expect(activity).toMatchObject({
chainId: mockChainId,
currencies: [MockUSDC_MAINNET, MockDAI],
title: 'Removed liquidity',
descriptor: `1.00 ${MockUSDC_MAINNET.symbol} and 1.00 ${MockDAI.symbol}`,
hash,
status: MockTxStatus.Confirmed,
receipt: {
id: hash,
status: MockTxStatus.Confirmed,
},
})
})
it('Adapts RemoveLiquidityV2 to Activity type', () => {
const hash = mockHash('0xadd_liquidity_v2')
const activity = renderHook(() => useLocalActivities(mockAccount2)).result.current[hash]
expect(activity).toMatchObject({
chainId: mockChainId,
currencies: [MockUSDC_MAINNET, MockDAI],
title: 'Added V2 liquidity',
descriptor: `1.00 ${MockUSDC_MAINNET.symbol} and 1.00 ${MockDAI.symbol}`,
hash,
status: MockTxStatus.Confirmed,
receipt: {
id: hash,
status: MockTxStatus.Confirmed,
},
})
})
it('Adapts CollectFees to Activity type', () => {
const hash = mockHash('0xcollect_fees')
const activity = renderHook(() => useLocalActivities(mockAccount2)).result.current[hash]
expect(activity).toMatchObject({
chainId: mockChainId,
currencies: [MockUSDC_MAINNET, MockDAI],
title: 'Collected fees',
descriptor: `1.00 ${MockUSDC_MAINNET.symbol} and 1.00 ${MockDAI.symbol}`,
hash,
status: MockTxStatus.Confirmed,
receipt: {
id: hash,
status: MockTxStatus.Confirmed,
},
})
})
it('Adapts MigrateLiquidityV3 to Activity type', () => {
const hash = mockHash('0xmigrate_v3_liquidity')
const activity = renderHook(() => useLocalActivities(mockAccount2)).result.current[hash]
expect(activity).toMatchObject({
chainId: mockChainId,
currencies: [MockUSDC_MAINNET, MockDAI],
title: 'Migrated liquidity',
descriptor: `${MockUSDC_MAINNET.symbol} and ${MockDAI.symbol}`,
hash,
status: MockTxStatus.Confirmed,
receipt: {
id: hash,
status: MockTxStatus.Confirmed,
},
})
})
})

View File

@@ -0,0 +1,202 @@
import { t } from '@lingui/macro'
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 { useMemo } from 'react'
import { TokenAddressMap, useCombinedActiveList } from 'state/lists/hooks'
import { useMultichainTransactions } from 'state/transactions/hooks'
import {
AddLiquidityV2PoolTransactionInfo,
AddLiquidityV3PoolTransactionInfo,
ApproveTransactionInfo,
CollectFeesTransactionInfo,
CreateV3PoolTransactionInfo,
ExactInputSwapTransactionInfo,
ExactOutputSwapTransactionInfo,
MigrateV2LiquidityToV3TransactionInfo,
RemoveLiquidityV3TransactionInfo,
TransactionDetails,
TransactionType,
WrapTransactionInfo,
} from 'state/transactions/types'
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 buildCurrencyDescriptor(
currencyA: Currency | undefined,
amtA: string,
currencyB: Currency | undefined,
amtB: string,
delimiter = t`for`
) {
const formattedA = currencyA ? formatCurrencyAmount(CurrencyAmount.fromRawAmount(currencyA, amtA)) : t`Unknown`
const symbolA = currencyA?.symbol ?? ''
const formattedB = currencyB ? formatCurrencyAmount(CurrencyAmount.fromRawAmount(currencyB, amtB)) : t`Unknown`
const symbolB = currencyB?.symbol ?? ''
return [formattedA, symbolA, delimiter, formattedB, symbolB].filter(Boolean).join(' ')
}
function parseSwap(
swap: ExactInputSwapTransactionInfo | ExactOutputSwapTransactionInfo,
chainId: SupportedChainId,
tokens: TokenAddressMap
): Partial<Activity> {
const tokenIn = getCurrency(swap.inputCurrencyId, chainId, tokens)
const tokenOut = getCurrency(swap.outputCurrencyId, chainId, tokens)
const [inputRaw, outputRaw] =
swap.tradeType === TradeType.EXACT_INPUT
? [swap.inputCurrencyAmountRaw, swap.expectedOutputCurrencyAmountRaw]
: [swap.expectedInputCurrencyAmountRaw, swap.outputCurrencyAmountRaw]
return {
descriptor: buildCurrencyDescriptor(tokenIn, inputRaw, tokenOut, outputRaw),
currencies: [tokenIn, tokenOut],
}
}
function parseWrap(wrap: WrapTransactionInfo, chainId: SupportedChainId, status: TransactionStatus): Partial<Activity> {
const native = nativeOnChain(chainId)
const wrapped = native.wrapped
const [input, output] = wrap.unwrapped ? [wrapped, native] : [native, wrapped]
const descriptor = buildCurrencyDescriptor(input, wrap.currencyAmountRaw, output, wrap.currencyAmountRaw)
const title = getActivityTitle(TransactionType.WRAP, status, wrap.unwrapped)
const currencies = wrap.unwrapped ? [wrapped, native] : [native, wrapped]
return { title, descriptor, currencies }
}
function parseApproval(
approval: ApproveTransactionInfo,
chainId: SupportedChainId,
tokens: TokenAddressMap
): Partial<Activity> {
// TODO: Add 'amount' approved to ApproveTransactionInfo so we can distinguish between revoke and approve
const currency = getCurrency(approval.tokenAddress, chainId, tokens)
const descriptor = currency?.symbol ?? currency?.name ?? t`Unknown`
return {
descriptor,
currencies: [currency],
}
}
type GenericLPInfo = Omit<
AddLiquidityV3PoolTransactionInfo | RemoveLiquidityV3TransactionInfo | AddLiquidityV2PoolTransactionInfo,
'type'
>
function parseLP(lp: GenericLPInfo, chainId: SupportedChainId, tokens: TokenAddressMap): Partial<Activity> {
const baseCurrency = getCurrency(lp.baseCurrencyId, chainId, tokens)
const quoteCurrency = getCurrency(lp.quoteCurrencyId, chainId, tokens)
const [baseRaw, quoteRaw] = [lp.expectedAmountBaseRaw, lp.expectedAmountQuoteRaw]
const descriptor = buildCurrencyDescriptor(baseCurrency, baseRaw, quoteCurrency, quoteRaw, t`and`)
return { descriptor, currencies: [baseCurrency, quoteCurrency] }
}
function parseCollectFees(
collect: CollectFeesTransactionInfo,
chainId: SupportedChainId,
tokens: TokenAddressMap
): Partial<Activity> {
// Adapts CollectFeesTransactionInfo to generic LP type
const {
currencyId0: baseCurrencyId,
currencyId1: quoteCurrencyId,
expectedCurrencyOwed0: expectedAmountBaseRaw,
expectedCurrencyOwed1: expectedAmountQuoteRaw,
} = collect
return parseLP({ baseCurrencyId, quoteCurrencyId, expectedAmountBaseRaw, expectedAmountQuoteRaw }, chainId, tokens)
}
function parseMigrateCreateV3(
lp: MigrateV2LiquidityToV3TransactionInfo | CreateV3PoolTransactionInfo,
chainId: SupportedChainId,
tokens: TokenAddressMap
): Partial<Activity> {
const baseCurrency = getCurrency(lp.baseCurrencyId, chainId, tokens)
const baseSymbol = baseCurrency?.symbol ?? t`Unknown`
const quoteCurrency = getCurrency(lp.quoteCurrencyId, chainId, tokens)
const quoteSymbol = quoteCurrency?.symbol ?? t`Unknown`
const descriptor = t`${baseSymbol} and ${quoteSymbol}`
return { descriptor, currencies: [baseCurrency, quoteCurrency] }
}
export function parseLocalActivity(
details: TransactionDetails,
chainId: SupportedChainId,
tokens: TokenAddressMap
): Activity | undefined {
try {
const status = !details.receipt
? TransactionStatus.Pending
: details.receipt.status === 1 || details.receipt?.status === undefined
? TransactionStatus.Confirmed
: TransactionStatus.Failed
const receipt: TransactionPartsFragment | undefined = details.receipt
? {
id: details.receipt.transactionHash,
...details.receipt,
...details,
status,
}
: undefined
const defaultFields = {
hash: details.hash,
chainId,
title: getActivityTitle(details.info.type, status),
status,
timestamp: (details.confirmedTime ?? details.addedTime) / 1000,
receipt,
}
let additionalFields: Partial<Activity> = {}
const info = details.info
if (info.type === TransactionType.SWAP) {
additionalFields = parseSwap(info, chainId, tokens)
} else if (info.type === TransactionType.APPROVAL) {
additionalFields = parseApproval(info, chainId, tokens)
} else if (info.type === TransactionType.WRAP) {
additionalFields = parseWrap(info, chainId, status)
} else if (
info.type === TransactionType.ADD_LIQUIDITY_V3_POOL ||
info.type === TransactionType.REMOVE_LIQUIDITY_V3 ||
info.type === TransactionType.ADD_LIQUIDITY_V2_POOL
) {
additionalFields = parseLP(info, chainId, tokens)
} else if (info.type === TransactionType.COLLECT_FEES) {
additionalFields = parseCollectFees(info, chainId, tokens)
} else if (info.type === TransactionType.MIGRATE_LIQUIDITY_V3 || info.type === TransactionType.CREATE_V3_POOL) {
additionalFields = parseMigrateCreateV3(info, chainId, tokens)
}
return { ...defaultFields, ...additionalFields }
} catch (error) {
console.debug(`Failed to parse transaction ${details.hash}`, error)
return undefined
}
}
export function useLocalActivities(account: string): ActivityMap {
const allTransactions = useMultichainTransactions()
const tokens = useCombinedActiveList()
return useMemo(() => {
const activityByHash: ActivityMap = {}
for (const [transaction, chainId] of allTransactions) {
if (transaction.from !== account) continue
activityByHash[transaction.hash] = parseLocalActivity(transaction, chainId, tokens)
}
return activityByHash
}, [account, allTransactions, tokens])
}

View File

@@ -0,0 +1,309 @@
import { t } from '@lingui/macro'
import { formatNumberOrString, NumberType } from '@uniswap/conedison/format'
import { SupportedChainId } from '@uniswap/sdk-core'
import { NONFUNGIBLE_POSITION_MANAGER_ADDRESSES, UNI_ADDRESS } from 'constants/addresses'
import { nativeOnChain } from 'constants/tokens'
import {
ActivityType,
AssetActivityPartsFragment,
NftApprovalPartsFragment,
NftApproveForAllPartsFragment,
NftTransferPartsFragment,
TokenApprovalPartsFragment,
TokenTransferPartsFragment,
} from 'graphql/data/__generated__/types-and-hooks'
import { fromGraphQLChain } from 'graphql/data/util'
import ms from 'ms.macro'
import { useEffect, useState } from 'react'
import { isAddress } from 'utils'
import { Activity } from './types'
type TransactionChanges = {
NftTransfer: NftTransferPartsFragment[]
TokenTransfer: TokenTransferPartsFragment[]
TokenApproval: TokenApprovalPartsFragment[]
NftApproval: NftApprovalPartsFragment[]
NftApproveForAll: NftApproveForAllPartsFragment[]
}
// TODO: Move common contract metadata to a backend service
const UNI_IMG =
'https://raw.githubusercontent.com/Uniswap/assets/master/blockchains/ethereum/assets/0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984/logo.png'
const ENS_IMG =
'https://464911102-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/collections%2F2TjMAeHSzwlQgcOdL48E%2Ficon%2FKWP0gk2C6bdRPliWIA6o%2Fens%20transparent%20background.png?alt=media&token=bd28b063-5a75-4971-890c-97becea09076'
const COMMON_CONTRACTS: { [key: string]: Partial<Activity> | undefined } = {
[UNI_ADDRESS[SupportedChainId.MAINNET].toLowerCase()]: {
title: t`UNI Governance`,
descriptor: t`Contract Interaction`,
logos: [UNI_IMG],
},
// TODO(cartcrom): Add permit2-specific logo
'0x000000000022d473030f116ddee9f6b43ac78ba3': {
title: t`Permit2`,
descriptor: t`Uniswap Protocol`,
logos: [UNI_IMG],
},
'0x4976fb03c32e5b8cfe2b6ccb31c09ba78ebaba41': {
title: t`Ethereum Name Service`,
descriptor: t`Public Resolver`,
logos: [ENS_IMG],
},
'0x58774bb8acd458a640af0b88238369a167546ef2': {
title: t`Ethereum Name Service`,
descriptor: t`DNS Registrar`,
logos: [ENS_IMG],
},
'0x084b1c3c81545d370f3634392de611caabff8148': {
title: t`Ethereum Name Service`,
descriptor: t`Reverse Registrar`,
logos: [ENS_IMG],
},
'0x283af0b28c62c092c9727f1ee09c02ca627eb7f5': {
title: t`Ethereum Name Service`,
descriptor: t`ETH Registrar Controller`,
logos: [ENS_IMG],
},
}
function isSameAddress(a?: string, b?: string) {
return a === b || a?.toLowerCase() === b?.toLowerCase() // Lazy-lowercases the addresses
}
function callsPositionManagerContract(assetActivity: AssetActivityPartsFragment) {
return isSameAddress(
assetActivity.transaction.to,
NONFUNGIBLE_POSITION_MANAGER_ADDRESSES[fromGraphQLChain(assetActivity.chain)]
)
}
// Gets counts for number of NFTs in each collection present
function getCollectionCounts(nftTransfers: NftTransferPartsFragment[]): { [key: string]: number | undefined } {
return nftTransfers.reduce((acc, NFTChange) => {
const key = NFTChange.asset.collection?.name ?? NFTChange.asset.name
if (key) {
acc[key] = (acc?.[key] ?? 0) + 1
}
return acc
}, {} as { [key: string]: number | undefined })
}
function getSwapTitle(sent: TokenTransferPartsFragment, received: TokenTransferPartsFragment) {
if (
sent.tokenStandard === 'NATIVE' &&
isSameAddress(nativeOnChain(fromGraphQLChain(sent.asset.chain)).wrapped.address, received.asset.address)
)
return t`Wrapped`
else if (
received.tokenStandard === 'NATIVE' &&
isSameAddress(nativeOnChain(fromGraphQLChain(received.asset.chain)).wrapped.address, received.asset.address)
) {
return t`Unwrapped`
} else {
return t`Swapped`
}
}
function parseSwap(changes: TransactionChanges) {
if (changes.NftTransfer.length > 0 && changes.TokenTransfer.length === 1) {
const collectionCounts = getCollectionCounts(changes.NftTransfer)
const title = changes.NftTransfer[0].direction === 'IN' ? t`Bought` : t`Sold`
const descriptor = Object.entries(collectionCounts)
.map(([collectionName, count]) => `${count} ${collectionName}`)
.join()
return { title, descriptor }
} else if (changes.TokenTransfer.length === 2) {
const sent = changes.TokenTransfer.find((t) => t?.__typename === 'TokenTransfer' && t.direction === 'OUT')
const received = changes.TokenTransfer.find((t) => t?.__typename === 'TokenTransfer' && t.direction === 'IN')
if (sent && received) {
const inputAmount = formatNumberOrString(sent.quantity, NumberType.TokenNonTx)
const outputAmount = formatNumberOrString(received.quantity, NumberType.TokenNonTx)
return {
title: getSwapTitle(sent, received),
descriptor: `${inputAmount} ${sent.asset.symbol} for ${outputAmount} ${received.asset.symbol}`,
}
}
}
return { title: t`Unknown Swap` }
}
function parseApprove(changes: TransactionChanges) {
if (changes.TokenApproval.length === 1) {
const title = parseInt(changes.TokenApproval[0].quantity) === 0 ? t`Revoked Approval` : t`Approved`
const descriptor = `${changes.TokenApproval[0].asset.symbol}`
return { title, descriptor }
}
return { title: t`Unknown Approval` }
}
function parseLPTransfers(changes: TransactionChanges) {
const poolTokenA = changes.TokenTransfer[0]
const poolTokenB = changes.TokenTransfer[1]
const tokenAQuanitity = formatNumberOrString(poolTokenA.quantity, NumberType.TokenNonTx)
const tokenBQuantity = formatNumberOrString(poolTokenB.quantity, NumberType.TokenNonTx)
return {
descriptor: `${tokenAQuanitity} ${poolTokenA.asset.symbol} and ${tokenBQuantity} ${poolTokenB.asset.symbol}`,
logos: [poolTokenA.asset.project?.logo?.url, poolTokenB.asset.project?.logo?.url],
}
}
function parseSendReceive(changes: TransactionChanges, assetActivity: AssetActivityPartsFragment) {
// TODO(cartcrom): remove edge cases after backend implements
// Edge case: Receiving two token transfers in interaction w/ V3 manager === removing liquidity. These edge cases should potentially be moved to backend
if (changes.TokenTransfer.length === 2 && callsPositionManagerContract(assetActivity)) {
return { title: t`Removed Liquidity`, ...parseLPTransfers(changes) }
}
let transfer: NftTransferPartsFragment | TokenTransferPartsFragment | undefined
let assetName: string | undefined
let amount: string | undefined
if (changes.NftTransfer.length === 1) {
transfer = changes.NftTransfer[0]
assetName = transfer.asset.collection?.name
amount = '1'
} else if (changes.TokenTransfer.length === 1) {
transfer = changes.TokenTransfer[0]
assetName = transfer.asset.symbol
amount = formatNumberOrString(transfer.quantity, NumberType.TokenNonTx)
}
if (transfer && assetName && amount) {
return transfer.direction === 'IN'
? {
title: t`Received`,
descriptor: `${amount} ${assetName} ${t`from`} `,
otherAccount: isAddress(transfer.sender) || undefined,
}
: {
title: t`Sent`,
descriptor: `${amount} ${assetName} ${t`to`} `,
otherAccount: isAddress(transfer.recipient) || undefined,
}
}
return { title: t`Unknown Send` }
}
function parseMint(changes: TransactionChanges, assetActivity: AssetActivityPartsFragment) {
const collectionMap = getCollectionCounts(changes.NftTransfer)
if (Object.keys(collectionMap).length === 1) {
const collectionName = Object.keys(collectionMap)[0]
// Edge case: Minting a v3 positon represents adding liquidity
if (changes.TokenTransfer.length === 2 && callsPositionManagerContract(assetActivity)) {
return { title: t`Added Liquidity`, ...parseLPTransfers(changes) }
}
return { title: t`Minted`, descriptor: `${collectionMap[collectionName]} ${collectionName}` }
}
return { title: t`Unknown Mint` }
}
function parseUnknown(_changes: TransactionChanges, assetActivity: AssetActivityPartsFragment) {
return { title: t`Contract Interaction`, ...COMMON_CONTRACTS[assetActivity.transaction.to.toLowerCase()] }
}
type ActivityTypeParser = (changes: TransactionChanges, assetActivity: AssetActivityPartsFragment) => Partial<Activity>
const ActivityParserByType: { [key: string]: ActivityTypeParser | undefined } = {
[ActivityType.Swap]: parseSwap,
[ActivityType.Approve]: parseApprove,
[ActivityType.Send]: parseSendReceive,
[ActivityType.Receive]: parseSendReceive,
[ActivityType.Mint]: parseMint,
[ActivityType.Unknown]: parseUnknown,
}
function getLogoSrcs(changes: TransactionChanges): string[] {
// Uses set to avoid duplicate logos (e.g. nft's w/ same image url)
const logoSet = new Set<string | undefined>()
// Uses only NFT logos if they are present (will not combine nft image w/ token image)
if (changes.NftTransfer.length > 0) {
changes.NftTransfer.forEach((nftChange) => logoSet.add(nftChange.asset.image?.url))
} else {
changes.TokenTransfer.forEach((tokenChange) => logoSet.add(tokenChange.asset.project?.logo?.url))
changes.TokenApproval.forEach((tokenChange) => logoSet.add(tokenChange.asset.project?.logo?.url))
}
return Array.from(logoSet).filter(Boolean) as string[]
}
function parseRemoteActivity(assetActivity: AssetActivityPartsFragment): Activity | undefined {
try {
const changes = assetActivity.assetChanges.reduce(
(acc: TransactionChanges, assetChange) => {
if (assetChange.__typename === 'NftApproval') acc.NftApproval.push(assetChange)
else if (assetChange.__typename === 'NftApproveForAll') acc.NftApproveForAll.push(assetChange)
else if (assetChange.__typename === 'NftTransfer') acc.NftTransfer.push(assetChange)
else if (assetChange.__typename === 'TokenTransfer') acc.TokenTransfer.push(assetChange)
else if (assetChange.__typename === 'TokenApproval') acc.TokenApproval.push(assetChange)
return acc
},
{ NftTransfer: [], TokenTransfer: [], TokenApproval: [], NftApproval: [], NftApproveForAll: [] }
)
const defaultFields = {
hash: assetActivity.transaction.hash,
chainId: fromGraphQLChain(assetActivity.chain),
status: assetActivity.transaction.status,
timestamp: assetActivity.timestamp,
logos: getLogoSrcs(changes),
title: assetActivity.type,
descriptor: assetActivity.transaction.to,
receipt: assetActivity.transaction,
}
const parsedFields = ActivityParserByType[assetActivity.type]?.(changes, assetActivity)
return { ...defaultFields, ...parsedFields }
} catch (e) {
console.error('Failed to parse activity', e, assetActivity)
return undefined
}
}
export function parseRemoteActivities(assetActivities?: AssetActivityPartsFragment[]) {
return assetActivities?.reduce((acc: { [hash: string]: Activity }, assetActivity) => {
const activity = parseRemoteActivity(assetActivity)
if (activity) acc[activity.hash] = activity
return acc
}, {})
}
const getTimeSince = (timestamp: number) => {
const seconds = Math.floor(Date.now() - timestamp * 1000)
let interval
// TODO(cartcrom): use locale to determine date shorthands to use for non-english
if ((interval = seconds / ms`1y`) > 1) return Math.floor(interval) + 'y'
if ((interval = seconds / ms`30d`) > 1) return Math.floor(interval) + 'mo'
if ((interval = seconds / ms`1d`) > 1) return Math.floor(interval) + 'd'
if ((interval = seconds / ms`1h`) > 1) return Math.floor(interval) + 'h'
if ((interval = seconds / ms`1m`) > 1) return Math.floor(interval) + 'm'
else return Math.floor(seconds / ms`1s`) + 's'
}
/**
* Keeps track of the time since a given timestamp, keeping it up to date every second when necessary
* @param timestamp
* @returns
*/
export function useTimeSince(timestamp: number) {
const [timeSince, setTimeSince] = useState<string>(getTimeSince(timestamp))
useEffect(() => {
const refreshTime = () => {
if (Math.floor(Date.now() - timestamp * 1000) / ms`61s` <= 1) {
setTimeSince(getTimeSince(timestamp))
setTimeout(() => {
refreshTime()
}, ms`1s`)
}
}
refreshTime()
}, [timestamp])
return timeSince
}

View File

@@ -0,0 +1,20 @@
import { Currency } from '@uniswap/sdk-core'
import { SupportedChainId } from 'constants/chains'
import { AssetActivityPartsFragment, TransactionStatus } from 'graphql/data/__generated__/types-and-hooks'
type Receipt = AssetActivityPartsFragment['transaction']
export type Activity = {
hash: string
chainId: SupportedChainId
status: TransactionStatus
timestamp: number
title: string
descriptor?: string
logos?: Array<string | undefined>
currencies?: Array<Currency | undefined>
otherAccount?: string
receipt?: Receipt
}
export type ActivityMap = { [hash: string]: Activity | undefined }

View File

@@ -0,0 +1,54 @@
import { t } from '@lingui/macro'
import Column from 'components/Column'
import Row from 'components/Row'
import { PropsWithChildren } from 'react'
import { ChevronDown } from 'react-feather'
import styled from 'styled-components/macro'
import { ThemedText } from 'theme'
const ExpandIcon = styled(ChevronDown)<{ $expanded: boolean }>`
color: ${({ theme }) => theme.textSecondary};
transform: ${({ $expanded }) => ($expanded ? 'rotate(180deg)' : 'rotate(0deg)')};
transition: transform ${({ theme }) => theme.transition.duration.medium};
`
const ToggleButton = styled(Row)`
background-color: ${({ theme }) => theme.backgroundInteractive};
border-radius: 12px;
padding: 4px 8px 4px 12px;
height: 100%;
width: fit-content;
cursor: pointer;
:hover {
opacity: 0.8;
}
`
const Wrapper = styled(Column)<{ numItems: number; isExpanded: boolean }>`
height: ${({ numItems, isExpanded }) => (isExpanded ? numItems * 68 + 'px' : 0)};
transition: ${({ theme }) => `height ${theme.transition.duration.medium} ease-in-out`};
overflow: hidden;
`
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
return (
<>
<Row align="center" justify="space-between" padding="16px">
<ThemedText.SubHeader color="textSecondary" variant="subheadSmall">
{`${title} (${numItems})`}
</ThemedText.SubHeader>
<ToggleButton align="center" onClick={toggle}>
<ThemedText.LabelSmall color="textSecondary" variant="buttonLabelSmall">
{isExpanded ? t`Hide` : t`Show`}
</ThemedText.LabelSmall>
<ExpandIcon $expanded={isExpanded} />
</ToggleButton>
</Row>
<Wrapper numItems={numItems} isExpanded={isExpanded}>
{children}
</Wrapper>
</>
)
}

View File

@@ -0,0 +1,108 @@
import { sendAnalyticsEvent, useTrace } from '@uniswap/analytics'
import { InterfaceElementName, SharedEventName } from '@uniswap/analytics-events'
import { useToggleAccountDrawer } from 'components/AccountDrawer'
import Column from 'components/Column'
import Row from 'components/Row'
import { Box } from 'nft/components/Box'
import { NftCard } from 'nft/components/card'
import { detailsHref } from 'nft/components/card/utils'
import { VerifiedIcon } from 'nft/components/icons'
import { WalletAsset } from 'nft/types'
import { floorFormatter } from 'nft/utils'
import { useNavigate } from 'react-router-dom'
import styled from 'styled-components/macro'
import { ThemedText } from 'theme'
const FloorPrice = styled(Row)`
opacity: 0;
// prevent empty whitespace from collapsing line height to maintain
// consistent spacing below rows
white-space: pre;
`
const NFTContainer = styled(Column)`
gap: 8px;
min-height: 150px;
&:hover {
${FloorPrice} {
opacity: 1;
}
}
`
const NFTCollectionName = styled(ThemedText.BodySmall)`
white-space: pre;
text-overflow: ellipsis;
overflow: hidden;
`
export function NFT({
asset,
mediaShouldBePlaying,
setCurrentTokenPlayingMedia,
}: {
asset: WalletAsset
mediaShouldBePlaying: boolean
setCurrentTokenPlayingMedia: (tokenId: string | undefined) => void
}) {
const toggleWalletDrawer = useToggleAccountDrawer()
const navigate = useNavigate()
const trace = useTrace()
const navigateToNFTDetails = () => {
toggleWalletDrawer()
navigate(detailsHref(asset))
}
return (
<NFTContainer>
<NftCard
asset={asset}
hideDetails
display={{ disabledInfo: true }}
isSelected={false}
isDisabled={false}
onCardClick={navigateToNFTDetails}
sendAnalyticsEvent={() =>
sendAnalyticsEvent(SharedEventName.ELEMENT_CLICKED, {
element: InterfaceElementName.MINI_PORTFOLIO_NFT_ITEM,
collection_name: asset.collection?.name,
collection_address: asset.collection?.address,
token_id: asset.tokenId,
...trace,
})
}
mediaShouldBePlaying={mediaShouldBePlaying}
setCurrentTokenPlayingMedia={setCurrentTokenPlayingMedia}
testId="mini-portfolio-nft"
/>
<NFTDetails asset={asset} />
</NFTContainer>
)
}
function NFTDetails({ asset }: { asset: WalletAsset }) {
return (
<Box overflow="hidden" width="full" flexWrap="nowrap">
<Row gap="4px">
<NFTCollectionName>{asset.asset_contract.name}</NFTCollectionName>
{asset.collectionIsVerified && <Verified />}
</Row>
<FloorPrice>
<ThemedText.Caption color="textSecondary">
{asset.floorPrice ? `${floorFormatter(asset.floorPrice)} ETH` : ' '}
</ThemedText.Caption>
</FloorPrice>
</Box>
)
}
const BADGE_SIZE = '18px'
function Verified() {
return (
<Row width="unset" flexShrink="0">
<VerifiedIcon height={BADGE_SIZE} width={BADGE_SIZE} />
</Row>
)
}

View File

@@ -0,0 +1,78 @@
import { useNftBalance } from 'graphql/data/nft/NftBalance'
import { LoadingAssets } from 'nft/components/collection/CollectionAssetLoading'
import { EmptyWalletModule } from 'nft/components/profile/view/EmptyWalletContent'
import { useState } from 'react'
import InfiniteScroll from 'react-infinite-scroll-component'
import styled from 'styled-components/macro'
import { useAccountDrawer } from '../..'
import { DEFAULT_NFT_QUERY_AMOUNT } from '../constants'
import { NFT } from './NFTItem'
export default function NFTs({ account }: { account: string }) {
const [walletDrawerOpen, toggleWalletDrawer] = useAccountDrawer()
const { walletAssets, loading, hasNext, loadMore } = useNftBalance(
account,
[],
[],
DEFAULT_NFT_QUERY_AMOUNT,
undefined,
undefined,
undefined,
!walletDrawerOpen
)
const [currentTokenPlayingMedia, setCurrentTokenPlayingMedia] = useState<string | undefined>()
if (loading && !walletAssets)
return (
<AssetsContainer>
<LoadingAssets count={2} />
</AssetsContainer>
)
if (!walletAssets || walletAssets?.length === 0) {
return <EmptyWalletModule onNavigateClick={toggleWalletDrawer} />
}
return (
<InfiniteScroll
next={loadMore}
hasMore={hasNext ?? false}
loader={
Boolean(hasNext && walletAssets?.length) && (
<AssetsContainer>
<LoadingAssets count={2} />
</AssetsContainer>
)
}
dataLength={walletAssets?.length ?? 0}
style={{ overflow: 'unset' }}
scrollableTarget="wallet-dropdown-scroll-wrapper"
>
<AssetsContainer>
{walletAssets?.length
? walletAssets.map((asset, index) => {
return (
<NFT
setCurrentTokenPlayingMedia={setCurrentTokenPlayingMedia}
mediaShouldBePlaying={currentTokenPlayingMedia === asset.tokenId}
key={index}
asset={asset}
/>
)
})
: null}
</AssetsContainer>
</InfiniteScroll>
)
}
const AssetsContainer = styled.div`
display: grid;
gap: 12px;
// use minmax to not let grid items escape the parent container
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
margin: 16px;
`

View File

@@ -0,0 +1,140 @@
import { Token } from '@uniswap/sdk-core'
import { Pool, Position } from '@uniswap/v3-sdk'
import { SupportedChainId } from 'constants/chains'
import { useAllTokensMultichain } from 'hooks/Tokens'
import { atom, useAtom } from 'jotai'
import { atomWithStorage } from 'jotai/utils'
import ms from 'ms.macro'
import { useCallback } from 'react'
import { deserializeToken, serializeToken } from 'state/user/hooks'
import { SerializedToken } from 'state/user/types'
import { PositionDetails } from 'types/position'
import { buildCurrencyKey, currencyKey } from 'utils/currencyKey'
import { getTokensAsync } from './getTokensAsync'
import { useInterfaceMulticallContracts } from './hooks'
export type PositionInfo = {
owner: string
chainId: SupportedChainId
position: Position
pool: Pool
details: PositionDetails
inRange: boolean
closed: boolean
fees?: [number?, number?]
prices?: [number?, number?]
}
const POSITION_CACHE_EXPIRY = ms`1m` // 1 minute is arbitrary here
// Allows reusing recently fetched positions between component mounts
type CachedPositionsEntry = { result: PositionInfo[]; stale: boolean }
const cachedPositionsAtom = atom<{ [address: string]: CachedPositionsEntry | undefined }>({})
type UseCachedPositionsReturnType = [CachedPositionsEntry | undefined, (positions: PositionInfo[]) => void]
/**
* Caches positions to allow reusing between component mounts
* @param account address to cache positions for
* @returns cached positions for the account, whether the cache is stale, and a function to update the positions and cache
*/
export function useCachedPositions(account: string): UseCachedPositionsReturnType {
const [cachedPositions, setCachedPositions] = useAtom(cachedPositionsAtom)
const setPositionsAndStaleTimeout = useCallback(
(positions: PositionInfo[]) => {
setCachedPositions((cache) => ({ ...cache, [account]: { result: positions, stale: false } }))
setTimeout(
() =>
setCachedPositions((cache) => {
// sets stale to true if the positions haven't been updated since the timeout
if (positions === cache[account]?.result) {
return { ...cache, [account]: { result: positions, stale: true } }
} else {
return cache
}
}),
POSITION_CACHE_EXPIRY
)
},
[account, setCachedPositions]
)
return [cachedPositions[account], setPositionsAndStaleTimeout]
}
const poolAddressKey = (details: PositionDetails, chainId: SupportedChainId) =>
`${chainId}-${details.token0}-${details.token1}-${details.fee}`
type PoolAddressMap = { [key: string]: string | undefined }
const poolAddressCacheAtom = atomWithStorage<PoolAddressMap>('poolCache', {})
/**
* Caches pool addresses to prevent components from having to re-compute them
* @returns get and set functions for the cache
*/
export function usePoolAddressCache() {
const [cache, updateCache] = useAtom(poolAddressCacheAtom)
const get = useCallback(
(details: PositionDetails, chainId: SupportedChainId) => cache[poolAddressKey(details, chainId)],
[cache]
)
const set = useCallback(
(details: PositionDetails, chainId: SupportedChainId, address: string) =>
updateCache((c) => ({ ...c, [poolAddressKey(details, chainId)]: address })),
[updateCache]
)
return { get, set }
}
// These values are static, so we can persist them across sessions using `WithStorage`
const tokenCacheAtom = atomWithStorage<{ [key: string]: SerializedToken | undefined }>('cachedAsyncTokens', {})
function useTokenCache() {
const [cache, setCache] = useAtom(tokenCacheAtom)
const get = useCallback(
(chainId: number, address: string) => {
const entry = cache[buildCurrencyKey(chainId, address)]
return entry ? deserializeToken(entry) : undefined
},
[cache]
)
const set = useCallback(
(token?: Token) => {
if (token) {
setCache((cache) => ({ ...cache, [currencyKey(token)]: serializeToken(token) }))
}
},
[setCache]
)
return { get, set }
}
type TokenGetterFn = (addresses: string[], chainId: SupportedChainId) => Promise<{ [key: string]: Token | undefined }>
export function useGetCachedTokens(chains: SupportedChainId[]): TokenGetterFn {
const allTokens = useAllTokensMultichain()
const multicallContracts = useInterfaceMulticallContracts(chains)
const tokenCache = useTokenCache()
// Used to fetch tokens not available in local state
const fetchRemoteTokens: TokenGetterFn = useCallback(
async (addresses, chainId) => {
const fetched = await getTokensAsync(addresses, chainId, multicallContracts[chainId])
Object.values(fetched).forEach(tokenCache.set)
return fetched
},
[multicallContracts, tokenCache]
)
// Uses tokens from local state if available, otherwise fetches them
const getTokens: TokenGetterFn = useCallback(
async (addresses, chainId) => {
const local: { [address: string]: Token | undefined } = {}
const missing = new Set<string>()
addresses.forEach((address) => {
const cached = tokenCache.get(chainId, address) ?? allTokens[chainId][address]?.token
cached ? (local[address] = cached) : missing.add(address)
})
const fetched = await fetchRemoteTokens([...missing], chainId)
return { ...local, ...fetched }
},
[allTokens, fetchRemoteTokens, tokenCache]
)
return getTokens
}

View File

@@ -0,0 +1,128 @@
import { Token } from '@uniswap/sdk-core'
import ERC20_ABI from 'abis/erc20.json'
import { Erc20Interface } from 'abis/types/Erc20'
import { Erc20Bytes32Interface } from 'abis/types/Erc20Bytes32'
import { SupportedChainId } from 'constants/chains'
import { DEFAULT_ERC20_DECIMALS } from 'constants/tokens'
import { Interface } from 'ethers/lib/utils'
import { UniswapInterfaceMulticall } from 'types/v3'
import { isAddress } from 'utils'
import { arrayToSlices } from 'utils/arrays'
import { buildCurrencyKey, CurrencyKey, currencyKey } from 'utils/currencyKey'
type TokenMap = { [address: string]: Token | undefined }
export type Call = { target: string; callData: string; gasLimit: number }
type CallResult = { success: boolean; returnData: string }
export const DEFAULT_GAS_LIMIT = 1_000_000
const Erc20 = new Interface(ERC20_ABI) as Erc20Interface
const Erc20Bytes32 = new Interface(ERC20_ABI) as Erc20Bytes32Interface // Used for tokens that return bytes32 for name/symbol rather than string
// TODO(WEB-3060): cartcrom - adapt support for multi-function multi-interface multicalls into redux-multicall to remove than this custom cache/chunking logic
// Infura rejects calls with gas costs > 10x the current block gas limit; in such case we split the call into 2 chunks
async function fetchChunk(multicall: UniswapInterfaceMulticall, chunk: Call[]): Promise<CallResult[]> {
try {
return (await multicall.callStatic.multicall(chunk)).returnData
} catch (error) {
if (error.code === -32603 || error.message?.indexOf('execution ran out of gas') !== -1) {
if (chunk.length > 1) {
const half = Math.floor(chunk.length / 2)
return Promise.all([
fetchChunk(multicall, chunk.slice(0, half)),
fetchChunk(multicall, chunk.slice(half, chunk.length)),
]).then(([c0, c1]) => [...c0, ...c1])
}
}
console.error('Failed to fetch chunk', error)
throw error
}
}
function tryParseToken(address: string, chainId: SupportedChainId, data: CallResult[]) {
try {
const [nameData, symbolData, decimalsData, nameDataBytes32, symbolDataBytes32] = data
const name = nameData.success
? (Erc20.decodeFunctionResult('name', nameData.returnData)[0] as string)
: nameDataBytes32.success
? (Erc20Bytes32.decodeFunctionResult('name', nameDataBytes32.returnData)[0] as string)
: undefined
const symbol = symbolData.success
? (Erc20.decodeFunctionResult('symbol', symbolData.returnData)[0] as string)
: symbolDataBytes32.success
? (Erc20Bytes32.decodeFunctionResult('symbol', symbolDataBytes32.returnData)[0] as string)
: undefined
const decimals = decimalsData.success ? parseInt(decimalsData.returnData) : DEFAULT_ERC20_DECIMALS
return new Token(chainId, address, decimals, symbol, name)
} catch (error) {
console.error(`Failed to fetch token at address ${address} on chain ${chainId}`)
return undefined
}
}
function parseTokens(addresses: string[], chainId: SupportedChainId, returnData: CallResult[]) {
const tokenDataSlices = arrayToSlices(returnData, 5)
return tokenDataSlices.reduce((acc: TokenMap, slice, index) => {
const parsedToken = tryParseToken(addresses[index], chainId, slice)
if (parsedToken) acc[parsedToken.address] = parsedToken
return acc
}, {})
}
const createCalls = (target: string, callData: string[]): Call[] =>
callData.map((callData) => ({ target, callData, gasLimit: DEFAULT_GAS_LIMIT }))
function createCallsForToken(address: string) {
return createCalls(address, [
Erc20.encodeFunctionData('name'),
Erc20.encodeFunctionData('symbol'),
Erc20.encodeFunctionData('decimals'),
Erc20Bytes32.encodeFunctionData('name'),
Erc20Bytes32.encodeFunctionData('symbol'),
])
}
// Prevents tokens from being fetched multiple times
const TokenPromiseCache: { [key: CurrencyKey]: Promise<Token | undefined> | undefined } = {}
// Returns tokens using a single RPC call to the multicall contract
export async function getTokensAsync(
addresses: string[],
chainId: SupportedChainId,
multicall: UniswapInterfaceMulticall
): Promise<TokenMap> {
if (addresses.length === 0) return {}
const formattedAddresses: string[] = []
const calls: Call[] = []
const previouslyCalledTokens: Promise<Token | undefined>[] = []
addresses.forEach((tokenAddress) => {
const key = buildCurrencyKey(chainId, tokenAddress)
const previousCall = TokenPromiseCache[key]
if (previousCall !== undefined) {
previouslyCalledTokens.push(previousCall)
} else {
const formattedAddress = isAddress(tokenAddress)
if (!formattedAddress) return
formattedAddresses.push(formattedAddress)
calls.push(...createCallsForToken(formattedAddress))
}
})
const calledTokens = fetchChunk(multicall, calls).then((returnData) => parseTokens(addresses, chainId, returnData))
// Caches tokens currently being fetched for further calls to use
formattedAddresses.forEach(
(address) =>
(TokenPromiseCache[buildCurrencyKey(chainId, address)] = calledTokens.then((tokenMap) => tokenMap[address]))
)
const tokenMap = await calledTokens
// Add tokens from previous calls to the map of tokens fetched in this call
const resolvedPreviousTokens = await Promise.all(previouslyCalledTokens)
resolvedPreviousTokens.forEach((token) => token && (tokenMap[currencyKey(token)] = token))
return tokenMap
}

View File

@@ -0,0 +1,96 @@
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 { useWeb3React } from '@web3-react/core'
import { MULTICALL_ADDRESS, NONFUNGIBLE_POSITION_MANAGER_ADDRESSES as V3NFT_ADDRESSES } from 'constants/addresses'
import { isSupportedChain, SupportedChainId } from 'constants/chains'
import { RPC_PROVIDERS } from 'constants/providers'
import { BaseContract } from 'ethers/lib/ethers'
import { ContractInput, useUniswapPricesQuery } from 'graphql/data/__generated__/types-and-hooks'
import { toContractInput } from 'graphql/data/util'
import useStablecoinPrice from 'hooks/useStablecoinPrice'
import { useMemo } from 'react'
import { NonfungiblePositionManager, UniswapInterfaceMulticall } from 'types/v3'
import { getContract } from 'utils'
import { CurrencyKey, currencyKey, currencyKeyFromGraphQL } from 'utils/currencyKey'
import { PositionInfo } from './cache'
type ContractMap<T extends BaseContract> = { [key: number]: T }
// Constructs a chain-to-contract map, using the wallet's provider when available
function useContractMultichain<T extends BaseContract>(
addressMap: AddressMap,
ABI: any,
chainIds?: SupportedChainId[]
): ContractMap<T> {
const { chainId: walletChainId, provider: walletProvider } = useWeb3React()
return useMemo(() => {
const relevantChains =
chainIds ??
Object.keys(addressMap)
.map((chainId) => parseInt(chainId))
.filter(isSupportedChain)
return relevantChains.reduce((acc: ContractMap<T>, chainId) => {
const provider = walletProvider && walletChainId === chainId ? walletProvider : RPC_PROVIDERS[chainId]
acc[chainId] = getContract(addressMap[chainId], ABI, provider) as T
return acc
}, {})
}, [ABI, addressMap, chainIds, walletChainId, walletProvider])
}
export function useV3ManagerContracts(chainIds: SupportedChainId[]): ContractMap<NonfungiblePositionManager> {
return useContractMultichain<NonfungiblePositionManager>(V3NFT_ADDRESSES, NFTPositionManagerABI, chainIds)
}
export function useInterfaceMulticallContracts(chainIds: SupportedChainId[]): ContractMap<UniswapInterfaceMulticall> {
return useContractMultichain<UniswapInterfaceMulticall>(MULTICALL_ADDRESS, MulticallABI, chainIds)
}
type PriceMap = { [key: CurrencyKey]: number | undefined }
export function usePoolPriceMap(positions: PositionInfo[] | undefined) {
const contracts = useMemo(() => {
if (!positions || !positions.length) return []
// Avoids fetching duplicate tokens by placing in map
const contractMap = positions.reduce((acc: { [key: string]: ContractInput }, { pool: { token0, token1 } }) => {
acc[currencyKey(token0)] = toContractInput(token0)
acc[currencyKey(token1)] = toContractInput(token1)
return acc
}, {})
return Object.values(contractMap)
}, [positions])
const { data, loading } = useUniswapPricesQuery({ variables: { contracts }, skip: !contracts.length })
const priceMap = useMemo(
() =>
data?.tokens?.reduce((acc: PriceMap, current) => {
if (current) acc[currencyKeyFromGraphQL(current)] = current.project?.markets?.[0]?.price?.value
return acc
}, {}) ?? {},
[data?.tokens]
)
return { priceMap, pricesLoading: loading && !data }
}
function useFeeValue(token: Token, fee: number | undefined, queriedPrice: number | undefined) {
const stablecoinPrice = useStablecoinPrice(!queriedPrice ? token : undefined)
return useMemo(() => {
// Prefers gql price, as fetching stablecoinPrice will trigger multiple infura calls for each pool position
const price = queriedPrice ?? (stablecoinPrice ? parseFloat(stablecoinPrice.toSignificant()) : undefined)
const feeValue = fee && price ? fee * price : undefined
return [price, feeValue]
}, [fee, queriedPrice, stablecoinPrice])
}
export function useFeeValues(position: PositionInfo) {
const [priceA, feeValueA] = useFeeValue(position.pool.token0, position.fees?.[0], position.prices?.[0])
const [priceB, feeValueB] = useFeeValue(position.pool.token1, position.fees?.[1], position.prices?.[1])
return { priceA, priceB, fees: (feeValueA || 0) + (feeValueB || 0) }
}

View File

@@ -0,0 +1,67 @@
import { BigNumber } from '@ethersproject/bignumber'
import { SupportedChainId, WETH9 } from '@uniswap/sdk-core'
import { FeeAmount, Pool, Position } from '@uniswap/v3-sdk'
import { USDC_MAINNET } from 'constants/tokens'
import { mocked } from 'test-utils/mocked'
import { render } from 'test-utils/render'
import Pools from '.'
import useMultiChainPositions from './useMultiChainPositions'
jest.mock('./useMultiChainPositions')
const owner = '0xf5b6bb25f5beaea03dd014c6ef9fa9f3926bf36c'
const pool = new Pool(
USDC_MAINNET,
WETH9[SupportedChainId.MAINNET],
FeeAmount.MEDIUM,
'1851127709498178402383049949138810',
'7076437181775065414',
201189
)
const position = new Position({
pool,
liquidity: 1341008833950736,
tickLower: 200040,
tickUpper: 202560,
})
const details = {
nonce: BigNumber.from('0'),
tokenId: BigNumber.from('0'),
operator: '0x0',
token0: USDC_MAINNET.address,
token1: WETH9[SupportedChainId.MAINNET].address,
fee: FeeAmount.MEDIUM,
tickLower: -100,
tickUpper: 100,
liquidity: BigNumber.from('9000'),
feeGrowthInside0LastX128: BigNumber.from('0'),
feeGrowthInside1LastX128: BigNumber.from('0'),
tokensOwed0: BigNumber.from('0'),
tokensOwed1: BigNumber.from('0'),
}
const useMultiChainPositionsReturnValue = {
positions: [
{
owner,
chainId: SupportedChainId.MAINNET,
position,
pool,
details,
inRange: true,
closed: false,
},
],
loading: false,
}
beforeEach(() => {
mocked(useMultiChainPositions).mockReturnValue(useMultiChainPositionsReturnValue)
})
test('Pools should render LP positions', () => {
const props = { account: owner }
const { container } = render(<Pools {...props} />)
expect(container).not.toBeEmptyDOMElement()
})

View File

@@ -0,0 +1,167 @@
import { t } from '@lingui/macro'
import { TraceEvent } from '@uniswap/analytics'
import { BrowserEvent, InterfaceElementName, SharedEventName } from '@uniswap/analytics-events'
import { formatNumber, NumberType } from '@uniswap/conedison/format'
import { Position } from '@uniswap/v3-sdk'
import { useWeb3React } from '@web3-react/core'
import { useToggleAccountDrawer } from 'components/AccountDrawer'
import Row from 'components/Row'
import { MouseoverTooltip } from 'components/Tooltip'
import { EmptyWalletModule } from 'nft/components/profile/view/EmptyWalletContent'
import { useCallback, useMemo, useReducer } from 'react'
import { useNavigate } from 'react-router-dom'
import styled from 'styled-components/macro'
import { ThemedText } from 'theme'
import { switchChain } from 'utils/switchChain'
import { hasURL } from 'utils/urlChecks'
import { ExpandoRow } from '../ExpandoRow'
import { PortfolioLogo } from '../PortfolioLogo'
import PortfolioRow, { PortfolioSkeleton, PortfolioTabWrapper } from '../PortfolioRow'
import { PositionInfo } from './cache'
import { useFeeValues } from './hooks'
import useMultiChainPositions from './useMultiChainPositions'
export default function Pools({ account }: { account: string }) {
const { positions, loading } = useMultiChainPositions(account)
const [showClosed, toggleShowClosed] = useReducer((showClosed) => !showClosed, false)
const [openPositions, closedPositions] = useMemo(() => {
const openPositions: PositionInfo[] = []
const closedPositions: PositionInfo[] = []
positions?.forEach((position) => (position.closed ? closedPositions : openPositions).push(position))
return [openPositions, closedPositions]
}, [positions])
const toggleWalletDrawer = useToggleAccountDrawer()
if (!positions || loading) {
return <PortfolioSkeleton />
}
if (positions?.length === 0) {
return <EmptyWalletModule type="pool" onNavigateClick={toggleWalletDrawer} />
}
return (
<PortfolioTabWrapper>
{openPositions.map((positionInfo) => (
<PositionListItem
key={positionInfo.details.tokenId.toString() + positionInfo.chainId}
positionInfo={positionInfo}
/>
))}
<ExpandoRow
title={t`Closed Positions`}
isExpanded={showClosed}
toggle={toggleShowClosed}
numItems={closedPositions.length}
>
{closedPositions.map((positionInfo) => (
<PositionListItem
key={positionInfo.details.tokenId.toString() + positionInfo.chainId}
positionInfo={positionInfo}
/>
))}
</ExpandoRow>
</PortfolioTabWrapper>
)
}
const ActiveDot = styled.span<{ closed: boolean; outOfRange: boolean }>`
background-color: ${({ theme, closed, outOfRange }) =>
closed ? theme.textSecondary : outOfRange ? theme.accentWarning : theme.accentSuccess};
border-radius: 50%;
height: 8px;
width: 8px;
margin-left: 4px;
margin-top: 1px;
`
function calculcateLiquidityValue(price0: number | undefined, price1: number | undefined, position: Position) {
if (!price0 || !price1) return undefined
const value0 = parseFloat(position.amount0.toExact()) * price0
const value1 = parseFloat(position.amount1.toExact()) * price1
return value0 + value1
}
function PositionListItem({ positionInfo }: { positionInfo: PositionInfo }) {
const { chainId, position, pool, details, inRange, closed } = positionInfo
const { priceA, priceB, fees: feeValue } = useFeeValues(positionInfo)
const liquidityValue = calculcateLiquidityValue(priceA, priceB, position)
const navigate = useNavigate()
const toggleWalletDrawer = useToggleAccountDrawer()
const { chainId: walletChainId, connector } = useWeb3React()
const onClick = useCallback(async () => {
if (walletChainId !== chainId) await switchChain(connector, chainId)
toggleWalletDrawer()
navigate('/pool/' + details.tokenId)
}, [walletChainId, chainId, connector, toggleWalletDrawer, navigate, details.tokenId])
const analyticsEventProperties = useMemo(
() => ({
chain_id: chainId,
pool_token_0_symbol: pool.token0.symbol,
pool_token_1_symbol: pool.token1.symbol,
pool_token_0_address: pool.token0.address,
pool_token_1_address: pool.token1.address,
}),
[chainId, pool.token0.address, pool.token0.symbol, pool.token1.address, pool.token1.symbol]
)
const shouldHidePosition = hasURL(pool.token0.symbol) || hasURL(pool.token1.symbol)
if (shouldHidePosition) {
return null
}
return (
<TraceEvent
events={[BrowserEvent.onClick]}
name={SharedEventName.ELEMENT_CLICKED}
element={InterfaceElementName.MINI_PORTFOLIO_POOLS_ROW}
properties={analyticsEventProperties}
>
<PortfolioRow
onClick={onClick}
left={<PortfolioLogo chainId={chainId} currencies={[pool.token0, pool.token1]} />}
title={
<Row>
<ThemedText.SubHeader fontWeight={500}>
{pool.token0.symbol} / {pool.token1?.symbol}
</ThemedText.SubHeader>
</Row>
}
descriptor={<ThemedText.Caption>{`${pool.fee / 10000}%`}</ThemedText.Caption>}
right={
<>
<MouseoverTooltip
placement="left"
text={
<div style={{ padding: '4px 0px' }}>
<ThemedText.Caption>{`${formatNumber(
liquidityValue,
NumberType.PortfolioBalance
)} (liquidity) + ${formatNumber(feeValue, NumberType.PortfolioBalance)} (fees)`}</ThemedText.Caption>
</div>
}
>
<ThemedText.SubHeader fontWeight={500}>
{formatNumber((liquidityValue ?? 0) + (feeValue ?? 0), NumberType.PortfolioBalance)}
</ThemedText.SubHeader>
</MouseoverTooltip>
<Row justify="flex-end">
<ThemedText.Caption color="textSecondary">
{closed ? t`Closed` : inRange ? t`In range` : t`Out of range`}
</ThemedText.Caption>
<ActiveDot closed={closed} outOfRange={!inRange} />
</Row>
</>
}
/>
</TraceEvent>
)
}

View File

@@ -0,0 +1,226 @@
import { CurrencyAmount, Token } from '@uniswap/sdk-core'
import { abi as IUniswapV3PoolStateABI } 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'
import { DEFAULT_ERC20_DECIMALS } from 'constants/tokens'
import { BigNumber } from 'ethers/lib/ethers'
import { Interface } from 'ethers/lib/utils'
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { PositionDetails } from 'types/position'
import { NonfungiblePositionManager, UniswapInterfaceMulticall } from 'types/v3'
import { UniswapV3PoolInterface } from 'types/v3/UniswapV3Pool'
import { currencyKey } from 'utils/currencyKey'
import { PositionInfo, useCachedPositions, useGetCachedTokens, usePoolAddressCache } from './cache'
import { Call, DEFAULT_GAS_LIMIT } from './getTokensAsync'
import { useInterfaceMulticallContracts, usePoolPriceMap, useV3ManagerContracts } from './hooks'
function createPositionInfo(
owner: string,
chainId: SupportedChainId,
details: PositionDetails,
slot0: any,
tokenA: Token,
tokenB: Token
): PositionInfo {
/* Instantiates a Pool with a hardcoded 0 liqudity value since the sdk only uses this value for swap state and this avoids an RPC fetch */
const pool = new Pool(tokenA, tokenB, details.fee, slot0.sqrtPriceX96.toString(), 0, slot0.tick)
const position = new Position({
pool,
liquidity: details.liquidity.toString(),
tickLower: details.tickLower,
tickUpper: details.tickUpper,
})
const inRange = slot0.tick >= details.tickLower && slot0.tick < details.tickUpper
const closed = details.liquidity.eq(0)
return { owner, chainId, pool, position, details, inRange, closed }
}
type FeeAmounts = [BigNumber, BigNumber]
const MAX_UINT128 = BigNumber.from(2).pow(128).sub(1)
const DEFAULT_CHAINS = [
SupportedChainId.MAINNET,
SupportedChainId.ARBITRUM_ONE,
SupportedChainId.OPTIMISM,
SupportedChainId.POLYGON,
SupportedChainId.CELO,
]
type UseMultiChainPositionsData = { positions: PositionInfo[] | undefined; loading: boolean }
/**
* Returns all positions for a given account on multiple chains.
*
* This hook doesn't use the redux-multicall library to avoid having to manually fetching blocknumbers for each chain.
*
* @param account - account to fetch positions for
* @param chains - chains to fetch positions from
* @returns positions, fees
*/
export default function useMultiChainPositions(account: string, chains = DEFAULT_CHAINS): UseMultiChainPositionsData {
const pms = useV3ManagerContracts(chains)
const multicalls = useInterfaceMulticallContracts(chains)
const getTokens = useGetCachedTokens(chains)
const poolAddressCache = usePoolAddressCache()
const [cachedPositions, setPositions] = useCachedPositions(account)
const positions = cachedPositions?.result
const positionsFetching = useRef(false)
const positionsLoading = !cachedPositions?.result && positionsFetching.current
const [feeMap, setFeeMap] = useState<{ [key: string]: FeeAmounts }>({})
const { priceMap, pricesLoading } = usePoolPriceMap(positions)
const fetchPositionFees = useCallback(
async (pm: NonfungiblePositionManager, positionIds: BigNumber[], chainId: number) => {
const callData = positionIds.map((id) =>
pm.interface.encodeFunctionData('collect', [
{ tokenId: id, recipient: account, amount0Max: MAX_UINT128, amount1Max: MAX_UINT128 },
])
)
const fees = (await pm.callStatic.multicall(callData)).reduce((acc, feeBytes, index) => {
const key = chainId.toString() + positionIds[index]
acc[key] = pm.interface.decodeFunctionResult('collect', feeBytes) as FeeAmounts
return acc
}, {} as { [key: string]: FeeAmounts })
setFeeMap((prev) => ({ ...prev, ...fees }))
},
[account]
)
const fetchPositionIds = useCallback(
async (pm: NonfungiblePositionManager, balance: BigNumber) => {
const callData = Array.from({ length: balance.toNumber() }, (_, i) =>
pm.interface.encodeFunctionData('tokenOfOwnerByIndex', [account, i])
)
return (await pm.callStatic.multicall(callData)).map((idByte) => BigNumber.from(idByte))
},
[account]
)
const fetchPositionDetails = useCallback(async (pm: NonfungiblePositionManager, positionIds: BigNumber[]) => {
const callData = positionIds.map((id) => pm.interface.encodeFunctionData('positions', [id]))
return (await pm.callStatic.multicall(callData)).map(
(positionBytes, index) =>
({
...pm.interface.decodeFunctionResult('positions', positionBytes),
tokenId: positionIds[index],
} as unknown as PositionDetails)
)
}, [])
// 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 tokens = await getTokens(
positionDetails.flatMap((details) => [details.token0, details.token1]),
chainId
)
const calls: Call[] = []
const poolPairs: [Token, Token][] = []
positionDetails.forEach((details) => {
const tokenA = tokens[details.token0] ?? new Token(chainId, details.token0, DEFAULT_ERC20_DECIMALS)
const tokenB = tokens[details.token1] ?? new Token(chainId, details.token1, DEFAULT_ERC20_DECIMALS)
let poolAddress = poolAddressCache.get(details, chainId)
if (!poolAddress) {
const factoryAddress = V3_CORE_FACTORY_ADDRESSES[chainId]
poolAddress = computePoolAddress({ factoryAddress, tokenA, tokenB, fee: details.fee })
poolAddressCache.set(details, chainId, poolAddress)
}
poolPairs.push([tokenA, tokenB])
calls.push({
target: poolAddress,
callData: poolInterface.encodeFunctionData('slot0'),
gasLimit: DEFAULT_GAS_LIMIT,
})
}, [])
return (await multicall.callStatic.multicall(calls)).returnData.reduce((acc: PositionInfo[], result, i) => {
if (result.success) {
const slot0 = poolInterface.decodeFunctionResult('slot0', result.returnData)
acc.push(createPositionInfo(account, chainId, positionDetails[i], slot0, ...poolPairs[i]))
} else {
console.debug('slot0 fetch errored', result)
}
return acc
}, [])
},
[account, poolAddressCache, getTokens]
)
const fetchPositionsForChain = useCallback(
async (chainId: SupportedChainId): Promise<PositionInfo[]> => {
try {
const pm = pms[chainId]
const multicall = multicalls[chainId]
const balance = await pm?.balanceOf(account)
if (!pm || !multicall || balance.lt(1)) return []
const positionIds = await fetchPositionIds(pm, balance)
// Fetches fees in the background and stores them separetely from the results of this function
fetchPositionFees(pm, positionIds, chainId)
const postionDetails = await fetchPositionDetails(pm, positionIds)
return fetchPositionInfo(postionDetails, chainId, multicall)
} catch (error) {
console.error(`Failed to fetch positions for chain ${chainId}`, error)
return []
}
},
[account, fetchPositionDetails, fetchPositionFees, fetchPositionIds, fetchPositionInfo, pms, multicalls]
)
const fetchAllPositions = useCallback(async () => {
positionsFetching.current = true
const positions = (await Promise.all(chains.map(fetchPositionsForChain))).flat()
positionsFetching.current = false
setPositions(positions)
}, [chains, fetchPositionsForChain, setPositions])
// Fetches positions when existing positions are stale and the document has focus
useEffect(() => {
if (positionsFetching.current || cachedPositions?.stale === false) return
else if (document.hasFocus()) {
fetchAllPositions()
} else {
// Avoids refetching positions until the user returns to Interface to avoid polling unnused rpc data
const onFocus = () => {
fetchAllPositions()
window.removeEventListener('focus', onFocus)
}
window.addEventListener('focus', onFocus)
return () => {
window.removeEventListener('focus', onFocus)
}
}
return
}, [fetchAllPositions, positionsFetching, cachedPositions?.stale])
const positionsWithFeesAndPrices: PositionInfo[] | undefined = useMemo(
() =>
positions?.map((position) => {
const key = position.chainId.toString() + position.details.tokenId
const fees = feeMap[key]
? [
// We parse away from SDK/ethers types so fees can be multiplied by primitive number prices
parseFloat(CurrencyAmount.fromRawAmount(position.pool.token0, feeMap[key]?.[0].toString()).toExact()),
parseFloat(CurrencyAmount.fromRawAmount(position.pool.token1, feeMap[key]?.[1].toString()).toExact()),
]
: undefined
const prices = [priceMap[currencyKey(position.pool.token0)], priceMap[currencyKey(position.pool.token1)]]
return { ...position, fees, prices } as PositionInfo
}),
[feeMap, positions, priceMap]
)
return { positions: positionsWithFeesAndPrices, loading: pricesLoading || positionsLoading }
}

View File

@@ -0,0 +1,20 @@
import { SupportedChainId } from '@uniswap/sdk-core'
import { DAI_ARBITRUM } from '@uniswap/smart-order-router'
import { DAI, USDC_ARBITRUM, USDC_MAINNET } from 'constants/tokens'
import { render } from 'test-utils/render'
import { PortfolioLogo } from './PortfolioLogo'
describe('PortfolioLogo', () => {
it('renders without L2 icon', () => {
const { container } = render(<PortfolioLogo chainId={SupportedChainId.MAINNET} currencies={[DAI, USDC_MAINNET]} />)
expect(container).toMatchSnapshot()
})
it('renders with L2 icon', () => {
const { container } = render(
<PortfolioLogo chainId={SupportedChainId.ARBITRUM_ONE} currencies={[DAI_ARBITRUM, USDC_ARBITRUM]} />
)
expect(container).toMatchSnapshot()
})
})

View File

@@ -0,0 +1,161 @@
import { Currency } from '@uniswap/sdk-core'
import blankTokenUrl from 'assets/svg/blank_token.svg'
import { ReactComponent as UnknownStatus } from 'assets/svg/contract-interaction.svg'
import { LogoImage, MissingImageLogo } from 'components/Logo/AssetLogo'
import { Unicon } from 'components/Unicon'
import { getChainInfo } from 'constants/chainInfo'
import { SupportedChainId } from 'constants/chains'
import useTokenLogoSource from 'hooks/useAssetLogoSource'
import useENSAvatar from 'hooks/useENSAvatar'
import React from 'react'
import { Loader } from 'react-feather'
import styled, { useTheme } from 'styled-components/macro'
const UnknownContract = styled(UnknownStatus)`
color: ${({ theme }) => theme.textSecondary};
`
const DoubleLogoContainer = styled.div`
display: flex;
flex-direction: row;
gap: 2px;
position: relative;
top: 0;
left: 0;
${LogoImage}:nth-child(n) {
width: 19px;
height: 40px;
object-fit: cover;
}
${LogoImage}:nth-child(1) {
border-radius: 20px 0 0 20px;
object-position: 0 0;
}
${LogoImage}:nth-child(2) {
border-radius: 0 20px 20px 0;
object-position: 100% 0;
}
`
type MultiLogoProps = {
chainId: SupportedChainId
accountAddress?: string
currencies?: Array<Currency | undefined>
images?: (string | undefined)[]
size?: string
style?: React.CSSProperties
}
const StyledLogoParentContainer = styled.div`
position: relative;
top: 0;
left: 0;
`
const ENSAvatarImg = styled.img`
border-radius: 8px;
height: 40px;
width: 40px;
`
const StyledChainLogo = styled.img`
height: 14px;
width: 14px;
`
const SquareChainLogo = styled.img`
height: 100%;
width: 100%;
`
const L2LogoContainer = styled.div<{ $backgroundColor?: string }>`
background-color: ${({ $backgroundColor }) => $backgroundColor};
border-radius: 2px;
height: 16px;
left: 60%;
position: absolute;
top: 60%;
outline: 2px solid ${({ theme }) => theme.backgroundSurface};
width: 16px;
display: flex;
align-items: center;
justify-content: center;
`
/**
* Renders an image by prioritizing a list of sources, and then eventually a fallback triangle alert
*/
export function PortfolioLogo({
chainId = SupportedChainId.MAINNET,
accountAddress,
currencies,
images,
size = '40px',
style,
}: MultiLogoProps) {
const { squareLogoUrl, logoUrl } = getChainInfo(chainId)
const chainLogo = squareLogoUrl ?? logoUrl
const { avatar, loading } = useENSAvatar(accountAddress, false)
const theme = useTheme()
const [src, nextSrc] = useTokenLogoSource(currencies?.[0]?.wrapped.address, chainId, currencies?.[0]?.isNative)
const [src2, nextSrc2] = useTokenLogoSource(currencies?.[1]?.wrapped.address, chainId, currencies?.[1]?.isNative)
let component
if (accountAddress) {
component = loading ? (
<Loader size={size} />
) : avatar ? (
<ENSAvatarImg src={avatar} alt="avatar" />
) : (
<Unicon size={40} address={accountAddress} />
)
} else if (currencies && currencies.length) {
const logo1 = <LogoImage size={size} src={src ?? blankTokenUrl} onError={nextSrc} />
const logo2 = <LogoImage size={size} src={src2 ?? blankTokenUrl} onError={nextSrc2} />
component =
currencies.length > 1 ? (
<DoubleLogoContainer style={style}>
{logo1}
{logo2}
</DoubleLogoContainer>
) : src ? (
logo1
) : (
<MissingImageLogo size={size}>
{currencies[0]?.symbol?.toUpperCase().replace('$', '').replace(/\s+/g, '').slice(0, 3)}
</MissingImageLogo>
)
} else if (images && images.length) {
component =
images.length > 1 ? (
<DoubleLogoContainer style={style}>
<LogoImage size={size} src={images[0]} />
<LogoImage size={size} src={images[images.length - 1]} />
</DoubleLogoContainer>
) : (
<LogoImage size={size} src={images[0]} />
)
} else {
return <UnknownContract width={size} height={size} />
}
const L2Logo =
chainId !== SupportedChainId.MAINNET && chainLogo ? (
<L2LogoContainer $backgroundColor={squareLogoUrl ? theme.backgroundSurface : theme.textPrimary}>
{squareLogoUrl ? (
<SquareChainLogo src={chainLogo} alt="chainLogo" />
) : (
<StyledChainLogo src={chainLogo} alt="chainLogo" />
)}
</L2LogoContainer>
) : null
return (
<StyledLogoParentContainer>
{component}
{L2Logo}
</StyledLogoParentContainer>
)
}

View File

@@ -0,0 +1,92 @@
import Column, { AutoColumn } from 'components/Column'
import Row from 'components/Row'
import { LoadingBubble } from 'components/Tokens/loading'
import styled, { css, keyframes } from 'styled-components/macro'
export const PortfolioRowWrapper = styled(Row)<{ onClick?: any }>`
gap: 12px;
height: 68px;
padding: 0 16px;
transition: ${({ theme }) => `${theme.transition.duration.medium} ${theme.transition.timing.ease} background-color`};
${({ onClick }) => onClick && 'cursor: pointer'};
&:hover {
cursor: pointer;
}
`
const EndColumn = styled(Column)`
align-items: flex-end;
`
export default function PortfolioRow({
left,
title,
descriptor,
right,
onClick,
}: {
left: React.ReactNode
title: React.ReactNode
descriptor?: React.ReactNode
right?: React.ReactNode
setIsHover?: (b: boolean) => void
onClick?: () => void
}) {
return (
<PortfolioRowWrapper onClick={onClick}>
{left}
<AutoColumn grow>
{title}
{descriptor}
</AutoColumn>
{right && <EndColumn>{right}</EndColumn>}
</PortfolioRowWrapper>
)
}
function PortfolioSkeletonRow({ shrinkRight }: { shrinkRight?: boolean }) {
return (
<PortfolioRowWrapper>
<LoadingBubble height="40px" width="40px" round />
<AutoColumn grow gap="4px">
<LoadingBubble height="16px" width="60px" delay="300ms" />
<LoadingBubble height="10px" width="90px" delay="300ms" />
</AutoColumn>
<EndColumn gap="xs">
{shrinkRight ? (
<LoadingBubble height="12px" width="20px" delay="600ms" />
) : (
<>
<LoadingBubble height="14px" width="70px" delay="600ms" />
<LoadingBubble height="14px" width="50px" delay="600ms" />
</>
)}
</EndColumn>
</PortfolioRowWrapper>
)
}
export function PortfolioSkeleton({ shrinkRight = false }: { shrinkRight?: boolean }) {
return (
<>
{Array.from({ length: 5 }).map((_, i) => (
<PortfolioSkeletonRow shrinkRight={shrinkRight} key={`portfolio loading row${i}`} />
))}
</>
)
}
const fadeIn = keyframes`
from { opacity: .25 }
to { opacity: 1 }
`
export const portfolioFadeInAnimation = css`
animation: ${fadeIn} ${({ theme }) => `${theme.transition.duration.medium} ${theme.transition.timing.in}`};
`
export const PortfolioTabWrapper = styled.div`
${portfolioFadeInAnimation}
`

View File

@@ -0,0 +1,138 @@
import { TraceEvent } from '@uniswap/analytics'
import { BrowserEvent, InterfaceElementName, SharedEventName } from '@uniswap/analytics-events'
import { formatNumber, NumberType } from '@uniswap/conedison/format'
import Row from 'components/Row'
import { formatDelta } from 'components/Tokens/TokenDetails/PriceChart'
import { PortfolioBalancesQuery, usePortfolioBalancesQuery } from 'graphql/data/__generated__/types-and-hooks'
import { getTokenDetailsURL, gqlToCurrency } from 'graphql/data/util'
import { useAtomValue } from 'jotai/utils'
import { EmptyWalletModule } from 'nft/components/profile/view/EmptyWalletContent'
import { useCallback, useMemo, useState } from 'react'
import { useNavigate } from 'react-router-dom'
import styled from 'styled-components/macro'
import { EllipsisStyle, ThemedText } from 'theme'
import { useToggleAccountDrawer } from '../..'
import { PortfolioArrow } from '../../AuthenticatedHeader'
import { hideSmallBalancesAtom } from '../../SmallBalanceToggle'
import { ExpandoRow } from '../ExpandoRow'
import { PortfolioLogo } from '../PortfolioLogo'
import PortfolioRow, { PortfolioSkeleton, PortfolioTabWrapper } from '../PortfolioRow'
const HIDE_SMALL_USD_BALANCES_THRESHOLD = 1
function meetsThreshold(tokenBalance: TokenBalance, hideSmallBalances: boolean) {
return !hideSmallBalances || (tokenBalance.denominatedValue?.value ?? 0) > HIDE_SMALL_USD_BALANCES_THRESHOLD
}
export default function Tokens({ account }: { account: string }) {
const toggleWalletDrawer = useToggleAccountDrawer()
const hideSmallBalances = useAtomValue(hideSmallBalancesAtom)
const [showHiddenTokens, setShowHiddenTokens] = useState(false)
const { data } = usePortfolioBalancesQuery({
variables: { ownerAddress: account },
fetchPolicy: 'cache-only', // PrefetchBalancesWrapper handles balance fetching/staleness; this component only reads from cache
errorPolicy: 'all',
})
const visibleTokens = useMemo(() => {
return !hideSmallBalances
? data?.portfolios?.[0].tokenBalances ?? []
: data?.portfolios?.[0].tokenBalances?.filter((tokenBalance) =>
meetsThreshold(tokenBalance, hideSmallBalances)
) ?? []
}, [data?.portfolios, hideSmallBalances])
const hiddenTokens = useMemo(() => {
return !hideSmallBalances
? []
: data?.portfolios?.[0].tokenBalances?.filter(
(tokenBalance) => !meetsThreshold(tokenBalance, hideSmallBalances)
) ?? []
}, [data?.portfolios, hideSmallBalances])
if (!data) {
return <PortfolioSkeleton />
}
if (data?.portfolios?.[0].tokenBalances?.length === 0) {
// TODO: consider launching moonpay here instead of just closing the drawer
return <EmptyWalletModule type="token" onNavigateClick={toggleWalletDrawer} />
}
const toggleHiddenTokens = () => setShowHiddenTokens((showHiddenTokens) => !showHiddenTokens)
return (
<PortfolioTabWrapper>
{visibleTokens.map(
(tokenBalance) =>
tokenBalance.token &&
meetsThreshold(tokenBalance, hideSmallBalances) && (
<TokenRow key={tokenBalance.id} {...tokenBalance} token={tokenBalance.token} />
)
)}
<ExpandoRow isExpanded={showHiddenTokens} toggle={toggleHiddenTokens} numItems={hiddenTokens.length}>
{hiddenTokens.map(
(tokenBalance) =>
tokenBalance.token && <TokenRow key={tokenBalance.id} {...tokenBalance} token={tokenBalance.token} />
)}
</ExpandoRow>
</PortfolioTabWrapper>
)
}
const TokenBalanceText = styled(ThemedText.BodySecondary)`
${EllipsisStyle}
`
type TokenBalance = NonNullable<
NonNullable<NonNullable<PortfolioBalancesQuery['portfolios']>[number]>['tokenBalances']
>[number]
type PortfolioToken = NonNullable<TokenBalance['token']>
function TokenRow({ token, quantity, denominatedValue, tokenProjectMarket }: TokenBalance & { token: PortfolioToken }) {
const percentChange = tokenProjectMarket?.pricePercentChange?.value ?? 0
const navigate = useNavigate()
const toggleWalletDrawer = useToggleAccountDrawer()
const navigateToTokenDetails = useCallback(async () => {
navigate(getTokenDetailsURL(token))
toggleWalletDrawer()
}, [navigate, token, toggleWalletDrawer])
const currency = gqlToCurrency(token)
return (
<TraceEvent
events={[BrowserEvent.onClick]}
name={SharedEventName.ELEMENT_CLICKED}
element={InterfaceElementName.MINI_PORTFOLIO_TOKEN_ROW}
properties={{ chain_id: currency.chainId, token_name: token?.name, address: token?.address }}
>
<PortfolioRow
left={<PortfolioLogo chainId={currency.chainId} currencies={[currency]} size="40px" />}
title={<ThemedText.SubHeader fontWeight={500}>{token?.name}</ThemedText.SubHeader>}
descriptor={
<TokenBalanceText>
{formatNumber(quantity, NumberType.TokenNonTx)} {token?.symbol}
</TokenBalanceText>
}
onClick={navigateToTokenDetails}
right={
denominatedValue && (
<>
<ThemedText.SubHeader fontWeight={500}>
{formatNumber(denominatedValue?.value, NumberType.PortfolioBalance)}
</ThemedText.SubHeader>
<Row justify="flex-end">
<PortfolioArrow change={percentChange} size={20} strokeWidth={1.75} />
<ThemedText.BodySecondary>{formatDelta(percentChange)}</ThemedText.BodySecondary>
</Row>
</>
)
}
/>
</TraceEvent>
)
}

View File

@@ -0,0 +1,164 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`PortfolioLogo renders with L2 icon 1`] = `
.c3 {
width: 40px;
height: 40px;
border-radius: 50%;
}
.c1 {
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
-webkit-flex-direction: row;
-ms-flex-direction: row;
flex-direction: row;
gap: 2px;
position: relative;
top: 0;
left: 0;
}
.c1 .c2:nth-child(n) {
width: 19px;
height: 40px;
object-fit: cover;
}
.c1 .c2:nth-child(1) {
border-radius: 20px 0 0 20px;
object-position: 0 0;
}
.c1 .c2:nth-child(2) {
border-radius: 0 20px 20px 0;
object-position: 100% 0;
}
.c0 {
position: relative;
top: 0;
left: 0;
}
.c5 {
height: 14px;
width: 14px;
}
.c4 {
background-color: #0D111C;
border-radius: 2px;
height: 16px;
left: 60%;
position: absolute;
top: 60%;
outline: 2px solid #FFFFFF;
width: 16px;
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
-webkit-align-items: center;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
-webkit-box-pack: center;
-webkit-justify-content: center;
-ms-flex-pack: center;
justify-content: center;
}
<div>
<div
class="c0"
>
<div
class="c1"
>
<img
class="c2 c3"
src="https://raw.githubusercontent.com/Uniswap/assets/master/blockchains/arbitrum/assets/0xDA10009cBd5D07dd0CeCc66161FC93D7c9000da1/logo.png"
/>
<img
class="c2 c3"
src="https://raw.githubusercontent.com/Uniswap/assets/master/blockchains/arbitrum/assets/0xFF970A61A04b1cA14834A43f5dE4533eBDDB5CC8/logo.png"
/>
</div>
<div
class="c4"
>
<img
alt="chainLogo"
class="c5"
src="arbitrum_logo.svg"
/>
</div>
</div>
</div>
`;
exports[`PortfolioLogo renders without L2 icon 1`] = `
.c3 {
width: 40px;
height: 40px;
border-radius: 50%;
}
.c1 {
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
-webkit-flex-direction: row;
-ms-flex-direction: row;
flex-direction: row;
gap: 2px;
position: relative;
top: 0;
left: 0;
}
.c1 .c2:nth-child(n) {
width: 19px;
height: 40px;
object-fit: cover;
}
.c1 .c2:nth-child(1) {
border-radius: 20px 0 0 20px;
object-position: 0 0;
}
.c1 .c2:nth-child(2) {
border-radius: 0 20px 20px 0;
object-position: 100% 0;
}
.c0 {
position: relative;
top: 0;
left: 0;
}
<div>
<div
class="c0"
>
<div
class="c1"
>
<img
class="c2 c3"
src="https://raw.githubusercontent.com/Uniswap/assets/master/blockchains/ethereum/assets/0x6B175474E89094C44Da98b954EedeAC495271d0F/logo.png"
/>
<img
class="c2 c3"
src="https://raw.githubusercontent.com/Uniswap/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png"
/>
</div>
</div>
</div>
`;

View File

@@ -0,0 +1,155 @@
import { t } from '@lingui/macro'
import { TransactionStatus } from 'graphql/data/__generated__/types-and-hooks'
import { TransactionType } from 'state/transactions/types'
// use even number because rows are in groups of 2
export const DEFAULT_NFT_QUERY_AMOUNT = 26
const TransactionTitleTable: { [key in TransactionType]: { [state in TransactionStatus]: string } } = {
[TransactionType.SWAP]: {
[TransactionStatus.Pending]: t`Swapping`,
[TransactionStatus.Confirmed]: t`Swapped`,
[TransactionStatus.Failed]: t`Swap failed`,
},
[TransactionType.WRAP]: {
[TransactionStatus.Pending]: t`Wrapping`,
[TransactionStatus.Confirmed]: t`Wrapped`,
[TransactionStatus.Failed]: t`Wrap failed`,
},
[TransactionType.ADD_LIQUIDITY_V3_POOL]: {
[TransactionStatus.Pending]: t`Adding liquidity`,
[TransactionStatus.Confirmed]: t`Added liquidity`,
[TransactionStatus.Failed]: t`Add liquidity failed`,
},
[TransactionType.REMOVE_LIQUIDITY_V3]: {
[TransactionStatus.Pending]: t`Removing liquidity`,
[TransactionStatus.Confirmed]: t`Removed liquidity`,
[TransactionStatus.Failed]: t`Remove liquidity failed`,
},
[TransactionType.CREATE_V3_POOL]: {
[TransactionStatus.Pending]: t`Creating pool`,
[TransactionStatus.Confirmed]: t`Created pool`,
[TransactionStatus.Failed]: t`Create pool failed`,
},
[TransactionType.COLLECT_FEES]: {
[TransactionStatus.Pending]: t`Collecting fees`,
[TransactionStatus.Confirmed]: t`Collected fees`,
[TransactionStatus.Failed]: t`Collect fees failed`,
},
[TransactionType.APPROVAL]: {
[TransactionStatus.Pending]: t`Approving`,
[TransactionStatus.Confirmed]: t`Approved`,
[TransactionStatus.Failed]: t`Approval failed`,
},
[TransactionType.CLAIM]: {
[TransactionStatus.Pending]: t`Claiming`,
[TransactionStatus.Confirmed]: t`Claimed`,
[TransactionStatus.Failed]: t`Claim failed`,
},
[TransactionType.BUY]: {
[TransactionStatus.Pending]: t`Buying`,
[TransactionStatus.Confirmed]: t`Bought`,
[TransactionStatus.Failed]: t`Buy failed`,
},
[TransactionType.SEND]: {
[TransactionStatus.Pending]: t`Sending`,
[TransactionStatus.Confirmed]: t`Sent`,
[TransactionStatus.Failed]: t`Send failed`,
},
[TransactionType.RECEIVE]: {
[TransactionStatus.Pending]: t`Receiving`,
[TransactionStatus.Confirmed]: t`Received`,
[TransactionStatus.Failed]: t`Receive failed`,
},
[TransactionType.MINT]: {
[TransactionStatus.Pending]: t`Minting`,
[TransactionStatus.Confirmed]: t`Minted`,
[TransactionStatus.Failed]: t`Mint failed`,
},
[TransactionType.BURN]: {
[TransactionStatus.Pending]: t`Burning`,
[TransactionStatus.Confirmed]: t`Burned`,
[TransactionStatus.Failed]: t`Burn failed`,
},
[TransactionType.VOTE]: {
[TransactionStatus.Pending]: t`Voting`,
[TransactionStatus.Confirmed]: t`Voted`,
[TransactionStatus.Failed]: t`Vote failed`,
},
[TransactionType.QUEUE]: {
[TransactionStatus.Pending]: t`Queuing`,
[TransactionStatus.Confirmed]: t`Queued`,
[TransactionStatus.Failed]: t`Queue failed`,
},
[TransactionType.EXECUTE]: {
[TransactionStatus.Pending]: t`Executing`,
[TransactionStatus.Confirmed]: t`Executed`,
[TransactionStatus.Failed]: t`Execute failed`,
},
[TransactionType.BORROW]: {
[TransactionStatus.Pending]: t`Borrowing`,
[TransactionStatus.Confirmed]: t`Borrowed`,
[TransactionStatus.Failed]: t`Borrow failed`,
},
[TransactionType.REPAY]: {
[TransactionStatus.Pending]: t`Repaying`,
[TransactionStatus.Confirmed]: t`Repaid`,
[TransactionStatus.Failed]: t`Repay failed`,
},
[TransactionType.DEPLOY]: {
[TransactionStatus.Pending]: t`Deploying`,
[TransactionStatus.Confirmed]: t`Deployed`,
[TransactionStatus.Failed]: t`Deploy failed`,
},
[TransactionType.CANCEL]: {
[TransactionStatus.Pending]: t`Cancelling`,
[TransactionStatus.Confirmed]: t`Cancelled`,
[TransactionStatus.Failed]: t`Cancel failed`,
},
[TransactionType.DELEGATE]: {
[TransactionStatus.Pending]: t`Delegating`,
[TransactionStatus.Confirmed]: t`Delegated`,
[TransactionStatus.Failed]: t`Delegate failed`,
},
[TransactionType.DEPOSIT_LIQUIDITY_STAKING]: {
[TransactionStatus.Pending]: t`Depositing`,
[TransactionStatus.Confirmed]: t`Deposited`,
[TransactionStatus.Failed]: t`Deposit failed`,
},
[TransactionType.WITHDRAW_LIQUIDITY_STAKING]: {
[TransactionStatus.Pending]: t`Withdrawing`,
[TransactionStatus.Confirmed]: t`Withdrew`,
[TransactionStatus.Failed]: t`Withdraw failed`,
},
[TransactionType.ADD_LIQUIDITY_V2_POOL]: {
[TransactionStatus.Pending]: t`Adding V2 liquidity`,
[TransactionStatus.Confirmed]: t`Added V2 liquidity`,
[TransactionStatus.Failed]: t`Add V2 liquidity failed`,
},
[TransactionType.MIGRATE_LIQUIDITY_V3]: {
[TransactionStatus.Pending]: t`Migrating liquidity`,
[TransactionStatus.Confirmed]: t`Migrated liquidity`,
[TransactionStatus.Failed]: t`Migrate liquidity failed`,
},
[TransactionType.SUBMIT_PROPOSAL]: {
[TransactionStatus.Pending]: t`Submitting proposal`,
[TransactionStatus.Confirmed]: t`Submitted proposal`,
[TransactionStatus.Failed]: t`Submit proposal failed`,
},
}
const AlternateTransactionTitleTable: { [key in TransactionType]?: { [state in TransactionStatus]: string } } = {
[TransactionType.WRAP]: {
[TransactionStatus.Pending]: t`Unwrapping`,
[TransactionStatus.Confirmed]: t`Unwrapped`,
[TransactionStatus.Failed]: t`Unwrap failed`,
},
}
export function getActivityTitle(type: TransactionType, status: TransactionStatus, alternate?: boolean) {
if (alternate) {
const alternateTitle = AlternateTransactionTitleTable[type]
if (alternateTitle !== undefined) return alternateTitle[status]
}
return TransactionTitleTable[type][status]
}

View File

@@ -0,0 +1,136 @@
import { Trans } from '@lingui/macro'
import { Trace, TraceEvent } from '@uniswap/analytics'
import { BrowserEvent, InterfaceElementName, InterfaceSectionName, SharedEventName } from '@uniswap/analytics-events'
import Column from 'components/Column'
import { AutoRow } from 'components/Row'
import { useMiniPortfolioEnabled } from 'featureFlags/flags/miniPortfolio'
import { useIsNftPage } from 'hooks/useIsNftPage'
import { useAtomValue } from 'jotai/utils'
import { useState } from 'react'
import { shouldDisableNFTRoutesAtom } from 'state/application/atoms'
import styled from 'styled-components/macro'
import { ThemedText } from 'theme'
import { ActivityTab } from './Activity'
import NFTs from './NFTs'
import Pools from './Pools'
import { PortfolioRowWrapper } from './PortfolioRow'
import Tokens from './Tokens'
const Wrapper = styled(Column)`
margin-top: 28px;
display: flex;
flex-direction: column;
height: 100%;
gap: 12px;
${PortfolioRowWrapper} {
&:hover {
background: ${({ theme }) => theme.hoverDefault};
}
}
`
const Nav = styled(AutoRow)`
gap: 20px;
`
const NavItem = styled(ThemedText.SubHeader)<{ active?: boolean }>`
color: ${({ theme, active }) => (active ? theme.textPrimary : theme.textTertiary)};
transition: ${({ theme }) => `${theme.transition.duration.medium} ${theme.transition.timing.ease} color`};
&:hover {
${({ theme, active }) => !active && `color: ${theme.textSecondary}`};
cursor: pointer;
}
`
const PageWrapper = styled.div`
border-radius: 12px;
margin-right: -16px;
margin-left: -16px;
width: calc(100% + 32px);
flex: 1;
`
interface Page {
title: React.ReactNode
key: string
component: ({ account }: { account: string }) => JSX.Element
loggingElementName: string
}
const Pages: Array<Page> = [
{
title: <Trans>Tokens</Trans>,
key: 'tokens',
component: Tokens,
loggingElementName: InterfaceElementName.MINI_PORTFOLIO_TOKENS_TAB,
},
{
title: <Trans>NFTs</Trans>,
key: 'nfts',
component: NFTs,
loggingElementName: InterfaceElementName.MINI_PORTFOLIO_NFT_TAB,
},
{
title: <Trans>Pools</Trans>,
key: 'pools',
component: Pools,
loggingElementName: InterfaceElementName.MINI_PORTFOLIO_POOLS_TAB,
},
{
title: <Trans>Activity</Trans>,
key: 'activity',
component: ActivityTab,
loggingElementName: InterfaceElementName.MINI_PORTFOLIO_ACTIVITY_TAB,
},
]
function MiniPortfolio({ account }: { account: string }) {
const isNftPage = useIsNftPage()
const [currentPage, setCurrentPage] = useState(isNftPage ? 1 : 0)
const shouldDisableNFTRoutes = useAtomValue(shouldDisableNFTRoutesAtom)
const Page = Pages[currentPage].component
return (
<Wrapper>
<Nav>
{Pages.map(({ title, loggingElementName, key }, index) => {
if (shouldDisableNFTRoutes && loggingElementName.includes('nft')) return null
return (
<TraceEvent
events={[BrowserEvent.onClick]}
name={SharedEventName.NAVBAR_CLICKED}
element={loggingElementName}
key={index}
>
<NavItem
data-testid={`mini-portfolio-nav-${key}`}
onClick={() => setCurrentPage(index)}
active={currentPage === index}
key={`Mini Portfolio page ${index}`}
>
{title}
</NavItem>
</TraceEvent>
)
})}
</Nav>
<PageWrapper>
<Page account={account} />
</PageWrapper>
</Wrapper>
)
}
export default function MiniPortfolioWrapper({ account }: { account: string }) {
const flagEnabled = useMiniPortfolioEnabled()
if (!flagEnabled) return null
return (
<Trace section={InterfaceSectionName.MINI_PORTFOLIO}>
<MiniPortfolio account={account} />
</Trace>
)
}

View File

@@ -0,0 +1,68 @@
import { useWeb3React } from '@web3-react/core'
import { usePortfolioBalancesLazyQuery } from 'graphql/data/__generated__/types-and-hooks'
import usePrevious from 'hooks/usePrevious'
import { PropsWithChildren, useCallback, useEffect, useMemo, useState } from 'react'
import { useAllTransactions } from 'state/transactions/hooks'
import { TransactionDetails } from 'state/transactions/types'
import { useAccountDrawer } from '.'
const isTxPending = (tx: TransactionDetails) => !tx.receipt
function wasPending(previousTxs: { [hash: string]: TransactionDetails | undefined }, current: TransactionDetails) {
const previousTx = previousTxs[current.hash]
return previousTx && isTxPending(previousTx)
}
function useHasUpdatedTx() {
// TODO: consider monitoring tx's on chains other than the wallet's current chain
const currentChainTxs = useAllTransactions()
const pendingTxs = useMemo(() => {
return Object.entries(currentChainTxs).reduce((acc: { [hash: string]: TransactionDetails }, [hash, tx]) => {
if (!tx.receipt) acc[hash] = tx
return acc
}, {})
}, [currentChainTxs])
const previousPendingTxs = usePrevious(pendingTxs)
return useMemo(() => {
if (!previousPendingTxs) return false
return Object.values(currentChainTxs).some(
(tx) => !isTxPending(tx) && wasPending(previousPendingTxs, tx),
[currentChainTxs, previousPendingTxs]
)
}, [currentChainTxs, previousPendingTxs])
}
/* Prefetches & caches portfolio balances when the wrapped component is hovered or the user completes a transaction */
export default function PrefetchBalancesWrapper({ children }: PropsWithChildren) {
const { account } = useWeb3React()
const [prefetchPortfolioBalances] = usePortfolioBalancesLazyQuery()
const [drawerOpen] = useAccountDrawer()
const [hasUnfetchedBalances, setHasUnfetchedBalances] = useState(true)
const fetchBalances = useCallback(() => {
if (account) {
prefetchPortfolioBalances({ variables: { ownerAddress: account } })
setHasUnfetchedBalances(false)
}
}, [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
// 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])
const onHover = useCallback(() => {
if (hasUnfetchedBalances) fetchBalances()
}, [fetchBalances, hasUnfetchedBalances])
return <div onMouseEnter={onHover}>{children}</div>
}

View File

@@ -4,37 +4,25 @@ import { useActiveLocale } from 'hooks/useActiveLocale'
import { useLocationLinkProps } from 'hooks/useLocationLinkProps'
import { Check } from 'react-feather'
import { Link } from 'react-router-dom'
import { Text } from 'rebass'
import styled, { useTheme } from 'styled-components/macro'
import { ClickableStyle, ThemedText } from 'theme'
import ThemeToggle from 'theme/components/ThemeToggle'
import { GitVersionRow } from './GitVersionRow'
import { SlideOutMenu } from './SlideOutMenu'
import { SmallBalanceToggle } from './SmallBalanceToggle'
const InternalMenuItem = styled(Link)`
const InternalLinkMenuItem = styled(Link)`
${ClickableStyle}
flex: 1;
padding: 0.5rem 0.5rem;
color: ${({ theme }) => theme.textTertiary};
:hover {
cursor: pointer;
}
`
const InternalLinkMenuItem = styled(InternalMenuItem)`
display: flex;
flex-direction: row;
align-items: center;
padding: 12px 16px;
padding: 12px 0;
justify-content: space-between;
text-decoration: none;
color: ${({ theme }) => theme.textPrimary};
:hover {
cursor: pointer;
background-color: ${({ theme }) => theme.backgroundModule};
transition: ${({
theme: {
transition: { duration, timing },
},
}) => `${duration.fast} background-color ${timing.in}`};
}
`
function LanguageMenuItem({ locale, isActive }: { locale: SupportedLocale; isActive: boolean }) {
@@ -45,24 +33,48 @@ function LanguageMenuItem({ locale, isActive }: { locale: SupportedLocale; isAct
return (
<InternalLinkMenuItem onClick={onClick} to={to}>
<Text data-testid="wallet-language-item" fontSize={16} fontWeight={400} lineHeight="24px">
{LOCALE_LABEL[locale]}
</Text>
<ThemedText.BodySmall data-testid="wallet-language-item">{LOCALE_LABEL[locale]}</ThemedText.BodySmall>
{isActive && <Check color={theme.accentActive} opacity={1} size={20} />}
</InternalLinkMenuItem>
)
}
const LanguageMenu = ({ onClose }: { onClose: () => void }) => {
const SectionTitle = styled(ThemedText.SubHeader)`
color: ${({ theme }) => theme.textSecondary};
padding-bottom: 24px;
`
const ThemeToggleContainer = styled.div`
margin: 0 0 6px;
`
const BalanceToggleContainer = styled.div`
padding: 16px 0;
margin-bottom: 26px;
`
export default function SettingsMenu({ onClose }: { onClose: () => void }) {
const activeLocale = useActiveLocale()
return (
<SlideOutMenu title={<Trans>Language</Trans>} onClose={onClose}>
<SlideOutMenu title={<Trans>Settings</Trans>} onClose={onClose}>
<SectionTitle>
<Trans>Preferences</Trans>
</SectionTitle>
<ThemeToggleContainer>
<ThemeToggle />
</ThemeToggleContainer>
<BalanceToggleContainer>
<SmallBalanceToggle />
</BalanceToggleContainer>
<SectionTitle data-testid="wallet-header">
<Trans>Language</Trans>
</SectionTitle>
{SUPPORTED_LOCALES.map((locale) => (
<LanguageMenuItem locale={locale} isActive={activeLocale === locale} key={locale} />
))}
<GitVersionRow />
</SlideOutMenu>
)
}
export default LanguageMenu

View File

@@ -0,0 +1,58 @@
import { ScrollBarStyles } from 'components/Common'
import { ArrowLeft } from 'react-feather'
import styled from 'styled-components/macro'
import { ClickableStyle, ThemedText } from 'theme'
const Menu = styled.div`
width: 100%;
overflow: auto;
margin-top: 4px;
padding: 14px 16px 16px;
${ScrollBarStyles}
::-webkit-scrollbar-track {
margin-top: 40px;
}
`
const Title = styled.span`
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
`
const StyledArrow = styled(ArrowLeft)`
${ClickableStyle}
`
const Header = styled.div`
color: ${({ theme }) => theme.textPrimary};
display: flex;
justify-content: space-between;
position: relative;
width: 100%;
margin-bottom: 20px;
`
export const SlideOutMenu = ({
children,
onClose,
title,
}: {
onClose: () => void
title: React.ReactNode
children: React.ReactNode
onClear?: () => void
}) => (
<Menu>
<Header>
<StyledArrow data-testid="wallet-back" onClick={onClose} size={24} />
<Title>
<ThemedText.SubHeader fontWeight={500}>{title}</ThemedText.SubHeader>
</Title>
</Header>
{children}
</Menu>
)

View File

@@ -0,0 +1,30 @@
import { Trans } from '@lingui/macro'
import Row from 'components/Row'
import Toggle from 'components/Toggle'
import { useAtom } from 'jotai'
import { atomWithStorage } from 'jotai/utils'
import { ThemedText } from 'theme'
export const hideSmallBalancesAtom = atomWithStorage<boolean>('hideSmallBalances', true)
export function SmallBalanceToggle() {
const [hideSmallBalances, updateHideSmallBalances] = useAtom(hideSmallBalancesAtom)
return (
<Row align="center">
<Row width="50%">
<ThemedText.SubHeaderSmall color="primary">
<Trans>Hide small balances</Trans>
</ThemedText.SubHeaderSmall>
</Row>
<Row width="50%" justify="flex-end">
<Toggle
isActive={hideSmallBalances}
toggle={() => {
updateHideSmallBalances(!hideSmallBalances)
}}
/>
</Row>
</Row>
)
}

View File

@@ -0,0 +1,130 @@
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 { 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 styled, { useTheme } from 'styled-components/macro'
import { CloseIcon, ThemedText } from 'theme'
import uniPng from '../../assets/images/uniwallet.svg'
import { DownloadButton } from './DownloadButton'
const UniwalletConnectWrapper = styled(RowBetween)`
display: flex;
flex-direction: column;
padding: 20px 16px 16px;
`
const HeaderRow = styled(RowBetween)`
display: flex;
`
const QRCodeWrapper = styled(RowBetween)`
aspect-ratio: 1;
border-radius: 12px;
background-color: ${({ theme }) => theme.white};
margin: 24px 32px 20px;
padding: 10px;
`
const Divider = styled.div`
border-bottom: 1px solid ${({ theme }) => theme.backgroundOutline};
width: 100%;
`
export default function UniwalletModal() {
const open = useModalIsOpen(ApplicationModal.UNIWALLET_CONNECT)
const toggle = useToggleUniwalletModal()
const [uri, setUri] = useState<string>()
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])
const theme = useTheme()
return (
<Modal isOpen={open} onDismiss={onClose}>
<UniwalletConnectWrapper>
<HeaderRow>
<ThemedText.SubHeader>
<Trans>Scan with Uniswap Wallet</Trans>
</ThemedText.SubHeader>
<CloseIcon onClick={onClose} />
</HeaderRow>
<QRCodeWrapper>
{uri && (
<QRCodeSVG
value={uri}
width="100%"
height="100%"
level="M"
fgColor={theme.darkMode ? theme.backgroundSurface : theme.black}
imageSettings={{
src: uniPng,
height: 27,
width: 27,
excavate: false,
}}
/>
)}
</QRCodeWrapper>
<Divider />
<InfoSection />
</UniwalletConnectWrapper>
</Modal>
)
}
const InfoSectionWrapper = styled(RowBetween)`
display: flex;
flex-direction: row;
padding-top: 20px;
gap: 20px;
`
function InfoSection() {
return (
<InfoSectionWrapper>
<AutoColumn gap="4px">
<ThemedText.SubHeaderSmall color="textPrimary">
<Trans>Don&apos;t have Uniswap Wallet?</Trans>
</ThemedText.SubHeaderSmall>
<ThemedText.Caption color="textSecondary">
<Trans>
Download in the App Store to safely store your tokens and NFTs, swap tokens, and connect to crypto apps.
</Trans>
</ThemedText.Caption>
</AutoColumn>
<Column>
<DownloadButton element={InterfaceElementName.UNISWAP_WALLET_MODAL_DOWNLOAD_BUTTON} />
</Column>
</InfoSectionWrapper>
)
}

View File

@@ -0,0 +1,219 @@
import { TraceEvent } from '@uniswap/analytics'
import { BrowserEvent, InterfaceEventName } from '@uniswap/analytics-events'
import { ScrollBarStyles } from 'components/Common'
import { useWindowSize } from 'hooks/useWindowSize'
import { atom } from 'jotai'
import { useAtomValue, useUpdateAtom } from 'jotai/utils'
import { useCallback, useEffect, useRef } from 'react'
import { ChevronsRight } from 'react-feather'
import styled from 'styled-components/macro'
import { BREAKPOINTS, ClickableStyle } from 'theme'
import { Z_INDEX } from 'theme/zIndex'
import DefaultMenu from './DefaultMenu'
const DRAWER_WIDTH_XL = '390px'
const DRAWER_WIDTH = '320px'
const DRAWER_MARGIN = '8px'
const DRAWER_OFFSET = '10px'
const DRAWER_TOP_MARGIN_MOBILE_WEB = '72px'
const accountDrawerOpenAtom = atom(false)
export function useToggleAccountDrawer() {
const updateAccountDrawerOpen = useUpdateAtom(accountDrawerOpenAtom)
return useCallback(() => {
updateAccountDrawerOpen((open) => !open)
}, [updateAccountDrawerOpen])
}
export function useAccountDrawer(): [boolean, () => void] {
const accountDrawerOpen = useAtomValue(accountDrawerOpenAtom)
return [accountDrawerOpen, useToggleAccountDrawer()]
}
const ScrimBackground = styled.div<{ open: boolean }>`
z-index: ${Z_INDEX.modalBackdrop};
overflow: hidden;
top: 0;
left: 0;
position: fixed;
width: 100%;
height: 100%;
background-color: ${({ theme }) => theme.backgroundScrim};
opacity: 0;
pointer-events: none;
@media only screen and (max-width: ${({ theme }) => `${theme.breakpoint.sm}px`}) {
opacity: ${({ open }) => (open ? 1 : 0)};
pointer-events: ${({ open }) => (open ? 'auto' : 'none')};
transition: opacity ${({ theme }) => theme.transition.duration.medium} ease-in-out;
}
`
const Scrim = ({ onClick, open }: { onClick: () => void; open: boolean }) => {
const { width } = useWindowSize()
useEffect(() => {
if (width && width < BREAKPOINTS.sm && open) document.body.style.overflow = 'hidden'
return () => {
document.body.style.overflow = 'visible'
}
}, [open, width])
return <ScrimBackground onClick={onClick} open={open} />
}
const AccountDrawerScrollWrapper = styled.div`
overflow: hidden;
&:hover {
overflow-y: auto;
}
${ScrollBarStyles}
scrollbar-gutter: stable;
overscroll-behavior: contain;
border-radius: 12px;
`
const Container = styled.div`
display: flex;
flex-direction: row;
height: calc(100% - 2 * ${DRAWER_MARGIN});
overflow: hidden;
position: fixed;
right: ${DRAWER_MARGIN};
top: ${DRAWER_MARGIN};
z-index: ${Z_INDEX.fixed};
@media only screen and (max-width: ${({ theme }) => `${theme.breakpoint.sm}px`}) {
top: 100%;
left: 0;
right: 0;
width: 100%;
overflow: visible;
}
`
const AccountDrawerWrapper = styled.div<{ open: boolean }>`
margin-right: ${({ open }) => (open ? 0 : '-' + DRAWER_WIDTH)};
height: 100%;
overflow: hidden;
@media only screen and (max-width: ${({ theme }) => `${theme.breakpoint.sm}px`}) {
z-index: ${Z_INDEX.modal};
position: absolute;
margin-right: 0;
top: ${({ open }) => (open ? `calc(-1 * (100% - ${DRAWER_TOP_MARGIN_MOBILE_WEB}))` : 0)};
width: 100%;
border-bottom-right-radius: 0px;
border-bottom-left-radius: 0px;
box-shadow: unset;
transition: top ${({ theme }) => theme.transition.duration.medium};
}
@media screen and (min-width: 1440px) {
margin-right: ${({ open }) => (open ? 0 : `-${DRAWER_WIDTH_XL}`)};
width: ${DRAWER_WIDTH_XL};
}
border-radius: 12px;
width: ${DRAWER_WIDTH};
font-size: 16px;
background-color: ${({ theme }) => theme.backgroundSurface};
border: ${({ theme }) => `1px solid ${theme.backgroundOutline}`};
box-shadow: ${({ theme }) => theme.deepShadow};
transition: margin-right ${({ theme }) => theme.transition.duration.medium};
`
const CloseIcon = styled(ChevronsRight).attrs({ size: 24 })`
stroke: ${({ theme }) => theme.textSecondary};
`
const CloseDrawer = styled.div`
${ClickableStyle}
cursor: pointer;
height: 100%;
// When the drawer is not hovered, the icon should be 18px from the edge of the sidebar.
padding: 24px calc(18px + ${DRAWER_OFFSET}) 24px 14px;
border-radius: 20px 0 0 20px;
transition: ${({ theme }) =>
`${theme.transition.duration.medium} ${theme.transition.timing.ease} background-color, ${theme.transition.duration.medium} ${theme.transition.timing.ease} margin`};
&:hover {
z-index: -1;
margin: 0 -8px 0 0;
background-color: ${({ theme }) => theme.stateOverlayHover};
}
@media only screen and (max-width: ${({ theme }) => `${theme.breakpoint.sm}px`}) {
display: none;
}
`
function AccountDrawer() {
const [walletDrawerOpen, toggleWalletDrawer] = useAccountDrawer()
const scrollRef = useRef<HTMLDivElement>(null)
useEffect(() => {
if (!walletDrawerOpen) {
scrollRef.current?.scrollTo({ top: 0, behavior: 'smooth' })
}
}, [walletDrawerOpen])
// close on escape keypress
useEffect(() => {
const escapeKeyDownHandler = (event: KeyboardEvent) => {
if (event.key === 'Escape' && walletDrawerOpen) {
event.preventDefault()
toggleWalletDrawer()
}
}
document.addEventListener('keydown', escapeKeyDownHandler)
return () => {
document.removeEventListener('keydown', escapeKeyDownHandler)
}
}, [walletDrawerOpen, toggleWalletDrawer])
// close on escape keypress
useEffect(() => {
const escapeKeyDownHandler = (event: KeyboardEvent) => {
if (event.key === 'Escape' && walletDrawerOpen) {
event.preventDefault()
toggleWalletDrawer()
}
}
document.addEventListener('keydown', escapeKeyDownHandler)
return () => {
document.removeEventListener('keydown', escapeKeyDownHandler)
}
}, [walletDrawerOpen, toggleWalletDrawer])
return (
<Container>
{walletDrawerOpen && (
<TraceEvent
events={[BrowserEvent.onClick]}
name={InterfaceEventName.MINI_PORTFOLIO_TOGGLED}
properties={{ type: 'close' }}
>
<CloseDrawer onClick={toggleWalletDrawer}>
<CloseIcon />
</CloseDrawer>
</TraceEvent>
)}
<Scrim onClick={toggleWalletDrawer} open={walletDrawerOpen} />
<AccountDrawerWrapper open={walletDrawerOpen}>
{/* id used for child InfiniteScrolls to reference when it has reached the bottom of the component */}
<AccountDrawerScrollWrapper ref={scrollRef} id="wallet-dropdown-scroll-wrapper">
<DefaultMenu />
</AccountDrawerScrollWrapper>
</AccountDrawerWrapper>
</Container>
)
}
export default AccountDrawer

View File

@@ -5,7 +5,7 @@ import uniswapNftAirdropClaim from 'abis/uniswap-nft-airdrop-claim.json'
import airdropBackgroundv2 from 'assets/images/airdopBackground.png'
import { ButtonEmphasis, ButtonSize, ThemeButton } from 'components/Button'
import { OpacityHoverState } from 'components/Common'
import Loader from 'components/Loader'
import Loader from 'components/Icons/LoadingSpinner'
import { UNISWAP_NFT_AIRDROP_CLAIM_ADDRESS } from 'constants/addresses'
import { useContract } from 'hooks/useContract'
import { ChevronRightIcon } from 'nft/components/icons'

View File

@@ -1,7 +1,6 @@
import { Trans } from '@lingui/macro'
import Badge, { BadgeVariant } from 'components/Badge'
import { AlertCircle } from 'react-feather'
import styled from 'styled-components/macro'
import { AlertTriangle, Slash } from 'react-feather'
import styled, { useTheme } from 'styled-components/macro'
import { MouseoverTooltip } from '../../components/Tooltip'
@@ -13,7 +12,9 @@ const BadgeWrapper = styled.div`
const BadgeText = styled.div`
font-weight: 500;
font-size: 14px;
font-size: 12px;
line-height: 14px;
margin-right: 8px;
`
const ActiveDot = styled.span`
@@ -21,7 +22,14 @@ const ActiveDot = styled.span`
border-radius: 50%;
height: 8px;
width: 8px;
margin-right: 4px;
`
const LabelText = styled.div<{ color: string }>`
align-items: center;
color: ${({ color }) => color};
display: flex;
flex-direction: row;
justify-content: flex-end;
`
export default function RangeBadge({
@@ -31,17 +39,17 @@ export default function RangeBadge({
removed: boolean | undefined
inRange: boolean | undefined
}) {
const theme = useTheme()
return (
<BadgeWrapper>
{removed ? (
<MouseoverTooltip text={<Trans>Your position has 0 liquidity, and is not earning fees.</Trans>}>
<Badge variant={BadgeVariant.DEFAULT}>
<AlertCircle width={14} height={14} />
&nbsp;
<LabelText color={theme.textSecondary}>
<BadgeText>
<Trans>Closed</Trans>
</BadgeText>
</Badge>
<Slash width={12} height={12} />
</LabelText>
</MouseoverTooltip>
) : inRange ? (
<MouseoverTooltip
@@ -51,12 +59,12 @@ export default function RangeBadge({
</Trans>
}
>
<Badge variant={BadgeVariant.DEFAULT}>
<ActiveDot /> &nbsp;
<LabelText color={theme.accentSuccess}>
<BadgeText>
<Trans>In range</Trans>
<Trans>In Range</Trans>
</BadgeText>
</Badge>
<ActiveDot />
</LabelText>
</MouseoverTooltip>
) : (
<MouseoverTooltip
@@ -66,13 +74,12 @@ export default function RangeBadge({
</Trans>
}
>
<Badge variant={BadgeVariant.WARNING}>
<AlertCircle width={14} height={14} />
&nbsp;
<LabelText color={theme.accentWarning}>
<BadgeText>
<Trans>Out of range</Trans>
</BadgeText>
</Badge>
<AlertTriangle width={12} height={12} />
</LabelText>
</MouseoverTooltip>
)}
</BadgeWrapper>

View File

@@ -8,6 +8,9 @@ export enum BadgeVariant {
POSITIVE = 'POSITIVE',
PRIMARY = 'PRIMARY',
WARNING = 'WARNING',
PROMOTIONAL = 'PROMOTIONAL',
BRANDED = 'BRANDED',
SOFT = 'SOFT',
WARNING_OUTLINE = 'WARNING_OUTLINE',
}
@@ -18,10 +21,16 @@ interface BadgeProps {
function pickBackgroundColor(variant: BadgeVariant | undefined, theme: DefaultTheme): string {
switch (variant) {
case BadgeVariant.BRANDED:
return theme.brandedGradient
case BadgeVariant.PROMOTIONAL:
return theme.promotionalGradient
case BadgeVariant.NEGATIVE:
return theme.accentFailure
return theme.accentCritical
case BadgeVariant.POSITIVE:
return theme.accentSuccess
case BadgeVariant.SOFT:
return theme.accentActionSoft
case BadgeVariant.PRIMARY:
return theme.accentAction
case BadgeVariant.WARNING:
@@ -44,10 +53,14 @@ function pickBorder(variant: BadgeVariant | undefined, theme: DefaultTheme): str
function pickFontColor(variant: BadgeVariant | undefined, theme: DefaultTheme): string {
switch (variant) {
case BadgeVariant.BRANDED:
return theme.darkMode ? theme.accentTextDarkPrimary : theme.white
case BadgeVariant.NEGATIVE:
return readableColor(theme.accentFailure)
case BadgeVariant.POSITIVE:
return readableColor(theme.accentSuccess)
case BadgeVariant.SOFT:
return theme.accentAction
case BadgeVariant.WARNING:
return readableColor(theme.accentWarning)
case BadgeVariant.WARNING_OUTLINE:
@@ -59,7 +72,7 @@ function pickFontColor(variant: BadgeVariant | undefined, theme: DefaultTheme):
const Badge = styled.div<PropsWithChildren<BadgeProps>>`
align-items: center;
background-color: ${({ theme, variant }) => pickBackgroundColor(variant, theme)};
background: ${({ theme, variant }) => pickBackgroundColor(variant, theme)};
border: ${({ theme, variant }) => pickBorder(variant, theme)};
border-radius: 0.5rem;
color: ${({ theme, variant }) => pickFontColor(variant, theme)};
@@ -70,3 +83,8 @@ const Badge = styled.div<PropsWithChildren<BadgeProps>>`
`
export default Badge
export const SmallBadge = styled(Badge)`
border-radius: 5px;
padding: 2px 4px;
`

View File

@@ -0,0 +1,147 @@
import { Trans } from '@lingui/macro'
import { InterfaceElementName } from '@uniswap/analytics-events'
import { openDownloadApp, openWalletMicrosite } from 'components/AccountDrawer/DownloadButton'
import { BaseButton } from 'components/Button'
import { AutoColumn } from 'components/Column'
import { OpacityHoverState } from 'components/Common'
import Row from 'components/Row'
import { useMgtmEnabled } from 'featureFlags/flags/mgtm'
import { useScreenSize } from 'hooks/useScreenSize'
import { X } from 'react-feather'
import { useLocation } from 'react-router-dom'
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 { ReactComponent as AppleLogo } from '../../assets/svg/apple_logo.svg'
import walletBannerPhoneImageSrc from '../../assets/svg/wallet_banner_phone_image.svg'
const PopupContainer = styled.div<{ show: boolean }>`
display: flex;
flex-direction: column;
justify-content: space-between;
${({ show }) => !show && 'display: none'};
background: url(${walletBannerPhoneImageSrc});
background-repeat: no-repeat;
background-position: bottom -1px right 15px;
background-size: 166px;
:hover {
background-size: 170px;
}
transition: background-size ${({ theme }) => theme.transition.duration.medium}
${({ theme }) => theme.transition.timing.inOut};
background-color: ${({ theme }) => theme.promotional};
color: ${({ theme }) => theme.textPrimary};
position: fixed;
z-index: ${Z_INDEX.sticky};
padding: 24px 16px 16px;
border-radius: 20px;
bottom: 20px;
right: 20px;
width: 390px;
height: 164px;
border: 1px solid ${({ theme }) => theme.backgroundOutline};
box-shadow: ${({ theme }) => theme.deepShadow};
@media only screen and (max-width: ${({ theme }) => `${theme.breakpoint.md}px`}) {
bottom: 62px;
}
@media only screen and (max-width: ${({ theme }) => `${theme.breakpoint.sm}px`}) {
width: unset;
right: 10px;
left: 10px;
}
user-select: none;
`
const ButtonRow = styled(Row)`
gap: 16px;
`
const StyledXButton = styled(X)`
cursor: pointer;
position: absolute;
top: 21px;
right: 17px;
color: ${({ theme }) => theme.white};
${OpacityHoverState};
`
const BannerButton = styled(BaseButton)`
height: 40px;
border-radius: 16px;
padding: 10px;
${OpacityHoverState};
`
export default function UniswapWalletBanner() {
const [hideUniswapWalletBanner, toggleHideUniswapWalletBanner] = useHideUniswapWalletBanner()
const mgtmEnabled = useMgtmEnabled()
const location = useLocation()
const isLandingScreen = location.search === '?intro=true' || location.pathname === '/'
const shouldDisplay = Boolean(mgtmEnabled && !hideUniswapWalletBanner && !isLandingScreen)
const screenSize = useScreenSize()
return (
<PopupContainer show={shouldDisplay}>
<StyledXButton
data-testid="uniswap-wallet-banner"
size={20}
onClick={(e) => {
// prevent click from bubbling to UI on the page underneath, i.e. clicking a token row
e.preventDefault()
e.stopPropagation()
toggleHideUniswapWalletBanner()
}}
/>
<AutoColumn gap="8px">
<ThemedText.HeadlineMedium fontSize="24px" lineHeight="28px" color="white" maxWidth="60%">
<Trans>Uniswap in your pocket</Trans>
</ThemedText.HeadlineMedium>
</AutoColumn>
<ButtonRow>
{isIOS ? (
<>
<BannerButton
backgroundColor="white"
onClick={() => openDownloadApp(InterfaceElementName.UNISWAP_WALLET_BANNER_DOWNLOAD_BUTTON)}
>
<AppleLogo width={14} height={14} />
<ThemedText.LabelSmall color="black" marginLeft="5px">
{!screenSize['xs'] ? <Trans>Download</Trans> : <Trans>Download app</Trans>}
</ThemedText.LabelSmall>
</BannerButton>
<BannerButton backgroundColor="black" onClick={openWalletMicrosite}>
<ThemedText.LabelSmall color="white">
<Trans>Learn more</Trans>
</ThemedText.LabelSmall>
</BannerButton>
</>
) : (
<BannerButton backgroundColor="white" width="125px" onClick={openWalletMicrosite}>
<ThemedText.LabelSmall color="black">
<Trans>Learn more</Trans>
</ThemedText.LabelSmall>
</BannerButton>
)}
</ButtonRow>
</PopupContainer>
)
}

View File

@@ -18,11 +18,13 @@ export const ColumnCenter = styled(Column)`
export const AutoColumn = styled.div<{
gap?: Gap | string
justify?: 'stretch' | 'center' | 'start' | 'end' | 'flex-start' | 'flex-end' | 'space-between'
grow?: true
}>`
display: grid;
grid-auto-rows: auto;
grid-row-gap: ${({ gap, theme }) => (gap && theme.grids[gap as Gap]) || gap};
justify-items: ${({ justify }) => justify && justify};
flex-grow: ${({ grow }) => grow && 1};
`
export default Column

View File

@@ -13,10 +13,6 @@ const ContentWrapper = styled(Column)`
text-align: center;
font-size: 12px;
`
const Copy = styled(CopyHelper)`
font-size: 12px;
`
interface ConnectedAccountBlockedProps {
account: string | null | undefined
isOpen: boolean
@@ -44,16 +40,16 @@ export default function ConnectedAccountBlocked(props: ConnectedAccountBlockedPr
<ThemedText.DeprecatedMain fontSize={12}>
<Trans>If you believe this is an error, please send an email including your address to </Trans>{' '}
</ThemedText.DeprecatedMain>
<Copy
<CopyHelper
toCopy="compliance@uniswap.org"
fontSize={14}
iconSize={16}
gap={6}
color={theme.accentAction}
iconPosition="right"
>
compliance@uniswap.org
</Copy>
</CopyHelper>
</ContentWrapper>
</Modal>
)

View File

@@ -5,7 +5,7 @@ import { formatNumber, formatPriceImpact, NumberType } from '@uniswap/conedison/
import { Percent } from '@uniswap/sdk-core'
import { LoadingBubble } from 'components/Tokens/loading'
import { MouseoverTooltip } from 'components/Tooltip'
import { useEffect, useMemo, useState } from 'react'
import { useMemo } from 'react'
import styled, { useTheme } from 'styled-components/macro'
import { ThemedText } from '../../theme'
@@ -20,14 +20,11 @@ const FiatLoadingBubble = styled(LoadingBubble)`
export function FiatValue({
fiatValue,
priceImpact,
isLoading = false,
}: {
fiatValue: number | null | undefined
fiatValue?: { data?: number; isLoading: boolean }
priceImpact?: Percent
isLoading?: boolean
}) {
const theme = useTheme()
const [showLoadingPlaceholder, setShowLoadingPlaceholder] = useState(false)
const priceImpactColor = useMemo(() => {
if (!priceImpact) return undefined
if (priceImpact.lessThan('0')) return theme.accentSuccess
@@ -37,26 +34,13 @@ export function FiatValue({
return theme.accentFailure
}, [priceImpact, theme.accentSuccess, theme.accentFailure, theme.textTertiary, theme.deprecated_yellow1])
useEffect(() => {
const stale = false
let timeoutId = 0
if (isLoading && !fiatValue) {
timeoutId = setTimeout(() => {
if (!stale) setShowLoadingPlaceholder(true)
}, 200) as unknown as number
} else {
setShowLoadingPlaceholder(false)
}
return () => clearTimeout(timeoutId)
}, [isLoading, fiatValue])
return (
<ThemedText.DeprecatedBody fontSize={14} color={theme.textSecondary}>
{showLoadingPlaceholder ? (
{fiatValue?.isLoading ? (
<FiatLoadingBubble />
) : (
<div>
{fiatValue ? formatNumber(fiatValue, NumberType.FiatTokenPrice) : undefined}
{fiatValue?.data ? formatNumber(fiatValue.data, NumberType.FiatTokenPrice) : undefined}
{priceImpact && (
<span style={{ color: priceImpactColor }}>
{' '}

View File

@@ -9,7 +9,7 @@ import { LoadingOpacityContainer, loadingOpacityMixin } from 'components/Loader/
import CurrencyLogo from 'components/Logo/CurrencyLogo'
import { isSupportedChain } from 'constants/chains'
import { darken } from 'polished'
import { ReactNode, useCallback, useEffect, useState } from 'react'
import { ReactNode, useCallback, useState } from 'react'
import { Lock } from 'react-feather'
import styled, { useTheme } from 'styled-components/macro'
import { flexColumnNoWrap, flexRowNoWrap } from 'theme/styles'
@@ -195,7 +195,7 @@ interface SwapCurrencyInputPanelProps {
pair?: Pair | null
hideInput?: boolean
otherCurrency?: Currency | null
fiatValue?: number | null
fiatValue: { data?: number; isLoading: boolean }
priceImpact?: Percent
id: string
showCommonBases?: boolean
@@ -229,7 +229,6 @@ export default function SwapCurrencyInputPanel({
...rest
}: SwapCurrencyInputPanelProps) {
const [modalOpen, setModalOpen] = useState(false)
const [fiatValueIsLoading, setFiatValueIsLoading] = useState(false)
const { account, chainId } = useWeb3React()
const selectedCurrencyBalance = useCurrencyBalance(account ?? undefined, currency ?? undefined)
const theme = useTheme()
@@ -240,10 +239,6 @@ export default function SwapCurrencyInputPanel({
const chainAllowed = isSupportedChain(chainId)
useEffect(() => {
!!value && !fiatValue ? setFiatValueIsLoading(true) : setFiatValueIsLoading(false)
}, [fiatValueIsLoading, value, fiatValue])
return (
<InputPanel id={id} hideInput={hideInput} {...rest}>
{locked && (
@@ -311,7 +306,7 @@ export default function SwapCurrencyInputPanel({
<FiatRow>
<RowBetween>
<LoadingOpacityContainer $loading={loading}>
<FiatValue fiatValue={fiatValue} priceImpact={priceImpact} isLoading={fiatValueIsLoading} />
<FiatValue fiatValue={fiatValue} priceImpact={priceImpact} />
</LoadingOpacityContainer>
{account ? (
<RowFixed style={{ height: '17px' }}>

View File

@@ -182,7 +182,7 @@ interface CurrencyInputPanelProps {
pair?: Pair | null
hideInput?: boolean
otherCurrency?: Currency | null
fiatValue?: number | null
fiatValue?: { data?: number; isLoading: boolean }
priceImpact?: Percent
id: string
showCommonBases?: boolean

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