Compare commits

...

128 Commits

Author SHA1 Message Date
eddie
9dc525bc56 fix: fail UR transaction if chain IDs don't match (#7151) 2023-08-11 15:28:44 -07:00
Jordan Frankfurt
09054530c8 fix: add back correct fellback setter (#7116) 2023-08-08 13:01:34 -05:00
eddie
1990d42ebd fix: activity nonce xchain prod (#7099)
* fix: check chainId in findCancelTx

* feat: comment
2023-08-07 14:55:30 -07:00
cartcrom
d8977fce36 fix: uniswapx review modal success display (#7096)
* fix: update swapConfirmed logic

* fix: success icon display
2023-08-07 17:54:41 -04:00
eddie
b766385722 fix: revert #6928 (#7089) 2023-08-04 15:07:12 -07:00
UL Service Account
ac4ee875f9 ci: add global CODEOWNERS 2023-08-04 20:31:45 +00:00
UL Service Account
f417dbebc0 ci(t9n): download translations from crowdin 2023-08-04 20:31:45 +00:00
eddie
fa4e75b777 feat: base network (#6997)
* feat: initial support for base goerli

* wip: update dependencies

* chore: yarn deduplicate

* feat: mainnet wip support

* feat: mainnet wip support

* fix: radial gradient

* fix: logo update

* fix: ur address

* fix: add weth to common bases

* fix: updates

* fix: yarn dedup

* fix: correct rpc url

* fix: correct explorer url

* feat: add USDbC to common bases

* fix lint warnings

* bump SOR version

* fix: fallback URLs

* feat: statsig flag for base support (#7079)

* feat: put base support behind statsig flag

* fix: null checks

* fix: hide pool page

* fix: baseEnabledChains

* feat: update sor

---------

Co-authored-by: Jordan Frankfurt <jordan@uniswap.org>
2023-08-04 11:43:11 -07:00
Brendan Wong
f845695f6e feat: caches GraphQL queries for Cloudflare workers (#6929)
* feat: add token and nft injection

* feat: basic tests

* fix: get jest configured properly

* fix: change timeout

* fix: uninstall port ready

* fix: readd port ready

* fix: local tests work

* Update yarn.lock

* add lint disable for setup files

* fix: update dependencies

* fix: basic test suite for nfts/tokens

* feat: collection data

* fix: make tests more comprehensive

* fix: change matches to contains

* fix: tests for twitter alt image tag

* fix: image gen

* fix: add patch-package

* fix: update yarn install

* feat: basic image gen for nfts and collections

* fix: remove vibrant attempt

* use watermark asset

* dynamically grab color

* modularize code and prototype for token preview

* refactor code

* finalize css

* fix color grabber

* update tests

* fix up css

* refactor code a bit more

* remove console logs

* tests

* update tests

* update images based on design feedback

* network logos

* update lint

* slight refactoring

* more refactoring

* fix packages

* Update yarn.lock

* remove dynamically generated image stuff

* cleanup return values

* Create README.md

* Revert "Create README.md"

This reverts commit 7a91c98d38.

* First round of feedback

* comments

* feat: cache

* Update test.yml

* Update test.yml

* Update test.yml

* feedback round 2

* final feedback

* final final feedback

* add coverage and other options

* Update test.yml

* start typecheck

* update cache

* update snapshots?

* Update jest.config.json

* Update jest.config.json

* give timeout some buffer

* update import

* upgrade ts

* fix typing for apollo deps

* finalize typechecks

* downgrade typescript to original version

* add cache directory to jest

* remove coverage

* remove google analytics from tests

* review changes

* try cache setup

* Update cache.test.ts

* make cache helper function

* cache test

* remove unneeded test causing issues

* feat: parallelize cache (#6930)

* feat: parallelize cache?

* remove graph query from concurrency await

* most of feedback

* move tests

* update token tests

* singleton cache

* restructuring res and cache promise

* abstract away repeated graph logic

* final feedback

* Update yarn.lock

* final final feedback

* final final final feedback!

* final final final final feedback?
2023-08-04 14:12:20 -04:00
eddie
dbb2f7f6a2 fix: re-enable UniswapX after disabling (#7080)
fix: allow gouda after user disables
2023-08-03 16:45:04 -07:00
eddie
264f145708 fix: Router label copy (#7060) 2023-08-03 15:08:59 -07:00
eddie
bb2677ab1b fix: match PendingModalContent number formatting w/ SwapModalHeader (#7053)
* fix: match PendingModalContent number formatting w/ SwapModalHeader

* fix: typo

* fix: remove argument
2023-08-03 12:50:46 -07:00
eddie
29e46455c1 fix: use submitted icon on mainnet (#7055)
* fix: use submitted icon on mainnet

* fix: e2e test

* fix: some cleanup
2023-08-03 12:15:25 -07:00
Jack Short
cfc9748036 fix: adjusting slippage in a different language (#7073)
* fix: adjusting slippage in a different language

* making overflow hidden on close

* passing width instead of visibility

* min width
2023-08-03 15:05:47 -04:00
eddie
715555f340 feat: time-to-swap metric (#7051)
* test(cypress): disable infura from browser

* build: typecheck cypress

* build: es5

* build: rm cypress videos

* fix failing tests

* skip nft failure and rm infra-175

* feat: implement tts metric

* wip e2e test

* fix: improve v2 network support (#7012)

* fix: improve v2 network support

* add an unsupported message to all v2 pages

* test: add v2 pool tests

* add guard on transaction callbacks

* fix: dep array

---------

Co-authored-by: eddie <66155195+just-toby@users.noreply.github.com>

* test: adjust test options

* fix: move to helper method

* fix: merge and make code style change

* fix: use local variable to track first event

* fix: amplitude cypress command

* fix: use file-level var

* fix: clear input in test

---------

Co-authored-by: Zach Pomerantz <zzmp@uniswap.org>
Co-authored-by: Jordan Frankfurt <jordanwfrankfurt@gmail.com>
2023-08-03 11:33:16 -07:00
cartcrom
6a1f17ab5a feat: update cancelled local tx's (#7008)
* feat: update cancelled local tx's

* fix: use updated queries

* test: add cancel reducer and hook tests

* fix: spelling in test descriptor

* feat: improved activity descriptors

* fix: check activity groups instead of activity

* fix: pending hooks cleanup

* fix: destruct object from pending hook

* refactor: update usage of pending util

* fix: removed unused util
2023-08-03 12:35:51 -04:00
Brendan Wong
cd43e0beaa fix: failing token cloud function tests (#7074)
* fix: failing token cloud function tests

* utilize enum from chains
2023-08-03 12:30:50 -04:00
gnewfield
24d00f7c39 fix: init pool behavior (#7052)
* Disable increment/decrement  when range prices undefined

* Enable full-range price selection for new pools

* Add cypress tests

* Fix lint error
2023-08-03 10:52:18 -04:00
Brendan Wong
b8573930b9 fix: update v2 pool information link (#7058)
update link
2023-08-02 19:07:08 -04:00
Brendan Wong
2c1e608f84 fix: closes settings state when MP is closed (#7065)
* fix: close settings menu on drawer close

* migrate change to default menu

* remove indent

* add cleanup
2023-08-02 19:06:57 -04:00
Brendan Wong
f90066263e fix: bring back safeNamehash (#7050)
* fix: bring back safeNamehash

* Update safeNamehash.test.ts
2023-08-02 15:15:10 -07:00
Nate Wienert
1daf15df9f chore: align @uniswap/analytics dep versions with wallet (#7037)
* chore: align @uniswap/analytics dep versions with wallet

* fix: use deeplink instead of universal link (#7017)

* fix: use deeplink instead of universal link

* docs: added comment

* test: mock quotes to avoid errors logged (#7031)

* fix: change style of mobile pool buttons and menu (#7020)

* fix: menu flyout alignment overridden on mobile

* fix: change button order, sizing

* Replace deprecated media queries and text components

* fix: remove flakey token test (#7029)

* fix: reverted swap state (#7044)

* fix: wait for transaction status in ConfirmSwapModal

* fix: use actual swap status in ConfirmSwapModal

* feat: add test

* fix: shared hook

* fix: fix test

* fix: dont create Activity instance

* fix: clearing input on connect wallet (#6928)

* fix: clearing input on connect wallet

* update e2e tests

* fix: text wrap and spacing on mobile TDP (#6909)

fix text wrap and spacing on mobile TDP

* fix: top token charts on mobile (#6967)

* fix: top token charts on mobile

* Update TokenRow.test.tsx.snap

* Update src/components/Tokens/TokenTable/TokenRow.tsx

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

* Update src/components/Tokens/TokenTable/__snapshots__/TokenRow.test.tsx.snap

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

* update

* Update TokenRow.test.tsx.snap

* Update TokenRow.test.tsx.snap

---------

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

* test: deflake and clean universal search (#7034)

* fix: improve v2 network support (#7012)

* fix: improve v2 network support

* add an unsupported message to all v2 pages

* test: add v2 pool tests

* add guard on transaction callbacks

* fix: dep array

---------

Co-authored-by: eddie <66155195+just-toby@users.noreply.github.com>

* fix: failing nft buy test (#7049)

---------

Co-authored-by: cartcrom <39385577+cartcrom@users.noreply.github.com>
Co-authored-by: gnewfield <18626088+gnewfield@users.noreply.github.com>
Co-authored-by: Brendan Wong <35351983+LunrEclipse@users.noreply.github.com>
Co-authored-by: eddie <66155195+just-toby@users.noreply.github.com>
Co-authored-by: Zach Pomerantz <zzmp@uniswap.org>
Co-authored-by: Jordan Frankfurt <jordanwfrankfurt@gmail.com>
Co-authored-by: Jack Short <john.short.tj@gmail.com>
2023-08-02 15:00:59 -07:00
Brendan Wong
708a813f2a feat: MP closes on scroll down (#6682)
* feat: MP closes on scroll down

* fix: make dismissal smoother

* fix: implement react gestures and fix scroll

* fix: fix drag when not starting from top

* fix: double scroll on mobile

* fix: comments for clarity

* fix: mobile scrolling?

* remove console logs

* potential fix?

* remove change again

* maybe fix?

* cope

* even more cope

* even more more cope

* copiest

* make less buggy

* nope

* maybe?

* HOLD

* maybe

* try 2

* maybe

* hopefully

* test

* another try

* cope

* redo

* attempt 2

* hmm

* maybe

* I THINK
2023-08-02 17:15:23 -04:00
gnewfield
18b50283d3 fix: clip long token names (#7066)
* Clip token name with ellipsis style

* Use built-in ellipsis style
2023-08-02 17:02:40 -04:00
Brendan Wong
d9ff7b15a1 fix: swap flashing the wrong value after the input is cleared. (#6719)
* fix: set input panel to 0 when recalculating

* fix: only reset number when input cleared

* fix: remove useEffect and more graciously handle trade mutation
2023-08-02 16:04:50 -04:00
Ankit Boghra
cc5309b18f fix: broken link for polygon mumbai bridge (#6986)
fix: broken link for polygon bridge

Co-authored-by: Charlie B <charles@bachmeier.io>
2023-08-02 12:20:21 -07:00
eddie
a060bf1079 fix: remove unused variable (#7061) 2023-08-02 12:06:04 -07:00
Jordan Frankfurt
1ffb9421b2 fix: style and copy updates to the swap box (#7006)
* add you pay/you receive label to swap inputs

* update styles

* update snapshots
2023-08-02 13:39:05 -05:00
Zach Pomerantz
7978ed97a9 test(cypress): clean up types/assumptions/infura (#7046) 2023-08-01 22:13:19 -07:00
Thomas Thachil
b4b6c347e3 feat: add mobile deeplinks json to public folder (#7059) 2023-08-01 14:43:48 -04:00
Jack Short
83cb750b2f fix: failing nft buy test (#7049) 2023-07-31 14:06:45 -04:00
Jordan Frankfurt
fc0bf229a7 fix: improve v2 network support (#7012)
* fix: improve v2 network support

* add an unsupported message to all v2 pages

* test: add v2 pool tests

* add guard on transaction callbacks

* fix: dep array

---------

Co-authored-by: eddie <66155195+just-toby@users.noreply.github.com>
2023-07-31 10:04:22 -05:00
Zach Pomerantz
b55680dbce test: deflake and clean universal search (#7034) 2023-07-28 16:36:13 -07:00
Brendan Wong
136e68201f fix: top token charts on mobile (#6967)
* fix: top token charts on mobile

* Update TokenRow.test.tsx.snap

* Update src/components/Tokens/TokenTable/TokenRow.tsx

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

* Update src/components/Tokens/TokenTable/__snapshots__/TokenRow.test.tsx.snap

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

* update

* Update TokenRow.test.tsx.snap

* Update TokenRow.test.tsx.snap

---------

Co-authored-by: Zach Pomerantz <zzmp@uniswap.org>
2023-07-28 17:50:02 -04:00
Brendan Wong
b4bb3d72d5 fix: text wrap and spacing on mobile TDP (#6909)
fix text wrap and spacing on mobile TDP
2023-07-28 17:24:35 -04:00
Brendan Wong
3f812f4aee fix: clearing input on connect wallet (#6928)
* fix: clearing input on connect wallet

* update e2e tests
2023-07-28 16:45:58 -04:00
eddie
02883aca13 fix: reverted swap state (#7044)
* fix: wait for transaction status in ConfirmSwapModal

* fix: use actual swap status in ConfirmSwapModal

* feat: add test

* fix: shared hook

* fix: fix test

* fix: dont create Activity instance
2023-07-28 12:48:12 -07:00
Brendan Wong
ace81ecc84 fix: remove flakey token test (#7029) 2023-07-28 13:31:19 -04:00
gnewfield
0aafcdf885 fix: change style of mobile pool buttons and menu (#7020)
* fix: menu flyout alignment overridden on mobile

* fix: change button order, sizing

* Replace deprecated media queries and text components
2023-07-28 13:22:48 -04:00
cartcrom
f55062f9a9 test: mock quotes to avoid errors logged (#7031) 2023-07-28 11:39:49 -04:00
cartcrom
1fde2504b4 fix: use deeplink instead of universal link (#7017)
* fix: use deeplink instead of universal link

* docs: added comment
2023-07-28 10:58:01 -04:00
Zach Pomerantz
6a3abbfb56 fix: update mainnet block (#7042)
* fix: update mainnet block

* fix: typecheck
2023-07-27 15:40:10 -07:00
gnewfield
9ced714718 fix: improve currency select styling (#7009)
* fix: remove currency select drop shadows and make deposit label non-interactive

* rename labelOnly prop to pointerEvents
2023-07-27 14:27:59 -04:00
Brendan Wong
8ed6481f16 fix: display ens name/avatar on other chains (#6981)
* maintain ens info on other chains

* update ens hook

* feedback!

* initial test

* test progress

* e2e test?

* final feedback

* fix(lint): rm unused test var

* fix final bugs

* last lint issue

* fix: update catches

* fix: define catch handlers

* fix: use error in catch

* empty commit

---------

Co-authored-by: Zach Pomerantz <zzmp@uniswap.org>
2023-07-27 13:34:26 -04:00
cartcrom
d160d1a929 feat: don't fetch UniswapX quotes if user has disabled UniswapX (#7028) 2023-07-27 13:33:20 -04:00
Brendan Wong
1092bc2c58 fix: collision of network banner and swap settings (#6910)
* change z-index of network banner

* change to use pointer-events rather than deprecated value
2023-07-27 13:22:03 -04:00
Zach Pomerantz
a2e56aaabd revert: "fix: update token tests for cloudflare functions" (#7027)
Revert "fix: update token tests for cloudflare functions (#7026)"

This reverts commit b58d4b72ab.
2023-07-27 09:49:18 -07:00
Tina
7ec6a965d3 feat: bump uniswapx-sdk (#7023)
* bump uniswapx-sdk

* add feature flag

* use undefined if feature flag is off

* move to top level config

* remove unused variable
2023-07-27 12:16:17 -04:00
Brendan Wong
b58d4b72ab fix: update token tests for cloudflare functions (#7026)
fix: update tests
2023-07-26 17:58:56 -04:00
Zach Pomerantz
a2d98607ea fix: use redirect for landing (#6993)
* fix: use redirect for landing

* chore: rm console.log
2023-07-26 13:24:33 -07:00
eddie
27d9152949 fix: add amplitude event for uniswapX orders failing to post (#6977) 2023-07-26 12:10:22 -07:00
Zach Pomerantz
3cb069283c build: fix promotion sha (#7022) 2023-07-26 11:51:49 -07:00
Brendan Wong
4d084d0055 fix: mock graphql pool queries in e2e tests (#7018)
mock graphql query
2023-07-26 13:34:17 -04:00
Jordan Frankfurt
4b39dfbd69 fix: update safety check cache time (#7005) 2023-07-26 10:14:54 -05:00
eddie
6c5c1c0032 fix: remove duplicate hook for permit2 approvals (#6999)
* fix: remove duplicate hook for permit2 approvals

* fix: address comments
2023-07-25 14:08:25 -07:00
Brendan Wong
22112c763c feat: automated testing for cloud functions (#6931)
* feat: add token and nft injection

* feat: basic tests

* fix: get jest configured properly

* fix: change timeout

* fix: uninstall port ready

* fix: readd port ready

* fix: local tests work

* Update yarn.lock

* add lint disable for setup files

* fix: update dependencies

* fix: basic test suite for nfts/tokens

* feat: collection data

* fix: make tests more comprehensive

* fix: change matches to contains

* fix: tests for twitter alt image tag

* fix: image gen

* fix: add patch-package

* fix: update yarn install

* feat: basic image gen for nfts and collections

* fix: remove vibrant attempt

* use watermark asset

* dynamically grab color

* modularize code and prototype for token preview

* refactor code

* finalize css

* fix color grabber

* update tests

* fix up css

* refactor code a bit more

* remove console logs

* tests

* update tests

* update images based on design feedback

* network logos

* update lint

* slight refactoring

* more refactoring

* fix packages

* Update yarn.lock

* remove dynamically generated image stuff

* cleanup return values

* Create README.md

* Revert "Create README.md"

This reverts commit 7a91c98d38.

* First round of feedback

* comments

* Update test.yml

* Update test.yml

* Update test.yml

* feedback round 2

* final feedback

* final final feedback

* add coverage and other options

* Update test.yml

* start typecheck

* update cache

* update snapshots?

* Update jest.config.json

* Update jest.config.json

* give timeout some buffer

* update import

* upgrade ts

* fix typing for apollo deps

* finalize typechecks

* downgrade typescript to original version

* add cache directory to jest

* remove coverage

* remove google analytics from tests

* review changes
2023-07-25 15:12:13 -04:00
Brendan Wong
b230cb62f4 feat: readme for cloud functions (#6914)
* feat: add token and nft injection

* feat: basic tests

* fix: get jest configured properly

* fix: change timeout

* fix: uninstall port ready

* fix: readd port ready

* fix: local tests work

* Update yarn.lock

* add lint disable for setup files

* fix: update dependencies

* fix: basic test suite for nfts/tokens

* feat: collection data

* fix: make tests more comprehensive

* fix: change matches to contains

* fix: tests for twitter alt image tag

* fix: image gen

* fix: add patch-package

* fix: update yarn install

* feat: basic image gen for nfts and collections

* fix: remove vibrant attempt

* use watermark asset

* dynamically grab color

* modularize code and prototype for token preview

* refactor code

* finalize css

* fix color grabber

* update tests

* fix up css

* refactor code a bit more

* remove console logs

* tests

* update tests

* update images based on design feedback

* network logos

* update lint

* slight refactoring

* more refactoring

* fix packages

* Update yarn.lock

* remove dynamically generated image stuff

* cleanup return values

* Create README.md

* Revert "Create README.md"

This reverts commit 7a91c98d38.

* Revert "Revert "Create README.md""

This reverts commit d0a4f5b951.

* add docs for html rewriter

* Update README.md

* remove previously removed files
2023-07-25 15:06:28 -04:00
Nate Wienert
9f71e384b2 fix: use deferred value to avoid suspense issues with inputting text (#6996)
fix: use deferred value to avoid suspense issues with inputting text during supsended render causing errors
2023-07-25 06:57:32 -10:00
Nate Wienert
8592a4a54d fix: flaky test covering searchbar (#7002)
fix flaky test covering searchbar
2023-07-24 15:06:10 -10:00
Charles Bachmeier
ccc94fdfc2 feat: update infura fork block height for cypress tests to be dynamic (#6998)
* import creation block from ur sdk

* update ur sdk
2023-07-24 18:00:29 -07:00
Zach Pomerantz
4537a4dc94 fix: display already-loaded imgs with no transition (#6990) 2023-07-24 16:21:05 -07:00
Jack Short
db0cb41b61 fix: fixing price range filter passing as number (#6994) 2023-07-24 17:51:04 -04:00
eddie
4ced70f737 fix: use 100vh to position bottom sheet (#6989) 2023-07-24 10:02:48 -07:00
Jack Short
9262fec093 feat: add opt out analytics (#6983)
* making shared settings toggle component

* adding description to analytics toggle

* trace analytics wrapper

* changing sendAnalytics to optOut

* updating functions

* moving atom location

* adding back testid to testnet toggle

* sending data on page load

* defaulting to true

* refactoring toggles

* renaming and moving to new filepath

* exporting everything out of analytics

* updating eslint

* typo

* responding to requested changes

* fixing merge conflicts
2023-07-21 13:15:51 -04:00
Jack Short
ff3bcc4693 fix: purchase tweet url (#6987) 2023-07-21 10:24:55 -04:00
Charles Bachmeier
34a22ef9f0 fix: chain query parameter doesn't block you from switching chains (#6926)
* useSearchParams

* delete old param

* properly handle a connected wallet

* update chain query via network switcher

* updated tests

* working test

* comment typo

* don't overwrite current params

* change chain based on user wallet interaction

* one instance of set

* update chainIdRef on account load

* useeffect

* set chainIdRef when isActive

* remove logging

* update comment

* make condition else if
2023-07-20 17:05:12 -07:00
eddie
5e86cf7b29 fix: token img loading state (#6984)
* fix: token img loading state

* fix: nits

* fix: update snapshots

* fix: token logos in MP and tests

* fix: really weird visibility / tooltip bug

* fix: update snapshots
2023-07-20 13:51:29 -07:00
Nate Wienert
83172dc5ea feat: add tracking params and go straight to app store for iOS for the landing page wallet CTA (#6732)
* feat: add tracking params and go straight to app store for iOS for the landing page wallet CTA
2023-07-20 10:28:08 -10:00
Nate Wienert
0ca68bb140 feat: disconnect button hides after hover out, click away, and improv… (#6968)
* feat: disconnect button hides after hover out, click away, and improved animations and colors
2023-07-20 09:00:56 -10:00
eddie
430356da9a fix: update text in ConfirmSwapModal (#6971) 2023-07-20 10:12:31 -07:00
Nate Wienert
1c50460160 fix: using search hotkey enter navigates to the wrong result (#6735)
* fix: fix SearchBarDropdown selecting invalid result on enter after initial search when recent searches are filled

* add test

* Update cypress/e2e/universal-search.test.ts

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

* Update cypress/e2e/universal-search.test.ts

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

* Update cypress/e2e/universal-search.test.ts

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

* use searhc hotkey

* Revert ""

This reverts commit 7b04d5d575.

* chore: gitignore cypress/downloads.html

---------

Co-authored-by: Zach Pomerantz <zzmp@uniswap.org>
2023-07-19 11:58:02 -10:00
Zach Pomerantz
2ef9e9b61c fix: show logos on warned tokens (#6978)
* fix: show logos on warned tokens

* test: update snapshots

* fix: hide logos for unsupported tokens
2023-07-19 14:00:50 -07:00
Charles Bachmeier
b906cddc6a fix: update depedencies to fix avax/bnb routing (#6974)
* fix: update depedencies to fix avax/bnb routing

* pull latest

* use rpc_url

* deduplicate
2023-07-19 09:58:08 -07:00
Ivan Sorokin
65fe8d4168 feat: add apple-app-site-association (#6957) 2023-07-18 23:59:58 +02:00
Charles Bachmeier
8956fba629 fix: Silence linter warnings for GA (#6962)
ga analytics
2023-07-18 09:59:19 -07:00
Charles Bachmeier
04884fe1b0 fix: Resolve Babel dependency warning (#6961) 2023-07-18 09:59:01 -07:00
Brendan Wong
ef28667d13 feat: cloudflare worker to inject meta tags (#6901)
* feat: add token and nft injection

* feat: basic tests

* fix: get jest configured properly

* fix: change timeout

* fix: uninstall port ready

* fix: readd port ready

* fix: local tests work

* Update yarn.lock

* add lint disable for setup files

* fix: update dependencies

* fix: basic test suite for nfts/tokens

* feat: collection data

* fix: make tests more comprehensive

* fix: change matches to contains

* fix: tests for twitter alt image tag

* fix: image gen

* fix: add patch-package

* fix: update yarn install

* feat: basic image gen for nfts and collections

* fix: remove vibrant attempt

* use watermark asset

* dynamically grab color

* modularize code and prototype for token preview

* refactor code

* finalize css

* fix color grabber

* update tests

* fix up css

* refactor code a bit more

* remove console logs

* tests

* update tests

* update images based on design feedback

* network logos

* update lint

* slight refactoring

* more refactoring

* fix packages

* Update yarn.lock

* remove dynamically generated image stuff

* cleanup return values

* Create README.md

* Revert "Create README.md"

This reverts commit 7a91c98d38.

* First round of feedback

* comments

* feedback round 2

* final feedback

* final final feedback

* nest twitter:image:alt in image check

* better title handling

---------

Co-authored-by: Zach Pomerantz <zzmp@uniswap.org>
2023-07-18 12:35:29 -04:00
Zach Pomerantz
0ced5f2402 fix: duck type error checking (#6924)
* fix: duck type error checking

* fix: use duck-typing

* simplify

* fix typings

* fix comments
2023-07-18 09:21:00 -07:00
Zach Pomerantz
252bf32d2f test(e2e): buy crypto modal flag (#6965) 2023-07-17 22:04:40 -07:00
eddie
01e87657c6 fix: format currency amounts as strings in logging (#6966)
* fix: format currency amounts as strings in logging

* fix: toExact
2023-07-17 17:27:17 -07:00
Brendan Wong
55bf30c0e0 fix: remove shadow and gap on mobile (#6906)
* remove shadow and gap on mobile

* remove empty brackets
2023-07-17 16:39:48 -04:00
Nate Wienert
95f61487e8 fix: uniswapx opt in double bounce animation (#6964) 2023-07-17 09:05:07 -10:00
eddie
3ed3ed4994 feat: sort tokens in selector by USD value (#6744)
* feat: sort tokens in selector by USD value

* fix: sync visible balances in list with the sorting values

* fix: tryParseCurrencyAmount

* fix: remove todo

* fix: make shared hook for cached query

* fix: replace true with modalOpen

* fix: default to zero balance

* fix: add test and comment

* feat: fallback to unfilterd tokens

* fix: unconnected balances

* fix: update tests

* fix: test selector
2023-07-17 11:28:28 -07:00
Zach Pomerantz
3a0c4ad4db fix: do not cache gql across CI (#6963) 2023-07-17 10:33:50 -07:00
Charles Bachmeier
803eb46e5f fix: Clear input and output when wrapping and unwrapping (#6913)
* add wrap handler

* onWrap passed back tx hash

* async await
2023-07-17 10:13:15 -07:00
Zach Pomerantz
a3f0c54f66 build: block promotion to prod on tests (#6904) 2023-07-17 10:12:59 -07:00
Charles Bachmeier
52796fcc55 feat: Sort chains based on user relevance (#6915)
* sort chains based on user relevance

* test.each

* remove unnecessary comment and add chainId to test name
2023-07-17 10:10:12 -07:00
eddie
ca02a6b56a fix: log opt in impression (#6959)
* fix: log opt in impression

* fix: move trace up to parent level
2023-07-17 10:06:42 -07:00
Jack Short
b722a20d96 fix: swap divide by zero (#6922)
* fix: swap divide by zero

* putting evaluation in memo
2023-07-17 12:38:49 -04:00
Jack Short
9b5261aaeb fix: v2 liquidity divide by zero (#6921) 2023-07-17 12:06:52 -04:00
Brendan Wong
0efb7f51a4 fix: remove new badge from Uniswap Wallet (#6911)
* remove new badge from Uniswap Wallet

* remove isNew field and badge

* clean lint
2023-07-17 11:55:28 -04:00
Zach Pomerantz
bbe42b81de fix: display token images on first pageload (#6956)
* fix: always show img for common bases

* fix: include backup img in first render

* fix: initialize and update token safety lookup

* test: update snapshots to include initial logos

* refactor: better code colocation

* test: updating token safety lookup table

* refactor: tokenSafetyLookup

* refactor: tokenLogoLookup

* fix: pass lists state to token safety update

* test: mock initial update
2023-07-15 18:33:13 -07:00
cartcrom
e9f469d399 fix: hide pending orders from GQL (#6955)
* fix: hide pending orders from GQL

* fix: update comment and add TODO
2023-07-15 18:03:21 +01:00
Tina
4c58258f01 feat: additional routing option prototype (#6934)
* add npm secret and modify github actions

* inject npm secret for tests as well

* revert changes to staging and prod actions because we arent going to use themmm

* remove unused github actions

* minor copy change for convenience lol

* feat: add DutchOrderTrade type to Swap components (#8)

* feat: add flag for gouda (#5)

* feat: add new signature details type (#4)

* feat: local gouda activity (#9)

* feat: Unified Routing API classic and dutch limit quote requests (#10)

* chore: Rebase 5/26 (#13)

Co-authored-by: Mike Grabowski <grabbou@gmail.com>
Co-authored-by: Zach Pomerantz <zzmp@uniswap.org>
Co-authored-by: Jordan Frankfurt <jordanwfrankfurt@gmail.com>
Co-authored-by: Vignesh Mohankumar <me@vig.xyz>
Co-authored-by: cartcrom <39385577+cartcrom@users.noreply.github.com>
Co-authored-by: Jack Short <john.short.tj@gmail.com>
Co-authored-by: eddie <66155195+just-toby@users.noreply.github.com>
Co-authored-by: Jordan Frankfurt <jordan@CORN-Jordan-949.frankfurt>
Co-authored-by: Jordan Frankfurt <jordan@corn-jordan-949.lan>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
Co-authored-by: Nate Wienert <natewienert@gmail.com>
Co-authored-by: Charles Bachmeier <charles@bachmeier.io>
Co-authored-by: Charles Bachmeier <charlie@genie.xyz>

* feat: add UniswapX to Settings (#7)

* feat: merge upstream 5/31 (#16)

* feat: Upgrade unified-routing-api URL (#15)

* chore: merge upstream 6/2 (#19)

Co-authored-by: Mike Grabowski <grabbou@gmail.com>
Co-authored-by: Zach Pomerantz <zzmp@uniswap.org>
Co-authored-by: Tina <59578595+tinaszheng@users.noreply.github.com>
Co-authored-by: Jordan Frankfurt <jordanwfrankfurt@gmail.com>
Co-authored-by: Vignesh Mohankumar <me@vig.xyz>
Co-authored-by: cartcrom <39385577+cartcrom@users.noreply.github.com>
Co-authored-by: Jack Short <john.short.tj@gmail.com>
Co-authored-by: Jordan Frankfurt <jordan@CORN-Jordan-949.frankfurt>
Co-authored-by: Jordan Frankfurt <jordan@corn-jordan-949.lan>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
Co-authored-by: Nate Wienert <natewienert@gmail.com>
Co-authored-by: Charles Bachmeier <charles@bachmeier.io>
Co-authored-by: Charles Bachmeier <charlie@genie.xyz>

* feat: uniswapx gas tooltip (#12)

Co-authored-by: Mike Grabowski <grabbou@gmail.com>
Co-authored-by: Zach Pomerantz <zzmp@uniswap.org>
Co-authored-by: Tina <59578595+tinaszheng@users.noreply.github.com>
Co-authored-by: Jordan Frankfurt <jordanwfrankfurt@gmail.com>
Co-authored-by: Vignesh Mohankumar <me@vig.xyz>
Co-authored-by: cartcrom <39385577+cartcrom@users.noreply.github.com>
Co-authored-by: Jack Short <john.short.tj@gmail.com>
Co-authored-by: Jordan Frankfurt <jordan@CORN-Jordan-949.frankfurt>
Co-authored-by: Jordan Frankfurt <jordan@corn-jordan-949.lan>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
Co-authored-by: Nate Wienert <natewienert@gmail.com>
Co-authored-by: Charles Bachmeier <charles@bachmeier.io>
Co-authored-by: Charles Bachmeier <charlie@genie.xyz>

* feat: swap callback (#17)

* feat: gouda gating (#14)

Co-authored-by: Mike Grabowski <grabbou@gmail.com>
Co-authored-by: Zach Pomerantz <zzmp@uniswap.org>
Co-authored-by: Tina <59578595+tinaszheng@users.noreply.github.com>
Co-authored-by: Jordan Frankfurt <jordanwfrankfurt@gmail.com>
Co-authored-by: Vignesh Mohankumar <me@vig.xyz>
Co-authored-by: cartcrom <39385577+cartcrom@users.noreply.github.com>
Co-authored-by: Jack Short <john.short.tj@gmail.com>
Co-authored-by: Jordan Frankfurt <jordan@CORN-Jordan-949.frankfurt>
Co-authored-by: Jordan Frankfurt <jordan@corn-jordan-949.lan>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
Co-authored-by: Nate Wienert <natewienert@gmail.com>
Co-authored-by: Charles Bachmeier <charles@bachmeier.io>
Co-authored-by: Charles Bachmeier <charlie@genie.xyz>

* fix: settings e2e test (#22)

* feat: update swap callback to add orders to redux state (#18)

* chore: Fix types for useBestTrade return result (#21)

* feat: gql gouda orders (#20)

* feat: show $0 for gas fee for now (#25)

* chore: Rebase 06/08 (#26)

Co-authored-by: Zach Pomerantz <zzmp@uniswap.org>
Co-authored-by: Tina <59578595+tinaszheng@users.noreply.github.com>
Co-authored-by: Jordan Frankfurt <jordanwfrankfurt@gmail.com>
Co-authored-by: Vignesh Mohankumar <me@vig.xyz>
Co-authored-by: cartcrom <39385577+cartcrom@users.noreply.github.com>
Co-authored-by: Jack Short <john.short.tj@gmail.com>
Co-authored-by: eddie <66155195+just-toby@users.noreply.github.com>
Co-authored-by: Jordan Frankfurt <jordan@CORN-Jordan-949.frankfurt>
Co-authored-by: Jordan Frankfurt <jordan@corn-jordan-949.lan>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
Co-authored-by: Nate Wienert <natewienert@gmail.com>
Co-authored-by: Charles Bachmeier <charles@bachmeier.io>
Co-authored-by: Charles Bachmeier <charlie@genie.xyz>
Co-authored-by: Brendan Wong <35351983+LunrEclipse@users.noreply.github.com>
Co-authored-by: cartcrom <cartergcromer@gmail.com>
Co-authored-by: clrdo <129212060+clrdo@users.noreply.github.com>
Co-authored-by: clrdo <clrdo@github.com>
Co-authored-by: Eddie Dugan <eddie.dugan@uniswap.org>

* feat: poll on order submit (#23)

* feat: update gouda-sdk to 1.0.0-alpha.3 (#31)

* feat: rename gasUseEstimateUSD for dutch orders (#30)

Co-authored-by: Tina Zheng <tina.s.zheng+github@gmail.com>

* chore: Fix response types (#36)

* feat: Gouda ETH input flow (#29)

Co-authored-by: Eddie Dugan <eddie.dugan@uniswap.org>

* fix: use trade to determine what router label to show (#41)

* feat: open uniswapx modal on click (#32)

* feat: gouda logging new params in swap quote received (#33)

* fix: wrap step ui fixes (#40)

* feat: use BE deadline padding (#46)

* chore: merge 6/23 (#50)

Co-authored-by: Mike Grabowski <grabbou@gmail.com>
Co-authored-by: Zach Pomerantz <zzmp@uniswap.org>
Co-authored-by: Jordan Frankfurt <jordanwfrankfurt@gmail.com>
Co-authored-by: Vignesh Mohankumar <me@vig.xyz>
Co-authored-by: cartcrom <39385577+cartcrom@users.noreply.github.com>
Co-authored-by: Jack Short <john.short.tj@gmail.com>
Co-authored-by: eddie <66155195+just-toby@users.noreply.github.com>
Co-authored-by: Jordan Frankfurt <jordan@CORN-Jordan-949.frankfurt>
Co-authored-by: Jordan Frankfurt <jordan@corn-jordan-949.lan>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
Co-authored-by: Nate Wienert <natewienert@gmail.com>
Co-authored-by: Charles Bachmeier <charles@bachmeier.io>
Co-authored-by: Charles Bachmeier <charlie@genie.xyz>
Co-authored-by: Brendan Wong <35351983+LunrEclipse@users.noreply.github.com>
Co-authored-by: cartcrom <cartergcromer@gmail.com>
Co-authored-by: clrdo <129212060+clrdo@users.noreply.github.com>
Co-authored-by: clrdo <clrdo@github.com>
Co-authored-by: Shubham Rasal <95695273+Shubham-Rasal@users.noreply.github.com>
Co-authored-by: Saro Vindigni <sarovindigni@bolket.com>
Co-authored-by: Jordan Frankfurt <<jordan@CORN-Jordan-949.frankfurt>
Co-authored-by: John Short <john.short@CORN-Jack-899.local>

* feat: Gouda opt-in flow request logic (#37)

Co-authored-by: eddie <66155195+just-toby@users.noreply.github.com>

* feat: hide slippage and deadline settings when the current trade is gouda (#44)

* feat: use settled order amounts (#45)

* feat: fetch receipt before dispatch (#49)

* fix: updated order popups to launch modal (#48)

* feat: Use slippage value from URA response for UniswapX trades (#51)

* fix: Bump gouda-sdk to match backend response for quotes (#58)

* feat: Change gouda order status URL param from offerer -> swapper (#59)

* feat: disable opt in flow (#57)

* feat: Dont show USD value change for uniswapx trades (#55)

* fix: Don't use WETH as input currency for Classic ETH-in trades (#54)

* feat: Reset to WETH after wrap is complete (#52)

* fix: correct descriptor in UniswapX activity row items (#61)

* fix: align review modal and gouda activity modal (#60)

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

* feat: Get wrap and approve gas info (#53)

Co-authored-by: eddie <66155195+just-toby@users.noreply.github.com>

* fix: restore summary view when wrap is rejected (#66)

* fix: serialize tx receipts before storing (#64)

* fix: Insufficient balance check should read from the right currency (#65)

* feat: update designs for gas tooltips (#67)

* fix: UniswapX gas descriptor boolean (#69)

* chore: Bump conedison for better gas price formatting (#70)

* chore: Switch from gouda-sdk to uniswapx-sdk (#71)

* chore: Rename all variables `gouda` to UniswapX (#72)

* chore: Merge 7/8/23 (#73)

Co-authored-by: Mike Grabowski <grabbou@gmail.com>
Co-authored-by: Zach Pomerantz <zzmp@uniswap.org>
Co-authored-by: Jordan Frankfurt <jordanwfrankfurt@gmail.com>
Co-authored-by: Vignesh Mohankumar <me@vig.xyz>
Co-authored-by: cartcrom <39385577+cartcrom@users.noreply.github.com>
Co-authored-by: Jack Short <john.short.tj@gmail.com>
Co-authored-by: eddie <66155195+just-toby@users.noreply.github.com>
Co-authored-by: Jordan Frankfurt <jordan@CORN-Jordan-949.frankfurt>
Co-authored-by: Jordan Frankfurt <jordan@corn-jordan-949.lan>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
Co-authored-by: Nate Wienert <natewienert@gmail.com>
Co-authored-by: Charles Bachmeier <charles@bachmeier.io>
Co-authored-by: Charles Bachmeier <charlie@genie.xyz>
Co-authored-by: Brendan Wong <35351983+LunrEclipse@users.noreply.github.com>
Co-authored-by: cartcrom <cartergcromer@gmail.com>
Co-authored-by: clrdo <129212060+clrdo@users.noreply.github.com>
Co-authored-by: clrdo <clrdo@github.com>
Co-authored-by: Shubham Rasal <95695273+Shubham-Rasal@users.noreply.github.com>
Co-authored-by: Saro Vindigni <sarovindigni@bolket.com>
Co-authored-by: Jordan Frankfurt <<jordan@CORN-Jordan-949.frankfurt>
Co-authored-by: John Short <john.short@CORN-Jack-899.local>
Co-authored-by: Charlie Bachmeier <charlie.bachmeier@Charlies-MacBook-Pro.local>
Co-authored-by: UL Service Account <hello-happy-puppy@users.noreply.github.com>

* chore(conedison): update package (#77)

* feat: add opt-in UI (#68)

* chore: address some todos (#79)

* chore: Rename feature flag from gouda_enabled to uniswapx_enabled (#81)

* feat: Copy changes (#82)

* fix: improve timings on animations for gouda opt-in (#80)

* chore: Use updated URLs (#84)

* chore: Merge 7/14 (#85)

Co-authored-by: Mike Grabowski <grabbou@gmail.com>
Co-authored-by: Zach Pomerantz <zzmp@uniswap.org>
Co-authored-by: Jordan Frankfurt <jordanwfrankfurt@gmail.com>
Co-authored-by: Vignesh Mohankumar <me@vig.xyz>
Co-authored-by: cartcrom <39385577+cartcrom@users.noreply.github.com>
Co-authored-by: Jack Short <john.short.tj@gmail.com>
Co-authored-by: eddie <66155195+just-toby@users.noreply.github.com>
Co-authored-by: Jordan Frankfurt <jordan@CORN-Jordan-949.frankfurt>
Co-authored-by: Jordan Frankfurt <jordan@corn-jordan-949.lan>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
Co-authored-by: Nate Wienert <natewienert@gmail.com>
Co-authored-by: Charles Bachmeier <charles@bachmeier.io>
Co-authored-by: Charles Bachmeier <charlie@genie.xyz>
Co-authored-by: Brendan Wong <35351983+LunrEclipse@users.noreply.github.com>
Co-authored-by: cartcrom <cartergcromer@gmail.com>
Co-authored-by: clrdo <129212060+clrdo@users.noreply.github.com>
Co-authored-by: clrdo <clrdo@github.com>
Co-authored-by: Shubham Rasal <95695273+Shubham-Rasal@users.noreply.github.com>
Co-authored-by: Saro Vindigni <sarovindigni@bolket.com>
Co-authored-by: Jordan Frankfurt <<jordan@CORN-Jordan-949.frankfurt>
Co-authored-by: John Short <john.short@CORN-Jack-899.local>
Co-authored-by: Charlie Bachmeier <charlie.bachmeier@Charlies-MacBook-Pro.local>
Co-authored-by: UL Service Account <hello-happy-puppy@users.noreply.github.com>

* remove changes to github actions files

* fix import

* actually revert changes to yml

* remove method export

* feat: Add feature flag for synthetic quotes (#6938)

* fix: use Lingui Trans macro (#6943)

* fix: use trans macro

* add comments

* fix: update updater.tsx (#6942)

* fix: reformat variable to use ms

* move interval definition above getOrderStatus

* lint :)

* revert

* chore: bunch of nits (#6944)

bunch of nits

* fix: translations etc (#6945)

* chore: Remove placeholder signature types (#6937)

remove placeholder

* chore: merge main into branch (#6948)

* fix: Handle Scientific Notation for NFT Collection Activity Prices (#6936)

wrap nft activity price in

* fix: e2e tests (#6941)

* fix: e2e test

* fix: set flag for buy-crypto-modal test

* fix: fund DAI

---------

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

---------

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

* feat: make inputCurrency optional for swapheader (#6947)

* make inputCurrency optional for swapheader

* optional pass in

* fix: function defined twice (#6950)

fix lint

* test: add signatureToActivity undefined tests (#6949)

* fix: update token lists schema (#6951)

fix: update token list schema

* chore: some last nits (#6953)

* refactor: base type

* test: useUserDisabledUniswapX

* chore: simplify useAllSignatures usage

* chore: standard check order

* lint

---------

Co-authored-by: eddie <66155195+just-toby@users.noreply.github.com>
Co-authored-by: cartcrom <39385577+cartcrom@users.noreply.github.com>
Co-authored-by: Mike Grabowski <grabbou@gmail.com>
Co-authored-by: Zach Pomerantz <zzmp@uniswap.org>
Co-authored-by: Jordan Frankfurt <jordanwfrankfurt@gmail.com>
Co-authored-by: Vignesh Mohankumar <me@vig.xyz>
Co-authored-by: Jack Short <john.short.tj@gmail.com>
Co-authored-by: Jordan Frankfurt <jordan@CORN-Jordan-949.frankfurt>
Co-authored-by: Jordan Frankfurt <jordan@corn-jordan-949.lan>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
Co-authored-by: Nate Wienert <natewienert@gmail.com>
Co-authored-by: Charles Bachmeier <charles@bachmeier.io>
Co-authored-by: Charles Bachmeier <charlie@genie.xyz>
Co-authored-by: Brendan Wong <35351983+LunrEclipse@users.noreply.github.com>
Co-authored-by: cartcrom <cartergcromer@gmail.com>
Co-authored-by: clrdo <129212060+clrdo@users.noreply.github.com>
Co-authored-by: clrdo <clrdo@github.com>
Co-authored-by: Eddie Dugan <eddie.dugan@uniswap.org>
Co-authored-by: marktoda <toda.mark@gmail.com>
Co-authored-by: Shubham Rasal <95695273+Shubham-Rasal@users.noreply.github.com>
Co-authored-by: Saro Vindigni <sarovindigni@bolket.com>
Co-authored-by: Jordan Frankfurt <<jordan@CORN-Jordan-949.frankfurt>
Co-authored-by: John Short <john.short@CORN-Jack-899.local>
Co-authored-by: Charlie Bachmeier <charlie.bachmeier@Charlies-MacBook-Pro.local>
Co-authored-by: UL Service Account <hello-happy-puppy@users.noreply.github.com>
2023-07-14 14:46:59 -07:00
Jack Short
f6e6db6430 fix: pwat stack overflow (#6952) 2023-07-14 17:35:08 -04:00
Charles Bachmeier
dab445f237 fix: e2e tests (#6941)
* fix: e2e test

* fix: set flag for buy-crypto-modal test

* fix: fund DAI

---------

Co-authored-by: Zach Pomerantz <zzmp@uniswap.org>
2023-07-14 11:54:25 -07:00
Charles Bachmeier
da90738ba1 fix: Handle Scientific Notation for NFT Collection Activity Prices (#6936)
wrap nft activity price in
2023-07-14 09:54:48 -07:00
Vignesh Mohankumar
e4dbef80eb chore: remove Google Analytics from CSP (#6888)
* disable ga

* comment
2023-07-07 14:15:41 -04:00
Vignesh Mohankumar
e169c9d900 chore: delete WalletConnect v1 (#6857)
* chore: remove walletConnectFallback flag

* chore: remove walletConnectV2 flag

* chore: delete WalletConnect v1

* chore: remove walletConnectV2 flag (#6856)

* rm hidden

* toggle account drawer

* fix test

* Revert "rm hidden"

This reverts commit 9e8dd65b04.

* fix test

* -u

* optional?

* Revert "optional?"

This reverts commit 8456080b78.

* switch mock

* expect

* test

* fix test

* update connection tests
2023-07-07 14:06:07 -04:00
Zach Pomerantz
c8a17f5fe9 build: use context.sha (#6900) 2023-07-07 10:48:00 -07:00
Vignesh Mohankumar
552a38994b chore: remove WalletConnect feature flags (#6855)
* chore: remove walletConnectFallback flag

* chore: remove walletConnectV2 flag (#6856)

* rm hidden

* toggle account drawer

* fix test

* Revert "rm hidden"

This reverts commit 9e8dd65b04.

* fix test

* -u

* optional?

* Revert "optional?"

This reverts commit 8456080b78.

* switch mock

* expect

* test
2023-07-07 13:15:13 -04:00
Zach Pomerantz
b313ac9843 build: upgrade cypress-hardhat (#6897) 2023-07-07 10:03:06 -07:00
Jack Short
88aec2c894 fix: 0 showing as collected fee amount (#6804)
* fix: 0 showing as collected fee amount

* initial test setup

* fire events

* checking for data in tests
2023-07-07 12:51:35 -04:00
Zach Pomerantz
86677ed193 test(staging): add staging-only t9n test (#6899)
* test(staging): add staging-only test

* test(staging): translations mount

* ci(t9n): download translations from crowdin

* test: proving that it works

* Revert "ci(t9n): download translations from crowdin"

This reverts commit b6e0f0c5c2.

* Revert "test: proving that it works"

This reverts commit 1d3a1c2a23.

* fix: spec->test

---------

Co-authored-by: UL Service Account <hello-happy-puppy@users.noreply.github.com>
2023-07-07 09:41:20 -07:00
Zach Pomerantz
accf0905f8 build: block promotion on Test (#6898) 2023-07-07 08:47:32 -07:00
Brendan Wong
ef5065de48 feat: wrangler infra and test environment (#6797)
* feat: add token and nft injection

* feat: basic tests

* fix: get jest configured properly

* fix: change timeout

* fix: uninstall port ready

* fix: readd port ready

* fix: local tests work

* fix: remove other stuff to make this pr smaller

* fix: remove unneeded dependencies

* fix: remove port-ready

* Update yarn.lock

* fix: add lint disable due to module exports for jest

* fix: added waitPort

* fix: deduplicated things?

* Update package.json

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

* update typing

* change tests to typescript instead of javascript

* changing typing of globalThis servers

* yarn deduplicate

* Update functions/global-teardown.ts

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

* Update functions/global.d.ts

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

* Update functions/tsconfig.json

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

* Update functions/tsconfig.json

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

* Update package.json

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

* change dependencies to dev dependencies

* final touches

---------

Co-authored-by: Zach Pomerantz <zzmp@uniswap.org>
2023-07-07 11:41:09 -04:00
Vignesh Mohankumar
bcb45cded6 feat: add user id to sentry (#6896)
* feat: add user id to sentry

* downgrade

* key

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

* fixes

---------

Co-authored-by: Zach Pomerantz <zzmp@uniswap.org>
2023-07-07 11:14:36 -04:00
cartcrom
dfe50b4bee feat: remove old routing slice and flag (#6895)
feat: remove ura flag
2023-07-07 09:49:42 -04:00
Charles Bachmeier
90f72e05b9 feat: upgrade sdk-core to 3.2.6 and add AVAX (#6757)
* init refactor

* upgrade to 3.2.6 and refactor more uses of chainid

* cleaned up lock file

* remove console log and add placeholder

* use supported chains type for switch fn

* allow passing of all chainIds to switcher

* additional typecast

* better casting solution

* yarn.lock cleanup

* prettier

* better casting for rpc

* prettier

* deprecate no longer needed addresses

* better isSupported checking

* deprecate redundant fn

* cleanup toSupportedChainId

* address initial ocmments

* pretier

* includes testnet

* remove unused export

* merge conflicts

* spread for mutable copy

* explorer text

* check is supported before activating chain

* remove extra uses of mumbai

* remove cast to MockChainId

* retain var name supportedChainId

* updated prettier

* use t for explorer translation

* use mockchainid for test

* feat: Add Avalanche support (#6776)

* init avax

* add most avax props

* add logo

* correct subgraph

* update avax blocksPerFetch

* add square logo

* upgrade ur sdk

* version syntax

* correct tokens

* remove unused token

* remove unused token

* update token list and add coming soon to searchbar

* add coming soon to token explore page

* names to ids

* cleaned up routing

* usdc token

* upgrade default token list

* update SOR

* upgrade SOR

* merge conflicts

* snowtrace

* upgrade SOR

* handle be avax support

* temp hide avax

* show pool positions

* whole

* spotprice update

* not yet supported

* BACKEND_SUPPORTED_CHAINS

* add avax to BE not supported

* update multicall blocks to 5

* add todo

* updated prettier

---------

Co-authored-by: Charles Bachmeier <charlie@genie.xyz>

* be added avax token balances

* validateUrlChainParam should return eth for backend unsupported chain

* readonly

* respond to comments

---------

Co-authored-by: Charles Bachmeier <charlie@genie.xyz>
2023-07-06 21:44:06 -07:00
Vignesh Mohankumar
3a0f6920d0 build: use webpack-retry-chunk-load-plugin (#6885)
* build: use webpack retry chunk load plugin

* fix

* dedupe

* lint

* retry backoff

* reduce from 1000 to 500ms

* add cache bust query

* rm cache bust

* 3

* cache bust

* Update craco.config.cjs

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

---------

Co-authored-by: Zach Pomerantz <zzmp@uniswap.org>
2023-07-06 17:54:49 -04:00
eddie
07eb9eb9a2 fix: combine userState with default injected wallet in cypress setup (#6849)
* fix: combine userState with default injected wallet in cypress setup

* fix: unconnected state in two tests

* fix: update name
2023-07-06 14:01:13 -07:00
Brendan Wong
a9e8e8b275 fix: web app without NFTs (#6712)
* fix: web app without NFTs

* fix: change card display to memoization

* fix: remove unneeded imports and variables

* fix: readd search bar modification

* fix: update dependencies

* fix: update atom value to use hook

* fix: change hook name

* fix: add tests

* Update src/pages/Landing/index.tsx

Co-authored-by: Jordan Frankfurt <jordanwfrankfurt@gmail.com>

* Update src/components/NavBar/SearchBar.tsx

Co-authored-by: Jordan Frankfurt <jordanwfrankfurt@gmail.com>

* Update src/components/NavBar/SearchBarDropdown.tsx

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

* Update src/components/NavBar/SearchBarDropdown.test.tsx

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

* Update SearchBar.tsx

* fix lint

---------

Co-authored-by: Jordan Frankfurt <jordanwfrankfurt@gmail.com>
Co-authored-by: Zach Pomerantz <zzmp@uniswap.org>
2023-07-06 15:46:41 -04:00
Vignesh Mohankumar
4708d3d3d7 fix: update InterfaceGqlChain syntax (#6892)
lint: update InterfaceGqlChain syntax
2023-07-06 12:31:15 -04:00
Vignesh Mohankumar
27acddc0e7 chore: upgrade prettier (#6887)
* lint

* upgrade prettier

---------

Co-authored-by: Zach Pomerantz <zzmp@uniswap.org>
2023-07-06 11:54:04 -04:00
Charles Bachmeier
102a935ff8 fix: Handle new and unsupported chains passed in from GQL BE (#6878)
* add subset chain type and checks

* add sentry logging for invalid chain errors

* add test cases

* dynamic token balances

* add test for BE adding a new chain

* rename and use slice

* address comments

* undo yarn.lock changes

* make copy in utils

* chore: Declare GQL variables as readonly (#6889)

* declare gql variables as readonly

* remove console log

* Merge branch 'cab/error_supported_chain' into tina/gql-readonly

---------

Co-authored-by: Charlie B <charles@bachmeier.io>

---------

Co-authored-by: Charlie Bachmeier <charlie.bachmeier@Charlies-MacBook-Pro.local>
Co-authored-by: Tina <59578595+tinaszheng@users.noreply.github.com>
2023-07-05 21:31:30 -07:00
Charles Bachmeier
614c15243e fix: tokens with same name brake cypress test (#6890)
* fix: tokens with same name brake cypress test

* lowercase
2023-07-05 20:23:03 -07:00
Zach Pomerantz
082db21308 build: set test status per commit (#6824)
* build: annotate commit with test status

* build: Test / promotion

* fix: ellipsis

* build: also run on releases/staging

* errant comma

* script context

* runId

* build: post after pre
2023-07-05 14:42:43 -07:00
Jack Short
0f75c6a52b chore: matching swap input number formatting to mobile (#6788)
* initial currency formatting

* updating price formatting on swap

* updating the test
2023-07-05 17:37:57 -04:00
Brendan Wong
1c2ed1d9e4 fix: add margins for empty pool icon (#6884)
* fix: increase height of svg

* update test
2023-07-05 16:36:18 -04:00
Vignesh Mohankumar
0e956fb7c4 chore: remove @walletconnect/modal dependency (#6877) 2023-07-05 13:34:24 -07:00
Zach Pomerantz
b49dd03e25 test(e2e): use electron (#6827)
* test(e2e): use electron

* build: use default container for cypress-test-matrix
2023-07-05 13:30:54 -07:00
Charles Bachmeier
82211a888c fix: show bnb pool positions in mini port (#6880) 2023-07-05 11:16:11 -07:00
cartcrom
5dd8cbbdc9 chore: update tos & privacy policy last updated date (#6882) 2023-07-05 14:12:20 -04:00
eddie
5640c115de fix: remove error popup when switching chains from URL param (#6866) 2023-07-03 17:13:12 -07:00
cartcrom
7b8114b401 fix: remove phantom from injection detection (#6875)
* fix: remove phantom from injection detection

* fix: remove logo image
2023-07-03 16:24:08 -04:00
Brendan Wong
8e36361866 fix: remove blue dot next to buy (#6808)
* remove button

* remove dependencies

* fix: remove useBuyFiatFlowCompleted

* fix: update lint and unit tests

* fix: update user state

* fix: remove migration

* Revert "fix: remove migration"

This reverts commit eef313f9ce.

* test

* fix: add ts ignore
2023-07-03 15:55:49 -04:00
Brendan Wong
0a04cff7eb fix: icons no longer clip (#6812) 2023-06-30 17:41:53 -04:00
Jack Short
d3dd1d4ebd fix: position list row highlight overflowing container (#6805) 2023-06-30 17:25:24 -04:00
485 changed files with 143523 additions and 6001 deletions

5
.env
View File

@@ -4,6 +4,8 @@ REACT_APP_AMPLITUDE_PROXY_URL="https://api.uniswap.org/v1/amplitude-proxy"
REACT_APP_AWS_API_REGION="us-east-2"
REACT_APP_AWS_API_ENDPOINT="https://beta.api.uniswap.org/v1/graphql"
REACT_APP_BNB_RPC_URL="https://rough-sleek-hill.bsc.quiknode.pro/413cc98cbc776cda8fdf1d0f47003583ff73d9bf"
REACT_APP_BASE_GOERLI_RPC_URL="https://wiser-compatible-mansion.base-goerli.quiknode.pro/5874f36248e17020a1006149e7f68c63967e1f45/"
REACT_APP_BASE_MAINNET_RPC_URL="https://cool-white-diagram.base-mainnet.quiknode.pro/d8f036f35dfab2c68f32dfa822cd971e7a25a117/"
REACT_APP_INFURA_KEY="4bf032f2d38a4ed6bb975b80d6340847"
REACT_APP_MOONPAY_API="https://api.moonpay.com"
REACT_APP_MOONPAY_LINK="https://us-central1-uniswap-mobile.cloudfunctions.net/signMoonpayLinkV2?platform=web&env=staging"
@@ -11,4 +13,5 @@ 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"
REACT_APP_WALLET_CONNECT_PROJECT_ID="c6c9bacd35afa3eb9e6cccf6d8464395"
REACT_APP_UNISWAP_API_URL="https://api.uniswap.org/v2"
REACT_APP_WALLET_CONNECT_PROJECT_ID="c6c9bacd35afa3eb9e6cccf6d8464395"

View File

@@ -13,7 +13,6 @@ module.exports = {
files: ['**/*'],
rules: {
'multiline-comment-style': ['error', 'separate-lines'],
'rulesdir/enforce-retry-on-import': 'error',
'rulesdir/no-undefined-or': 'error',
},
},
@@ -58,5 +57,22 @@ module.exports = {
],
},
},
{
files: ['**/*.ts', '**/*.tsx'],
excludedFiles: ['src/analytics/*'],
rules: {
'no-restricted-imports': [
'error',
{
paths: [
{
name: '@uniswap/analytics',
message: `Do not import from '@uniswap/analytics' directly. Use 'analytics' instead.`,
},
],
},
],
},
},
],
}

View File

@@ -40,16 +40,11 @@ runs:
run: yarn contracts
shell: bash
# GraphQL is generated from schema. The schema is always fetched, but if unchanged, graphql does not need to be re-generated.
- run: yarn graphql:fetch
shell: bash
- uses: actions/cache@v3
id: graphql-cache
with:
path: src/graphql/**/__generated__
key: ${{ runner.os }}-graphql-${{ hashFiles('src/graphql/**/schema.graphql') }}
- if: steps.graphql-cache.outputs.cache-hit != 'true'
run: yarn graphql:generate
# GraphQL is generated from schema and client-side graphql queries. The schema is always fetched and changes to
# client-side queries are hard to detect, so it is always re-generated.
# TODO(WEB-2498): Cache based on both fetched schema and client-side graphql queries.
# This will require some processing: cp all literal graphql tags into a separate file and hash it?
- run: yarn graphql
shell: bash
# Messages are extracted from source.

View File

@@ -14,6 +14,21 @@ jobs:
environment:
name: push/staging
steps:
- name: Check test status
uses: actions/github-script@v6.4.1
with:
script: |
const statuses = await github.rest.repos.listCommitStatusesForRef({
owner: context.repo.owner,
repo: context.repo.repo,
ref: context.sha
})
const status = statuses.data.find(status => status.context === 'Test / promotion')?.state || 'missing'
core.info('Status: ' + status)
if (status !== 'success') {
core.setFailed('"Test / promotion" must be successful before pushing')
}
- uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab
with:
token: ${{ secrets.RELEASE_SERVICE_ACCESS_TOKEN }}

View File

@@ -14,6 +14,21 @@ jobs:
environment:
name: push/prod
steps:
- name: Check test status
uses: actions/github-script@v6.4.1
with:
script: |
const statuses = await github.rest.repos.listCommitStatusesForRef({
owner: context.repo.owner,
repo: context.repo.repo,
ref: context.sha
})
const status = statuses.data.find(status => status.context === 'Test / promotion')?.state || 'missing'
core.info('Status: ' + status)
if (status !== 'success') {
core.setFailed('"Test / promotion" must be successful before pushing')
}
- uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab
with:
token: ${{ secrets.RELEASE_SERVICE_ACCESS_TOKEN }}

View File

@@ -9,6 +9,7 @@ on:
push:
branches:
- main
- releases/staging
pull_request:
jobs:
@@ -99,6 +100,23 @@ jobs:
path: build
if-no-files-found: error
cypress-typecheck:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: ./.github/actions/setup
- uses: ./.github/actions/cache-on-main
with:
path: node_modules/.cache
key: ${{ runner.os }}-cypress-tsc-${{ github.run_id }}
restore-keys: ${{ runner.os }}-cypress-tsc-
- run: yarn typecheck:cypress
- if: failure() && github.ref_name == 'main'
uses: ./.github/actions/report
with:
name: Cypress typecheck
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_TEST_REPORTER_WEBHOOK }}
# Allows for parallel re-runs of cypress tests without re-building.
cypress-rerun:
runs-on: ubuntu-latest
@@ -108,7 +126,6 @@ jobs:
cypress-test-matrix:
needs: [build-e2e, cypress-rerun]
runs-on: ubuntu-latest
container: cypress/browsers:node-18.14.1-chrome-111.0.5563.64-1-ff-111.0-edge-111.0.1661.43-1
strategy:
fail-fast: false
matrix:
@@ -142,8 +159,9 @@ jobs:
parallel: true
start: yarn serve
wait-on: 'http://localhost:3000'
browser: chrome
browser: electron
group: e2e
spec: ${{ github.ref_name == 'releases/staging' && 'cypress/{e2e,staging}/**/*.test.ts' || 'cypress/e2e/**/*.test.ts' }}
env:
CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -167,3 +185,80 @@ jobs:
with:
name: Cypress tests
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_TEST_REPORTER_WEBHOOK }}
cloud-typecheck:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: ./.github/actions/setup
- uses: ./.github/actions/cache-on-main
with:
path: node_modules/.cache
key: ${{ runner.os }}-cloud-tsc-${{ github.run_id }}
restore-keys: ${{ runner.os }}-cloud-tsc-
- run: yarn typecheck:cloud
- if: failure() && github.ref_name == 'main'
uses: ./.github/actions/report
with:
name: Cloud typecheck
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_TEST_REPORTER_WEBHOOK }}
# TODO(WEB-2537): Setup CodeCOV
cloud-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: ./.github/actions/setup
- uses: ./.github/actions/cache-on-main
with:
path: node_modules/.cache
key: ${{ runner.os }}-cloud-jest-${{ github.run_id }}
restore-keys: ${{ runner.os }}-cloud-jest-
- run: yarn test:cloud --coverage --maxWorkers=100%
- if: failure() && github.ref_name == 'main'
uses: ./.github/actions/report
with:
name: Cloud tests
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_TEST_REPORTER_WEBHOOK }}
pre:
if: ${{ github.ref_name == 'main' || github.ref_name == 'releases/staging' }}
runs-on: ubuntu-latest
steps:
- uses: actions/github-script@v6.4.1
with:
script: |
github.rest.repos.createCommitStatus({
owner: context.repo.owner,
repo: context.repo.repo,
sha: context.sha,
state: 'pending',
context: 'Test / promotion',
description: 'Running tests...',
target_url: 'https://github.com/Uniswap/interface/actions/runs/' + context.runId
})
post:
if: ${{ github.ref_name == 'main' || github.ref_name == 'releases/staging' }}
needs: [pre, lint, typecheck, deps-tests, unit-tests, cypress-test-matrix]
runs-on: ubuntu-latest
steps:
- uses: actions/github-script@v6.4.1
with:
script: |
github.rest.repos.createCommitStatus({
owner: context.repo.owner,
repo: context.repo.repo,
sha: context.sha,
state: ${{ env.STATUS }} ? 'success' : 'failure',
context: 'Test / promotion',
description: ${{ env.STATUS }} ? 'All tests passed' : 'One or more tests failed and are blocking promotion',
target_url: 'https://github.com/Uniswap/interface/actions/runs/' + context.runId
})
env:
STATUS: |
${{ needs.lint.result == 'success' }} &&
${{ needs.typecheck.result == 'success' }} &&
${{ needs.deps-tests.result == 'success' }} &&
${{ needs.unit-tests.result == 'success' }} &&
${{ needs.cypress-test-matrix.result == 'success' }}

4
.gitignore vendored
View File

@@ -19,6 +19,7 @@ schema.graphql
# testing
/coverage
/cache
/functions/coverage
# builds
/build
@@ -46,7 +47,10 @@ notes.txt
package-lock.json
cypress/downloads
cypress/videos
cypress/screenshots
.vercel
.wrangler

1
CODEOWNERS Normal file
View File

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

View File

@@ -6,6 +6,7 @@ const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const path = require('path')
const ModuleScopePlugin = require('react-dev-utils/ModuleScopePlugin')
const { DefinePlugin, IgnorePlugin, ProvidePlugin } = require('webpack')
const { RetryChunkLoadPlugin } = require('webpack-retry-chunk-load-plugin')
const commitHash = execSync('git rev-parse HEAD').toString().trim()
const isProduction = process.env.NODE_ENV === 'production'
@@ -93,6 +94,16 @@ module.exports = {
// See https://vanilla-extract.style/documentation/integrations/webpack/#identifiers for docs.
// See https://github.com/vanilla-extract-css/vanilla-extract/issues/771#issuecomment-1249524366.
new VanillaExtractPlugin({ identifiers: 'short' }),
new RetryChunkLoadPlugin({
cacheBust: `function() {
return 'cache-bust=' + Date.now();
}`,
// Retries with exponential backoff (500ms, 1000ms, 2000ms).
retryDelay: `function(retryAttempt) {
return 2 ** (retryAttempt - 1) * 500;
}`,
maxRetries: 3,
}),
],
configure: (webpackConfig) => {
// Configure webpack plugins:

View File

@@ -1,7 +1,6 @@
import codeCoverageTask from '@cypress/code-coverage/task'
import { defineConfig } from 'cypress'
import { setupHardhatEvents } from 'cypress-hardhat'
import { unlinkSync } from 'fs'
export default defineConfig({
projectId: 'yp82ef',
@@ -9,30 +8,14 @@ export default defineConfig({
chromeWebSecurity: false,
experimentalMemoryManagement: true, // better memory management, see https://github.com/cypress-io/cypress/pull/25462
retries: { runMode: 2 },
videoCompression: false,
video: false, // GH provides 2 CPUs, and cypress video eats one up, see https://github.com/cypress-io/cypress/issues/20468#issuecomment-1307608025
e2e: {
async setupNodeEvents(on, config) {
await setupHardhatEvents(on, config)
codeCoverageTask(on, config)
// Delete recorded videos for specs that passed without flakes.
on('after:spec', async (spec, results) => {
if (results && results.video) {
// If there were no failures (including flakes), delete the recorded video.
if (!results.tests?.some((test) => test.attempts.some((attempt) => attempt?.state === 'failed'))) {
unlinkSync(results.video)
}
}
})
return {
...config,
// Only enable Chrome.
// Electron (the default) has issues injecting window.ethereum before pageload, so it is not viable.
browsers: config.browsers.filter(({ name }) => name === 'chrome'),
}
return config
},
baseUrl: 'http://localhost:3000',
specPattern: 'cypress/e2e/**/*.{js,jsx,ts,tsx}',
specPattern: 'cypress/{e2e,staging}/**/*.test.ts',
},
})

View File

@@ -52,7 +52,7 @@ This becomes more relevant as you work with data on the blockchain, as you'll ne
```
cy.hardhat().then(async (hardhat) => {
cy.visit(`/swap?inputCurrency=ETH&outputCurrency=${USDC_MAINNET.address}`, { ethereum: 'hardhat' })
cy.visit(`/swap?inputCurrency=ETH&outputCurrency=${USDC_MAINNET.address}`)
cy.get('#swap-currency-output .token-amount-input').type('1').should('have.value', '1')
cy.get('#swap-button').click()
cy.contains('Confirm swap').click()
@@ -68,7 +68,7 @@ cy.hardhat().then(async (hardhat) => {
```
```
cy.visit(`/swap?inputCurrency=ETH&outputCurrency=${USDC_MAINNET.address}`, { ethereum: 'hardhat' })
cy.visit(`/swap?inputCurrency=ETH&outputCurrency=${USDC_MAINNET.address}`)
cy.get('#swap-currency-output .token-amount-input').type('1').should('have.value', '1')
cy.get('#swap-button').click()
cy.contains('Confirm swap').click()
@@ -87,7 +87,6 @@ cy.hardhat().then(async (hardhat) => {
### Working with the blockchain (ie hardhat)
Our tests use a local hardhat node to simulate blockchain transactions. This can be accessed with `cy.hardhat().then((hardhat) => ...)`.
Currently, tests using hardhat must opt-in in when they load the page: `cy.visit('/swap', { ethereum: 'hardhat' })`. This will not be necessary once we've totally migrated to hardhat.
By default, automining is turned on, so that any transaction that you send to the blockchain is mined immediately. If you want to assert on intermediate states (between sending a transaction and mining it), you can turn off automining: `cy.hardhat({ automine: false })`.

View File

@@ -9,36 +9,29 @@ describe('Add Liquidity', () => {
})
})
it('loads the two correct tokens', () => {
cy.visit('/add/0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984/0xB4FBF271143F4FBf7B91A5ded31805e42b2208d6/500')
it('loads the token pair', () => {
cy.visit('/add/0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984/ETH/500')
cy.get('#add-liquidity-input-tokena .token-symbol-container').should('contain.text', 'UNI')
cy.get('#add-liquidity-input-tokenb .token-symbol-container').should('contain.text', 'ETH')
cy.contains('0.05% fee tier')
})
it('does not crash if ETH is duplicated', () => {
cy.visit('/add/0xB4FBF271143F4FBf7B91A5ded31805e42b2208d6/0xB4FBF271143F4FBf7B91A5ded31805e42b2208d6')
cy.get('#add-liquidity-input-tokena .token-symbol-container').should('contain.text', 'ETH')
cy.get('#add-liquidity-input-tokenb .token-symbol-container').should('not.contain.text', 'ETH')
it('does not crash if token is duplicated', () => {
cy.visit('/add/0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984/0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984')
cy.get('#add-liquidity-input-tokena .token-symbol-container').should('contain.text', 'UNI')
cy.get('#add-liquidity-input-tokenb .token-symbol-container').should('not.contain.text', 'UNI')
})
it.skip('token not in storage is loaded', () => {
cy.visit('/add/0x07865c6e87b9f70255377e024ace6630c1eaa37f/0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984')
cy.get('#add-liquidity-input-tokena .token-symbol-container').should('contain.text', 'USDC')
cy.get('#add-liquidity-input-tokenb .token-symbol-container').should('contain.text', 'UNI')
})
it.skip('single token can be selected', () => {
cy.visit('/add/0x07865c6e87b9f70255377e024ace6630c1eaa37f')
cy.get('#add-liquidity-input-tokena .token-symbol-container').should('contain.text', 'USDC')
it('single token can be selected', () => {
cy.visit('/add/0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984')
cy.get('#add-liquidity-input-tokena .token-symbol-container').should('contain.text', 'UNI')
})
it.skip('loads fee tier distribution', () => {
it('loads fee tier distribution', () => {
cy.fixture('feeTierDistribution.json').then((feeTierDistribution) => {
cy.intercept('POST', '/subgraphs/name/uniswap/uniswap-v3', (req: CyHttpMessages.IncomingHttpRequest) => {
if (hasQuery(req, 'FeeTierDistributionQuery')) {
req.alias = 'FeeTierDistributionQuery'
if (hasQuery(req, 'FeeTierDistribution')) {
req.alias = 'FeeTierDistribution'
req.reply({
body: {
@@ -53,12 +46,57 @@ describe('Add Liquidity', () => {
}
})
cy.visit('/add/0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984/0xB4FBF271143F4FBf7B91A5ded31805e42b2208d6')
cy.wait('@FeeTierDistributionQuery')
cy.visit('/add/0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984/ETH')
cy.wait('@FeeTierDistribution')
cy.get('#add-liquidity-selected-fee .selected-fee-label').should('contain.text', '0.3% fee tier')
cy.get('#add-liquidity-selected-fee .selected-fee-percentage').should('contain.text', '40%')
cy.get('#add-liquidity-selected-fee .selected-fee-percentage').should('contain.text', '40% select')
})
})
it('disables increment and decrement until initial prices are inputted', () => {
// ETH / BITCOIN pool (0.05% tier not created)
cy.visit('/add/ETH/0x72e4f9F808C49A2a61dE9C5896298920Dc4EEEa9/500')
// Set starting price in order to enable price range step counters
cy.get('.start-price-input').type('1000')
// Min Price increment / decrement buttons should be disabled
cy.get('[data-testid="increment-price-range"]').eq(0).should('be.disabled')
cy.get('[data-testid="decrement-price-range"]').eq(0).should('be.disabled')
// Enter min price, which should enable the buttons
cy.get('.rate-input-0').eq(0).type('900').blur()
cy.get('[data-testid="increment-price-range"]').eq(0).should('not.be.disabled')
cy.get('[data-testid="decrement-price-range"]').eq(0).should('not.be.disabled')
// Repeat for Max Price step counter
cy.get('[data-testid="increment-price-range"]').eq(1).should('be.disabled')
cy.get('[data-testid="decrement-price-range"]').eq(1).should('be.disabled')
// Enter max price, which should enable the buttons
cy.get('.rate-input-0').eq(1).type('1100').blur()
cy.get('[data-testid="increment-price-range"]').eq(1).should('not.be.disabled')
cy.get('[data-testid="decrement-price-range"]').eq(1).should('not.be.disabled')
})
it('allows full range selection on new pool creation', () => {
// ETH / BITCOIN pool (0.05% tier not created)
cy.visit('/add/ETH/0x72e4f9F808C49A2a61dE9C5896298920Dc4EEEa9/500')
// Set starting price in order to enable price range step counters
cy.get('.start-price-input').type('1000')
cy.get('[data-testid="set-full-range"]').click()
// Check that the min price is 0 and the max price is infinity
cy.get('.rate-input-0').eq(0).should('have.value', '0')
cy.get('.rate-input-0').eq(1).should('have.value', '∞')
// Increment and decrement buttons are disabled when full range is selected
cy.get('[data-testid="increment-price-range"]').eq(0).should('be.disabled')
cy.get('[data-testid="decrement-price-range"]').eq(0).should('be.disabled')
cy.get('[data-testid="increment-price-range"]').eq(1).should('be.disabled')
cy.get('[data-testid="decrement-price-range"]').eq(1).should('be.disabled')
// Check that url params were added
cy.url().then((url) => {
const params = new URLSearchParams(url)
const minPrice = params.get('minPrice')
const maxPrice = params.get('maxPrice')
// Note: although 0 and ∞ displayed, actual values in query are ticks at limit
return minPrice && maxPrice && parseFloat(minPrice) < parseFloat(maxPrice)
})
})
})

View File

@@ -1,8 +1,9 @@
import { FeatureFlag } from '../../src/featureFlags'
import { getTestSelector } from '../utils'
describe('Buy Crypto Modal', () => {
it('should open and close', () => {
cy.visit('/')
cy.visit('/', { featureFlags: [FeatureFlag.fiatOnRampButtonOnSwap] })
// Open the fiat onramp modal
cy.get(getTestSelector('buy-fiat-button')).click()
@@ -15,7 +16,7 @@ describe('Buy Crypto Modal', () => {
it('should open and close, mobile viewport', () => {
cy.viewport('iphone-6')
cy.visit('/')
cy.visit('/', { featureFlags: [FeatureFlag.fiatOnRampButtonOnSwap] })
// Open the fiat onramp modal
cy.get(getTestSelector('buy-fiat-button')).click()

View File

@@ -1,9 +1,9 @@
import { getTestSelector } from '../utils'
import { CONNECTED_WALLET_USER_STATE } from '../utils/user-state'
import { CONNECTED_WALLET_USER_STATE, DISCONNECTED_WALLET_USER_STATE } from '../utils/user-state'
describe('Landing Page', () => {
it('shows landing page when no user state exists', () => {
cy.visit('/', { userState: {} })
cy.visit('/', { userState: DISCONNECTED_WALLET_USER_STATE })
cy.get(getTestSelector('landing-page'))
cy.screenshot()
})

View File

@@ -3,7 +3,7 @@ import { getTestSelector } from '../../utils'
describe('Mini Portfolio account drawer', () => {
beforeEach(() => {
cy.intercept(/api.uniswap.org\/v1\/graphql/, cy.spy().as('gqlSpy'))
cy.visit('/swap', { ethereum: 'hardhat' })
cy.visit('/swap')
})
it('fetches balances when account button is first hovered', () => {
@@ -41,6 +41,7 @@ describe('Mini Portfolio account drawer', () => {
cy.get(getTestSelector('mini-portfolio-navbar')).contains('NFTs').click()
cy.get(getTestSelector('mini-portfolio-page')).contains('I Got Plenty')
cy.intercept(/graphql/, { fixture: 'mini-portfolio/pools.json' })
cy.get(getTestSelector('mini-portfolio-navbar')).contains('Pools').click()
cy.get(getTestSelector('mini-portfolio-page')).contains('No pools yet')
@@ -76,4 +77,36 @@ describe('Mini Portfolio account drawer', () => {
})
})
})
it('fetches ENS name', () => {
cy.hardhat().then(() => {
const haydenAccount = '0x50EC05ADe8280758E2077fcBC08D878D4aef79C3'
const haydenENS = 'hayden.eth'
// Opens the account drawer
cy.get(getTestSelector('web3-status-connected')).click()
// Simulate wallet changing to Hayden's account
cy.window().then((win) => win.ethereum.emit('accountsChanged', [haydenAccount]))
// Hayden's ENS name should be shown
cy.contains(haydenENS).should('exist')
// Close account drawer
cy.get(getTestSelector('close-account-drawer')).click()
// Switch chain to Polygon
cy.get(getTestSelector('chain-selector')).eq(1).click()
cy.contains('Polygon').click()
//Reopen account drawer
cy.get(getTestSelector('web3-status-connected')).click()
// Simulate wallet changing to Hayden's account
cy.window().then((win) => win.ethereum.emit('accountsChanged', [haydenAccount]))
// Hayden's ENS name should be shown
cy.contains(haydenENS).should('exist')
})
})
})

View File

@@ -94,9 +94,7 @@ describe('mini-portfolio activity history', () => {
})
it('should deduplicate activity history by nonce', () => {
cy.visit(`/swap?inputCurrency=ETH&outputCurrency=${USDC_MAINNET.address}`, { ethereum: 'hardhat' }).hardhat({
automine: false,
})
cy.visit(`/swap?inputCurrency=ETH&outputCurrency=${USDC_MAINNET.address}`).hardhat({ automine: false })
// Input swap info.
cy.get('#swap-currency-input .token-amount-input').clear().type('1').should('have.value', '1')

View File

@@ -1,7 +1,6 @@
import { getTestSelector } from '../utils'
const PUDGY_COLLECTION_ADDRESS = '0xbd3531da5cf5857e7cfaa92426877b022e612cf8'
const BONSAI_COLLECTION_ADDRESS = '0xec9c519d49856fd2f8133a0741b4dbe002ce211b'
describe('Testing nfts', () => {
it('should load nft leaderboard', () => {
@@ -38,7 +37,10 @@ describe('Testing nfts', () => {
})
it('should toggle buy now on details page', () => {
cy.visit(`#/nfts/asset/${BONSAI_COLLECTION_ADDRESS}/7580`)
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-collection-asset')).first().click()
cy.get(getTestSelector('nft-details-description-text')).should('exist')
cy.get(getTestSelector('nft-details-description')).click()
cy.get(getTestSelector('nft-details-description-text')).should('not.exist')
@@ -50,7 +52,7 @@ describe('Testing nfts', () => {
cy.visit('/')
cy.get(getTestSelector('web3-status-connected')).click()
cy.get(getTestSelector('mini-portfolio-navbar')).contains('NFTs').click()
cy.get(getTestSelector('mini-portfolio-nft')).click()
cy.get(getTestSelector('mini-portfolio-nft')).first().click()
cy.get(getTestSelector('mini-portfolio-navbar')).should('not.be.visible')
})
})

View File

@@ -17,9 +17,7 @@ function initiateSwap() {
describe('Permit2', () => {
function setupInputs(inputToken: Token, outputToken: Token) {
// Sets up a swap between inputToken and outputToken.
cy.visit(`/swap/?inputCurrency=${inputToken.address}&outputCurrency=${outputToken.address}`, {
ethereum: 'hardhat',
})
cy.visit(`/swap/?inputCurrency=${inputToken.address}&outputCurrency=${outputToken.address}`)
cy.get('#swap-currency-input .token-amount-input').type('0.01')
}
@@ -28,15 +26,19 @@ describe('Permit2', () => {
// check token approval
cy.hardhat()
.then(({ approval, wallet }) => approval.getTokenAllowanceForPermit2({ owner: wallet, token: inputToken }))
.should('deep.equal', MaxUint256)
.then((allowance) => {
Cypress.log({ name: `Token allowance: ${allowance.toString()}` })
cy.wrap(allowance).should('deep.equal', MaxUint256)
})
}
/** Asserts the universal router has a max permit2 approval for spend of the input token on-chain. */
function expectPermit2AllowanceForUniversalRouterToBeMax(inputToken: Token) {
cy.hardhat()
.then((hardhat) => hardhat.approval.getPermit2Allowance({ owner: hardhat.wallet, token: inputToken }))
.then(({ approval, wallet }) => approval.getPermit2Allowance({ owner: wallet, token: inputToken }))
.then((allowance) => {
cy.wrap(MaxUint160.eq(allowance.amount)).should('eq', true)
Cypress.log({ name: `Permit2 allowance: ${allowance.amount.toString()}` })
cy.wrap(allowance.amount).should('deep.equal', MaxUint160)
// Asserts that the on-chain expiration is in 30 days, within a tolerance of 40 seconds.
const THIRTY_DAYS_SECONDS = 2_592_000
const expected = Math.floor(Date.now() / 1000 + THIRTY_DAYS_SECONDS)
@@ -44,6 +46,13 @@ describe('Permit2', () => {
})
}
beforeEach(() =>
cy.hardhat().then(async (hardhat) => {
await hardhat.fund(hardhat.wallet, CurrencyAmount.fromRawAmount(DAI, 1e18))
await hardhat.mine()
})
)
describe('approval process (with intermediate screens)', () => {
// Turn off automine so that intermediate screens are available to assert on.
beforeEach(() => cy.hardhat({ automine: false }))
@@ -66,8 +75,9 @@ describe('Permit2', () => {
cy.contains('Allow DAI to be used for swapping')
cy.wait('@eth_signTypedData_v4')
cy.wait('@eth_sendRawTransaction')
cy.contains('Swap submitted')
cy.hardhat().then((hardhat) => hardhat.mine())
cy.contains('Success')
cy.contains('Swap success!')
cy.get(getTestSelector('popups')).contains('Swapped')
expectPermit2AllowanceForUniversalRouterToBeMax(DAI)
})
@@ -90,7 +100,7 @@ describe('Permit2', () => {
// Verify transaction
cy.wait('@eth_sendRawTransaction')
cy.hardhat().then((hardhat) => hardhat.mine())
cy.contains('Success')
cy.contains('Swap success!')
cy.get(getTestSelector('popups')).contains('Swapped')
})
@@ -133,7 +143,7 @@ describe('Permit2', () => {
// Verify transaction
cy.wait('@eth_sendRawTransaction')
cy.hardhat().then((hardhat) => hardhat.mine())
cy.contains('Success')
cy.contains('Swap success!')
cy.get(getTestSelector('popups')).contains('Swapped')
})
})
@@ -149,7 +159,7 @@ describe('Permit2', () => {
initiateSwap()
// Verify transaction
cy.contains('Success')
cy.contains('Swap success!')
cy.get(getTestSelector('popups')).contains('Swapped')
})
@@ -188,7 +198,7 @@ describe('Permit2', () => {
cy.contains('Confirm swap').click()
// Verify permit2 approval
cy.contains('Success')
cy.contains('Swap success!')
cy.get(getTestSelector('popups')).contains('Swapped')
expectPermit2AllowanceForUniversalRouterToBeMax(DAI)
})
@@ -222,7 +232,7 @@ describe('Permit2', () => {
// Verify permit2 approval
cy.wait('@eth_signTypedData_v4')
cy.contains('Success')
cy.contains('Swap success!')
cy.get(getTestSelector('popups')).contains('Swapped')
expectPermit2AllowanceForUniversalRouterToBeMax(DAI)
})
@@ -240,7 +250,7 @@ describe('Permit2', () => {
// Verify permit2 approval
cy.wait('@eth_signTypedData_v4')
cy.contains('Success')
cy.contains('Swap success!')
cy.get(getTestSelector('popups')).contains('Swapped')
expectPermit2AllowanceForUniversalRouterToBeMax(DAI)
})

View File

@@ -1,25 +1,7 @@
describe('Remove Liquidity', () => {
it('eth remove', () => {
it('loads the token pair', () => {
cy.visit('/remove/v2/ETH/0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984')
cy.get('#remove-liquidity-tokena-symbol').should('contain.text', 'ETH')
cy.get('#remove-liquidity-tokenb-symbol').should('contain.text', 'UNI')
})
it('eth remove swap order', () => {
cy.visit('/remove/v2/0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984/ETH')
cy.get('#remove-liquidity-tokena-symbol').should('contain.text', 'UNI')
cy.get('#remove-liquidity-tokenb-symbol').should('contain.text', 'ETH')
})
it('loads the two correct tokens', () => {
cy.visit('/remove/v2/0xB4FBF271143F4FBf7B91A5ded31805e42b2208d6/0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984')
cy.get('#remove-liquidity-tokena-symbol').should('contain.text', 'WETH')
cy.get('#remove-liquidity-tokenb-symbol').should('contain.text', 'UNI')
})
it('does not crash if ETH is duplicated', () => {
cy.visit('/remove/v2/0xB4FBF271143F4FBf7B91A5ded31805e42b2208d6/0xB4FBF271143F4FBf7B91A5ded31805e42b2208d6')
cy.get('#remove-liquidity-tokena-symbol').should('contain.text', 'WETH')
cy.get('#remove-liquidity-tokenb-symbol').should('contain.text', 'WETH')
})
})

View File

@@ -1,15 +1,13 @@
import { BigNumber } from '@ethersproject/bignumber'
import { SupportedChainId } from '@uniswap/sdk-core'
import { CurrencyAmount } from '@uniswap/sdk-core'
import { DEFAULT_DEADLINE_FROM_NOW } from '../../../src/constants/misc'
import { UNI, USDC_MAINNET } from '../../../src/constants/tokens'
import { DAI, USDC_MAINNET } from '../../../src/constants/tokens'
import { getBalance, getTestSelector } from '../../utils'
const UNI_MAINNET = UNI[SupportedChainId.MAINNET]
describe('Swap errors', () => {
it('wallet rejection', () => {
cy.visit(`/swap?inputCurrency=ETH&outputCurrency=${USDC_MAINNET.address}`, { ethereum: 'hardhat' })
cy.visit(`/swap?inputCurrency=ETH&outputCurrency=${USDC_MAINNET.address}`)
cy.hardhat().then((hardhat) => {
// Stub the wallet to reject any transaction.
cy.stub(hardhat.wallet, 'sendTransaction').log(false).rejects(new Error('user cancelled'))
@@ -30,7 +28,7 @@ describe('Swap errors', () => {
})
it('transaction past deadline', () => {
cy.visit(`/swap?inputCurrency=ETH&outputCurrency=${USDC_MAINNET.address}`, { ethereum: 'hardhat' })
cy.visit(`/swap?inputCurrency=ETH&outputCurrency=${USDC_MAINNET.address}`)
cy.hardhat({ automine: false })
getBalance(USDC_MAINNET).then((initialBalance) => {
// Enter amount to swap
@@ -41,7 +39,7 @@ describe('Swap errors', () => {
cy.get('#swap-button').click()
cy.contains('Confirm swap').click()
cy.wait('@eth_estimateGas').wait('@eth_sendRawTransaction').wait('@eth_getTransactionReceipt')
cy.contains('Transaction submitted')
cy.contains('Swap submitted')
cy.get(getTestSelector('confirmation-close-icon')).click()
cy.get(getTestSelector('web3-status-connected')).should('contain', '1 Pending')
@@ -64,10 +62,19 @@ describe('Swap errors', () => {
})
})
it.skip('slippage failure', () => {
cy.visit(`/swap?inputCurrency=ETH&outputCurrency=${UNI_MAINNET.address}`, { ethereum: 'hardhat' })
cy.hardhat({ automine: false })
getBalance(USDC_MAINNET).then((initialBalance) => {
it('slippage failure', () => {
cy.visit(`/swap?inputCurrency=${USDC_MAINNET.address}&outputCurrency=${DAI.address}`)
cy.hardhat({ automine: false }).then(async (hardhat) => {
await hardhat.fund(hardhat.wallet, CurrencyAmount.fromRawAmount(USDC_MAINNET, 500e6))
await hardhat.mine()
await Promise.all([
hardhat.approval.setTokenAllowanceForPermit2({ owner: hardhat.wallet, token: USDC_MAINNET }),
hardhat.approval.setPermit2Allowance({ owner: hardhat.wallet, token: USDC_MAINNET }),
])
await hardhat.mine()
})
getBalance(DAI).then((initialBalance) => {
// Gas estimation fails for this transaction (that would normally fail), so we stub it.
cy.hardhat().then((hardhat) => {
const send = cy.stub(hardhat.provider, 'send').log(false)
@@ -89,8 +96,10 @@ describe('Swap errors', () => {
cy.get('#swap-button').click()
cy.contains('Confirm swap').click()
cy.wait('@eth_sendRawTransaction').wait('@eth_getTransactionReceipt')
cy.contains('Transaction submitted')
cy.get(getTestSelector('confirmation-close-icon')).click()
cy.contains('Swap submitted')
if (i === 0) {
cy.get(getTestSelector('confirmation-close-icon')).click()
}
}
cy.get(getTestSelector('web3-status-connected')).should('contain', '2 Pending')
@@ -98,10 +107,13 @@ describe('Swap errors', () => {
cy.hardhat().then((hardhat) => hardhat.mine())
cy.wait('@eth_getTransactionReceipt')
// Verify transaction did not occur
cy.contains('Swap failed')
// Verify only 1 transaction occurred
cy.get(getTestSelector('web3-status-connected')).should('not.contain', 'Pending')
cy.get(getTestSelector('popups')).contains('Swapped')
cy.get(getTestSelector('popups')).contains('Swap failed')
getBalance(UNI_MAINNET).should('eq', initialBalance)
getBalance(DAI).should('be.closeTo', initialBalance + 200, 1)
})
})
})

View File

@@ -1,13 +1,15 @@
import { FeatureFlag } from '../../../src/featureFlags'
import { getTestSelector } from '../../utils'
describe('Swap settings', () => {
it('Opens and closes the settings menu', () => {
cy.visit('/swap')
cy.visit('/swap', { featureFlags: [FeatureFlag.uniswapXEnabled] })
cy.contains('Settings').should('not.exist')
cy.get(getTestSelector('open-settings-dialog-button')).click()
cy.contains('Max slippage').should('exist')
cy.contains('Transaction deadline').should('exist')
cy.contains('Auto Router API').should('exist')
cy.contains('UniswapX').should('exist')
cy.contains('Local routing').should('exist')
cy.get(getTestSelector('open-settings-dialog-button')).click()
cy.contains('Settings').should('not.exist')
})

View File

@@ -1,9 +1,9 @@
import { SupportedChainId } from '@uniswap/sdk-core'
import { ChainId } from '@uniswap/sdk-core'
import { UNI, USDC_MAINNET } from '../../../src/constants/tokens'
import { getBalance, getTestSelector } from '../../utils'
const UNI_MAINNET = UNI[SupportedChainId.MAINNET]
const UNI_MAINNET = UNI[ChainId.MAINNET]
describe('Swap', () => {
describe('Swap on main page', () => {
@@ -52,13 +52,13 @@ describe('Swap', () => {
})
it('swaps ETH for USDC', () => {
cy.visit('/swap', { ethereum: 'hardhat' })
cy.visit('/swap')
cy.hardhat({ automine: false })
getBalance(USDC_MAINNET).then((initialBalance) => {
// Select USDC
cy.get('#swap-currency-output .open-currency-select-button').click()
cy.get(getTestSelector('token-search-input')).type(USDC_MAINNET.address)
cy.contains('USDC').click()
cy.get(getTestSelector('common-base-USDC')).click()
// Enter amount to swap
cy.get('#swap-currency-output .token-amount-input').type('1').should('have.value', '1')
@@ -69,9 +69,9 @@ describe('Swap', () => {
cy.contains('Review swap')
cy.contains('Confirm swap').click()
cy.wait('@eth_estimateGas').wait('@eth_sendRawTransaction').wait('@eth_getTransactionReceipt')
cy.contains('Transaction submitted')
cy.contains('Swap submitted')
cy.get(getTestSelector('confirmation-close-icon')).click()
cy.contains('Transaction submitted').should('not.exist')
cy.contains('Swap submitted').should('not.exist')
cy.get(getTestSelector('web3-status-connected')).should('contain', '1 Pending')
// Mine transaction

View File

@@ -0,0 +1,45 @@
import { SwapEventName } from '@uniswap/analytics-events'
import { USDC_MAINNET } from '../../../src/constants/tokens'
import { getTestSelector } from '../../utils'
describe('time-to-swap logging', () => {
it('completes two swaps and verifies the TTS logging for the first', () => {
cy.visit(`/swap?inputCurrency=ETH&outputCurrency=${USDC_MAINNET.address}`)
cy.hardhat()
// First swap in the session:
// Enter amount to swap
cy.get('#swap-currency-output .token-amount-input').type('1').should('have.value', '1')
cy.get('#swap-currency-input .token-amount-input').should('not.have.value', '')
// Submit transaction
cy.get('#swap-button').click()
cy.contains('Confirm swap').click()
cy.get(getTestSelector('confirmation-close-icon')).click()
cy.get(getTestSelector('popups')).contains('Swapped')
// Verify logging
cy.waitForAmplitudeEvent(SwapEventName.SWAP_TRANSACTION_COMPLETED).then((event: any) => {
cy.wrap(event.event_properties).should('have.property', 'time_to_swap')
cy.wrap(event.event_properties.time_to_swap).should('be.a', 'number')
cy.wrap(event.event_properties.time_to_swap).should('be.gte', 0)
})
// Second swap in the session:
// Enter amount to swap
cy.get('#swap-currency-output .token-amount-input').clear().type('1').should('have.value', '1')
cy.get('#swap-currency-input .token-amount-input').should('not.have.value', '')
// Submit transaction
cy.get('#swap-button').click()
cy.contains('Confirm swap').click()
cy.get(getTestSelector('confirmation-close-icon')).click()
cy.get(getTestSelector('popups')).contains('Swapped')
cy.waitForAmplitudeEvent(SwapEventName.SWAP_TRANSACTION_COMPLETED).then((event: any) => {
cy.wrap(event.event_properties).should('not.have.property', 'time_to_swap')
})
})
})

View File

@@ -1,14 +1,12 @@
import { CurrencyAmount, SupportedChainId, WETH9 } from '@uniswap/sdk-core'
import { ChainId, CurrencyAmount, WETH9 } from '@uniswap/sdk-core'
import { getBalance, getTestSelector } from '../../utils'
const WETH = WETH9[SupportedChainId.MAINNET]
const WETH = WETH9[ChainId.MAINNET]
describe('Swap wrap', () => {
beforeEach(() => {
cy.visit(`/swap?inputCurrency=ETH&outputCurrency=${WETH.address}`, { ethereum: 'hardhat' }).hardhat({
automine: false,
})
cy.visit(`/swap?inputCurrency=ETH&outputCurrency=${WETH.address}`).hardhat({ automine: false })
})
it('ETH to wETH is same value (wrapped swaps have no price impact)', () => {

View File

@@ -1,9 +1,9 @@
import { SupportedChainId, WETH9 } from '@uniswap/sdk-core'
import { ChainId, WETH9 } from '@uniswap/sdk-core'
import { UNI } from '../../src/constants/tokens'
import { ARB, UNI } from '../../src/constants/tokens'
import { getTestSelector } from '../utils'
const UNI_MAINNET = UNI[SupportedChainId.MAINNET]
const UNI_MAINNET = UNI[ChainId.MAINNET]
const UNI_ADDRESS = '0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984'
@@ -93,9 +93,7 @@ describe('Token details', () => {
beforeEach(() => {
// On mobile widths, we just link back to /swap instead of rendering the swap component.
cy.viewport(1200, 800)
cy.visit(`/tokens/ethereum/${UNI_MAINNET.address}`, {
ethereum: 'hardhat',
}).then(() => {
cy.visit(`/tokens/ethereum/${UNI_MAINNET.address}`).then(() => {
cy.wait('@eth_blockNumber')
cy.scrollTo('top')
})
@@ -145,11 +143,11 @@ describe('Token details', () => {
})
it('should show a L2 token even if the user is connected to a different network', () => {
cy.visit('/tokens', { ethereum: 'hardhat' })
cy.visit('/tokens')
cy.get(getTestSelector('tokens-network-filter-selected')).click()
cy.get(getTestSelector('tokens-network-filter-option-arbitrum')).click()
cy.get(getTestSelector('tokens-network-filter-selected')).should('contain', 'Arbitrum')
cy.get(getTestSelector('token-table-row-ARB')).click()
cy.get(getTestSelector(`token-table-row-${ARB.address.toLowerCase()}`)).click()
cy.get(`#swap-currency-output .token-symbol-container`).should('contain.text', 'ARB')
cy.get(getTestSelector('open-settings-dialog-button')).should('be.disabled')
cy.contains('Connect to Arbitrum').should('exist')

View File

@@ -10,11 +10,11 @@ describe('Token explore', () => {
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')
cy.get(getTestSelector('token-table-row-ETH')).find(getTestSelector('volume-cell')).should('include.text', '$')
cy.get(getTestSelector('token-table-row-ETH')).find(getTestSelector('price-cell')).should('include.text', '$')
cy.get(getTestSelector('token-table-row-ETH')).find(getTestSelector('tvl-cell')).should('include.text', '$')
cy.get(getTestSelector('token-table-row-ETH'))
cy.get(getTestSelector('token-table-row-NATIVE')).find(getTestSelector('name-cell')).should('include.text', 'Ether')
cy.get(getTestSelector('token-table-row-NATIVE')).find(getTestSelector('volume-cell')).should('include.text', '$')
cy.get(getTestSelector('token-table-row-NATIVE')).find(getTestSelector('price-cell')).should('include.text', '$')
cy.get(getTestSelector('token-table-row-NATIVE')).find(getTestSelector('tvl-cell')).should('include.text', '$')
cy.get(getTestSelector('token-table-row-NATIVE'))
.find(getTestSelector('percent-change-cell'))
.should('include.text', '%')
cy.get(getTestSelector('header-row')).find(getTestSelector('price-cell')).click()
@@ -24,24 +24,24 @@ describe('Token explore', () => {
it('should update when time window toggled', () => {
cy.visit('/tokens/ethereum')
cy.get(getTestSelector('time-selector')).should('contain', '1D')
cy.get(getTestSelector('token-table-row-ETH'))
cy.get(getTestSelector('token-table-row-NATIVE'))
.find(getTestSelector('volume-cell'))
.then(function ($elem) {
cy.wrap($elem.text()).as('dailyEthVol')
})
cy.get(getTestSelector('time-selector')).click()
cy.get(getTestSelector('1Y')).click()
cy.get(getTestSelector('token-table-row-ETH'))
cy.get(getTestSelector('token-table-row-NATIVE'))
.find(getTestSelector('volume-cell'))
.then(function ($elem) {
cy.wrap($elem.text()).as('yearlyEthVol')
})
expect(cy.get('@dailyEthVol')).to.not.equal(cy.get('@yearlyEthVol'))
cy.get('@dailyEthVol').should('not.equal', cy.get('@yearlyEthVol'))
})
it('should navigate to token detail page when row clicked', () => {
cy.visit('/tokens/ethereum')
cy.get(getTestSelector('token-table-row-ETH')).click()
cy.get(getTestSelector('token-table-row-NATIVE')).click()
cy.get(getTestSelector('token-details-about-section')).should('exist')
cy.get(getTestSelector('token-details-stats')).should('exist')
cy.get(getTestSelector('token-info-container')).should('exist')
@@ -53,13 +53,15 @@ describe('Token explore', () => {
it('should update when global network changed', () => {
cy.visit('/tokens/ethereum')
cy.get(getTestSelector('tokens-network-filter-selected')).should('contain', 'Ethereum')
cy.get(getTestSelector('token-table-row-ETH')).should('exist')
cy.get(getTestSelector('token-table-row-NATIVE')).should('exist')
// note: cannot switch global chain via UI because we cannot approve the network switch
// in metamask modal using plain cypress. this is a workaround.
cy.visit('/tokens/polygon')
cy.get(getTestSelector('tokens-network-filter-selected')).should('contain', 'Polygon')
cy.get(getTestSelector('token-table-row-MATIC')).should('exist')
cy.get(getTestSelector('token-table-row-NATIVE'))
.find(getTestSelector('name-cell'))
.should('include.text', 'Polygon Matic')
})
it('should update when token explore table network changed', () => {

View File

@@ -1,45 +1,53 @@
import { getTestSelector } from '../utils'
describe('Universal search bar', () => {
function openSearch() {
// can't just type "/" because on mobile it doesn't respond to that
cy.get('[data-cy="magnifying-icon"]').parent().eq(1).click()
}
beforeEach(() => {
cy.visit('/')
cy.get('[data-cy="magnifying-icon"]').parent().eq(1).click()
})
it('should yield clickable result for regular token or nft collection search term', () => {
// Search for uni token by name.
cy.get('[data-cy="search-bar-input"]').last().clear().type('uni')
cy.get('[data-cy="searchbar-token-row-UNI"]')
function getSearchBar() {
return cy.get('[data-cy="search-bar-input"]').last()
}
it('should yield clickable result that is then added to recent searches', () => {
// Search for UNI token by name.
openSearch()
getSearchBar().clear().type('uni')
cy.get(getTestSelector('searchbar-token-row-UNI'))
.should('contain.text', 'Uniswap')
.and('contain.text', 'UNI')
.and('contain.text', '$')
.and('contain.text', '%')
cy.get('[data-cy="searchbar-token-row-UNI"]').first().click()
.click()
cy.location('hash').should('equal', '#/tokens/ethereum/0x1f9840a85d5af5bf1d1762f925bdaddc4201f984')
})
it.skip('should show recent tokens and popular tokens with empty search term', () => {
cy.get('[data-cy="magnifying-icon"]')
.parent()
.then(($navIcon) => {
$navIcon.click()
})
// Recently searched UNI token should exist.
cy.get('[data-cy="search-bar-input"]').last().clear()
cy.get('[data-cy="searchbar-dropdown"]')
.contains('[data-cy="searchbar-dropdown"]', 'Recent searches')
.find('[data-cy="searchbar-token-row-UNI"]')
openSearch()
cy.get(getTestSelector('searchbar-dropdown'))
.contains(getTestSelector('searchbar-dropdown'), 'Recent searches')
.find(getTestSelector('searchbar-token-row-UNI'))
.should('exist')
// Most popular 3 tokens should be shown.
cy.get('[data-cy="searchbar-dropdown"]')
.contains('[data-cy="searchbar-dropdown"]', 'Popular tokens')
.find('[data-cy^="searchbar-token-row"]')
.its('length')
.should('be.eq', 3)
})
it.skip('should show blocked badge when blocked token is searched for', () => {
// Search for mTSLA, which is a blocked token.
cy.get('[data-cy="search-bar-input"]').last().clear().type('mtsla')
cy.get('[data-cy="searchbar-token-row-mTSLA"]').find('[data-cy="blocked-icon"]').should('exist')
it('should go to the selected result when recent results are shown', () => {
// Seed recent results with UNI.
openSearch()
getSearchBar().type('uni')
cy.get(getTestSelector('searchbar-token-row-UNI'))
getSearchBar().clear().type('{esc}')
// Search a different token by name.
openSearch()
getSearchBar().type('eth')
cy.get(getTestSelector('searchbar-token-row-ETH'))
// Validate that we go to the searched/selected result.
getSearchBar().type('{enter}')
cy.url().should('contain', 'tokens/ethereum/NATIVE')
})
})

View File

@@ -1,8 +1,9 @@
import { getTestSelector } from '../../utils'
import { DISCONNECTED_WALLET_USER_STATE } from '../../utils/user-state'
describe('disconnect wallet', () => {
it('should clear state', () => {
cy.visit('/swap', { ethereum: 'hardhat' })
cy.visit('/swap')
cy.get('#swap-currency-input .token-amount-input').clear().type('1')
// Verify wallet is connected
@@ -27,7 +28,7 @@ describe('disconnect wallet', () => {
describe('connect wallet', () => {
it('should load state', () => {
cy.visit('/swap', { ethereum: 'hardhat', userState: {} })
cy.visit('/swap', { userState: DISCONNECTED_WALLET_USER_STATE })
// Connect the wallet
cy.get(getTestSelector('navbar-connect-wallet')).contains('Connect').click()

View File

@@ -12,7 +12,7 @@ function switchChain(chain: string) {
describe('network switching', () => {
beforeEach(() => {
cy.visit('/swap', { ethereum: 'hardhat' })
cy.visit('/swap')
cy.get(getTestSelector('web3-status-connected'))
})
@@ -111,6 +111,7 @@ describe('network switching', () => {
cy.wait('@wallet_switchEthereumChain')
waitsForActiveChain('Polygon')
cy.get(getTestSelector('web3-status-connected'))
cy.url().should('contain', 'chain=polygon')
// Verify that the input/output fields were reset
cy.get('#swap-currency-input .token-amount-input').should('have.value', '')
@@ -119,3 +120,25 @@ describe('network switching', () => {
cy.get(`#swap-currency-output .token-symbol-container`).should('contain.text', 'Select token')
})
})
describe('network switching from URL param', () => {
it('should switch network from URL param', () => {
cy.visit('/swap?chain=polygon')
cy.get(getTestSelector('web3-status-connected'))
cy.wait('@wallet_switchEthereumChain')
waitsForActiveChain('Polygon')
})
it('should be able to switch network after loading from URL param', () => {
cy.visit('/swap?chain=polygon')
cy.get(getTestSelector('web3-status-connected'))
cy.wait('@wallet_switchEthereumChain')
waitsForActiveChain('Polygon')
// switching to another chain clears query param
switchChain('Ethereum')
cy.wait('@wallet_switchEthereumChain')
waitsForActiveChain('Ethereum')
cy.url().should('not.contain', 'chain=polygon')
})
})

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
{}

View File

@@ -0,0 +1,19 @@
import { getTestSelector } from '../utils'
describe('translations', () => {
it('loads locale from the query param', () => {
cy.visit('/?lng=fr-FR')
cy.contains('Échanger')
cy.contains('Uniswap disponible en : English')
})
it('loads locale from menu', () => {
cy.visit('/')
cy.get(getTestSelector('web3-status-connected')).click()
cy.get(getTestSelector('wallet-settings')).click()
cy.get(getTestSelector('wallet-language-item')).contains('français').click({ force: true })
cy.location('hash').should('match', /\?lng=fr-FR$/)
cy.contains('Échanger')
cy.contains('Uniswap disponible en : English')
})
})

View File

@@ -5,7 +5,6 @@ import { Eip1193Bridge } from '@ethersproject/experimental/lib/eip1193-bridge'
import { FeatureFlag } from '../../src/featureFlags'
import { UserState } from '../../src/state/user/reducer'
import { CONNECTED_WALLET_USER_STATE } from '../utils/user-state'
import { injected } from './ethereum'
declare global {
// eslint-disable-next-line @typescript-eslint/no-namespace
@@ -13,15 +12,19 @@ declare global {
interface ApplicationWindow {
ethereum: Eip1193Bridge
}
interface Chainable<Subject> {
/**
* Wait for a specific event to be sent to amplitude. If the event is found, the subject will be the event.
*
* @param {string} eventName - The type of the event to search for e.g. SwapEventName.SWAP_TRANSACTION_COMPLETED
* @param {number} timeout - The maximum amount of time (in ms) to wait for the event.
* @returns {Chainable<Subject>}
*/
waitForAmplitudeEvent(eventName: string, timeout?: number): Chainable<Subject>
}
interface VisitOptions {
serviceWorker?: true
featureFlags?: Array<FeatureFlag>
/**
* The mock ethereum provider to inject into the page.
* @default 'goerli'
*/
// TODO(INFRA-175): Migrate all usage of 'goerli' to 'hardhat'.
ethereum?: 'goerli' | 'hardhat'
/**
* Initial user state.
* @default {@type import('../utils/user-state').CONNECTED_WALLET_USER_STATE}
@@ -39,8 +42,7 @@ Cypress.Commands.overwrite(
if (typeof url !== 'string') throw new Error('Invalid arguments. The first argument to cy.visit must be the path.')
// Add a hash in the URL if it is not present (to use hash-based routing correctly with queryParams).
let hashUrl = url.startsWith('/') && url.length > 2 && !url.startsWith('/#') ? `/#${url}` : url
if (options?.ethereum === 'goerli') hashUrl += `${url.includes('?') ? '&' : '?'}chain=goerli`
const hashUrl = url.startsWith('/') && url.length > 2 && !url.startsWith('/#') ? `/#${url}` : url
return cy
.intercept('/service-worker.js', options?.serviceWorker ? undefined : { statusCode: 404 })
@@ -58,7 +60,7 @@ Cypress.Commands.overwrite(
// Set initial user state.
win.localStorage.setItem(
'redux_localstorage_simple_user', // storage key for the user reducer using 'redux-localstorage-simple'
JSON.stringify(options?.userState ?? CONNECTED_WALLET_USER_STATE)
JSON.stringify({ ...CONNECTED_WALLET_USER_STATE, ...(options?.userState ?? {}) })
)
// Set feature flags, if configured.
@@ -68,13 +70,29 @@ Cypress.Commands.overwrite(
}
// Inject the mock ethereum provider.
if (options?.ethereum === 'hardhat') {
win.ethereum = provider
} else {
win.ethereum = injected
}
win.ethereum = provider
},
})
)
}
)
Cypress.Commands.add('waitForAmplitudeEvent', (eventName, timeout = 5000 /* 5s */) => {
const startTime = new Date().getTime()
function checkRequest() {
return cy.wait('@amplitude', { timeout }).then((interception) => {
const events = interception.request.body.events
const event = events.find((event: any) => event.event_type === eventName)
if (event) {
return cy.wrap(event)
} else if (new Date().getTime() - startTime > timeout) {
throw new Error(`Event ${eventName} not found within the specified timeout`)
} else {
return checkRequest()
}
})
}
return checkRequest()
})

View File

@@ -1,74 +0,0 @@
/**
* Updates cy.visit() to include an injected window.ethereum provider.
*/
import { Eip1193Bridge } from '@ethersproject/experimental/lib/eip1193-bridge'
// eslint-disable-next-line @typescript-eslint/no-restricted-imports
import { JsonRpcProvider } from '@ethersproject/providers'
import { Wallet } from '@ethersproject/wallet'
import { SupportedChainId } from '../../src/constants/chains'
// todo: figure out how env vars actually work in CI
// const TEST_PRIVATE_KEY = Cypress.env('INTEGRATION_TEST_PRIVATE_KEY')
const TEST_PRIVATE_KEY = '0xe580410d7c37d26c6ad1a837bbae46bc27f9066a466fb3a66e770523b4666d19'
// address of the above key
const TEST_ADDRESS_NEVER_USE = new Wallet(TEST_PRIVATE_KEY).address
const CHAIN_ID = SupportedChainId.GOERLI
const HEXLIFIED_CHAIN_ID = `0x${CHAIN_ID.toString(16)}`
const provider = new JsonRpcProvider('https://goerli.infura.io/v3/4bf032f2d38a4ed6bb975b80d6340847', 5)
const signer = new Wallet(TEST_PRIVATE_KEY, provider)
export const injected = new (class extends Eip1193Bridge {
chainId = CHAIN_ID
async sendAsync(...args: any[]) {
console.debug('sendAsync called', ...args)
return this.send(...args)
}
async send(...args: any[]) {
console.debug('send called', ...args)
const isCallbackForm = typeof args[0] === 'object' && typeof args[1] === 'function'
let callback
let method
let params
if (isCallbackForm) {
callback = args[1]
method = args[0].method
params = args[0].params
} else {
method = args[0]
params = args[1]
}
if (method === 'eth_requestAccounts' || method === 'eth_accounts') {
if (isCallbackForm) {
callback({ result: [TEST_ADDRESS_NEVER_USE] })
} else {
return Promise.resolve([TEST_ADDRESS_NEVER_USE])
}
}
if (method === 'eth_chainId') {
if (isCallbackForm) {
callback(null, { result: HEXLIFIED_CHAIN_ID })
} else {
return Promise.resolve(HEXLIFIED_CHAIN_ID)
}
}
try {
const result = await super.send(method, params)
console.debug('result received', method, params, result)
if (isCallbackForm) {
callback(null, { result })
} else {
return result
}
} catch (error) {
if (isCallbackForm) {
callback(error, null)
} else {
throw error
}
}
}
})(signer, provider)

View File

@@ -9,13 +9,8 @@ beforeEach(() => {
req.headers['origin'] = 'https://app.uniswap.org'
})
// Infura uses a test endpoint, which allow-lists http://localhost:3000 instead.
cy.intercept(/infura.io/, (req) => {
req.headers['referer'] = 'http://localhost:3000'
req.headers['origin'] = 'http://localhost:3000'
req.alias = req.body.method
req.continue()
})
// Infura is disabled for cypress tests - calls should be routed through the connected wallet instead.
cy.intercept(/infura.io/, { statusCode: 404 })
// Log requests to hardhat.
cy.intercept(/:8545/, logJsonRpc)
@@ -24,6 +19,7 @@ beforeEach(() => {
cy.intercept('https://api.uniswap.org/v1/amplitude-proxy', (req) => {
const requestBody = JSON.stringify(req.body)
const byteSize = new Blob([requestBody]).size
req.alias = 'amplitude'
req.reply(
JSON.stringify({
code: 200,
@@ -49,7 +45,7 @@ function logJsonRpc(req: CyHttpMessages.IncomingHttpRequest) {
const log = Cypress.log({
autoEnd: false,
name: req.body.method,
message: req.body.params?.map((param: unknown) =>
message: req.body.params?.map((param: any) =>
typeof param === 'object' ? '{...}' : param?.toString().substring(0, 10)
),
})

View File

@@ -1,18 +1,13 @@
{
"extends": "../tsconfig.json",
"compilerOptions": {
"esModuleInterop": true,
"composite": false,
"incremental": true,
"lib": ["DOM", "DOM.Iterable", "ESNext"],
"noEmit": true,
"strict": true,
"isolatedModules": false,
"noImplicitAny": false,
"target": "ES5",
"tsBuildInfoFile": "../node_modules/.cache/tsbuildinfo/cypress", // avoid clobbering the build tsbuildinfo
"types": ["cypress", "node"],
"jsx": "react"
},
"exclude": ["node_modules"],
"include": ["**/*.ts"],
"watchOptions": {
"excludeDirectories": ["node_modules"]
}
}

View File

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

View File

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

50
functions/README.md Normal file
View File

@@ -0,0 +1,50 @@
# Cloudflare Cloud Functions
## Purpose
These functions utilize Cloudflare Functions to dynamically inject meta tags server side for richer link sharing capabilities.
## Functions
Currently, there are 2 types of cloudflare functions developed
- Meta Data Injectors - Workers that inject [Open Graph](https://ogp.me/) standardized meta tags into the `header` of specific webpages.
- Currently we support this functionaltiy for three separate webpages: NFT Assets, NFT Collections, and Token Detail Pages
- These functions query data from GraphQL and then formats them into HTML `meta` tags to be injected
- Dynamically Generated Images - Utilizes Vercel's [Open Graph Image Generation Library](https://vercel.com/docs/concepts/functions/edge-functions/og-image-generation) to create custom thumbnails for specific webpages
- Currently supports NFT Assets, NFT Collections, and Token Detail Pages
- These functions query data from GraphQL, and utilize `Satori` to convert HTML into a png image response which is then returned when the api is called.
- Can be found in the `api/image` folder.
## Testing
Testing is done utilizing a custom jest environment as well as Cloudflare's local tester: `wrangler`. Wrangler enables testing locally by running a proxy to wrap `localhost`. Testing can be done the following ways.
- Manually by running `yarn start:cloud` to setup wrangler on `localhost:3000`
- Automated tests by running `yarn test:cloud` to setup both a jest and wrangler environment and automatically test features
## Deployment
Functions will be deployed to Cloudlfare where they will be ran automatically when the appropriate route is hit.
## Miscellaneous
- Caching: In order to speed up webpage requests, repeated GraphQL queries will be saved and pulled using Cloudflare's Cache API.
## Scripts
- `yarn start:cloud` (NODE_OPTIONS=--dns-result-order=ipv4first PORT=3001 npx wrangler pages dev --node-compat --proxy=3001 --port=3000 -- yarn start), script to start local wrangler environment
- `npx wrangler pages dev`: this basis of this command which starts a local instance of wrangler to test cloud functions
- `--node-compat`: wrangler option that enables compatibility with Node.js modules
- `--proxy:3001`: telling the proxy to listen on port 3001
- `--port=3000`: telling wrangler to run our proxy on port 3000
- `NODE_OPTIONS=--dns-result-order=ipv4first`: wrangler still serves to IPv4 which isn't compatible with Node 18 which default resolves to IPv6 so we need to specify to serve to IPv4
- `PORT-3001 --yarn start`: runs default yarn start on port 3001
- `yarn test:cloud` (NODE_OPTIONS=--experimental-vm-modules yarn jest functions --watch --config=functions/jest.config.json), script to test cloud functions with jest
- `NODE_OPTIONS=--experimental-vm-modules`: support for ES Modules and Web Assembly
- `--config=functions/jest.config.json`: specifying which config file to use
## Additional Documents
- [Open Graph Protocol](https://ogp.me/)
- [Open Graph Image Generation](https://vercel.com/docs/concepts/functions/edge-functions/og-image-generation)
- [Cloudflare Workers](https://developers.cloudflare.com/workers/)
- [HTML Rewriter](https://developers.cloudflare.com/workers/runtime-apis/html-rewriter/)
- [Cache API](https://developers.cloudflare.com/workers/runtime-apis/cache/)

View File

@@ -0,0 +1 @@
export const presets = ['@babel/preset-env']

20
functions/client.ts Normal file
View File

@@ -0,0 +1,20 @@
import { ApolloClient, InMemoryCache } from '@apollo/client'
const GRAPHQL_ENDPOINT = 'https://api.uniswap.org/v1/graphql'
//TODO: Figure out how to make ApolloClient global variable
export default new ApolloClient({
connectToDevTools: true,
uri: GRAPHQL_ENDPOINT,
headers: {
'Content-Type': 'application/json',
Origin: 'https://app.uniswap.org',
'User-Agent':
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.5735.110 Safari/537.36',
},
cache: new InMemoryCache(),
defaultOptions: {
watchQuery: {
fetchPolicy: 'cache-first',
},
},
})

View File

@@ -0,0 +1,38 @@
type MetaTagInjectorInput = {
title: string
image?: string
url: string
}
/**
* Listener class for Cloudflare's HTMLRewriter {@link https://developers.cloudflare.com/workers/runtime-apis/html-rewriter}
* to inject meta tags into the <head> of an HTML document.
*/
export class MetaTagInjector implements HTMLRewriterElementContentHandlers {
constructor(private input: MetaTagInjectorInput) {}
append(element: Element, property: string, content: string) {
element.append(`<meta property="${property}" content="${content}"/>`, { html: true })
}
element(element: Element) {
//Open Graph Tags
this.append(element, 'og:title', this.input.title)
if (this.input.image) {
this.append(element, 'og:image', this.input.image)
this.append(element, 'og:image:width', '1200')
this.append(element, 'og:image:height', '630')
this.append(element, 'og:image:alt', this.input.title)
}
this.append(element, 'og:type', 'website')
this.append(element, 'og:url', this.input.url)
//Twitter Tags
this.append(element, 'twitter:card', 'summary_large_image')
this.append(element, 'twitter:title', this.input.title)
if (this.input.image) {
this.append(element, 'twitter:image', this.input.image)
this.append(element, 'twitter:image:alt', this.input.title)
}
}
}

View File

@@ -0,0 +1,9 @@
import { setup } from 'jest-dev-server'
module.exports = async function globalSetup() {
globalThis.servers = await setup({
command: `yarn start:cloud`,
port: 3000,
launchTimeout: 50000,
})
}

View File

@@ -0,0 +1,5 @@
import { teardown } from 'jest-dev-server'
module.exports = async function globalTeardown() {
await teardown(globalThis.servers)
}

6
functions/global.d.ts vendored Normal file
View File

@@ -0,0 +1,6 @@
import { setup } from 'jest-dev-server'
declare global {
// eslint-disable-next-line no-var
var servers: Awaited<ReturnType<typeof setup>>
}

View File

@@ -0,0 +1,11 @@
{
"globalSetup": "<rootDir>/global-setup.ts",
"globalTeardown": "<rootDir>/global-teardown.ts",
"preset": "ts-jest",
"transform": {
"'^.+\\.(ts|tsx)?$'": "ts-jest",
"^.+\\.(js|jsx)$": "babel-jest"
},
"testTimeout": 360000,
"cacheDirectory": "../node_modules/.cache/cloud-jest"
}

View File

@@ -0,0 +1,15 @@
/* eslint-disable import/no-unused-modules */
import getAsset from '../../utils/getAsset'
import getRequest from '../../utils/getRequest'
export const onRequest: PagesFunction = async ({ params, request, next }) => {
const res = next()
try {
const { index } = params
const collectionAddress = index[0]?.toString()
const tokenId = index[1]?.toString()
return getRequest(res, request.url, () => getAsset(collectionAddress, tokenId, request.url))
} catch (e) {
return res
}
}

View File

@@ -0,0 +1,397 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`should inject metadata for valid assets 1`] = `
"<!DOCTYPE html>
<html translate="no">
<head>
<meta charset="utf-8" />
<title>Uniswap Interface</title>
<meta name="description" content="Swap or provide liquidity on the Uniswap Protocol" />
<!--
. will be replaced with the URL of the \`public\` folder during build.
Only files inside the \`public\` folder can be referenced from the HTML.
-->
<link rel="shortcut icon" type="image/png" href="./favicon.png" />
<link rel="apple-touch-icon" sizes="192x192" href="./images/192x192_App_Icon.png" />
<link rel="apple-touch-icon" sizes="512x512" href="./images/512x512_App_Icon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<meta name="theme-color" content="#FC72FF" />
<meta
http-equiv="Content-Security-Policy"
content="script-src 'self' 'unsafe-inline'"
/>
<!--
Apple Smart App Banner for Safari on iOS
https://developer.apple.com/documentation/webkit/promoting_apps_with_smart_app_banners
-->
<meta name="apple-itunes-app" content="app-id=6443944476">
<!--
manifest.json provides metadata used when the app is installed as a PWA.
See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href="./manifest.json" />
<link rel="preload" href="./fonts/Inter-roman.var.woff2" as="font" type="font/woff2" crossorigin />
<style>
* {
font-family: 'Inter', sans-serif;
box-sizing: border-box;
}
/**
Explicitly load Inter var from public/ so it does not block LCP's critical path.
*/
@font-face {
font-family: 'Inter custom';
font-weight: 100 900;
font-style: normal;
font-display: block;
font-named-instance: 'Regular';
src: url(./fonts/Inter-roman.var.woff2) format('woff2 supports variations(gvar)'),
url(./fonts/Inter-roman.var.woff2) format('woff2-variations'),
url(./fonts/Inter-roman.var.woff2) format('woff2');
}
@supports (font-variation-settings: normal) {
* {
font-family: 'Inter custom', sans-serif;
}
}
html,
body {
margin: 0;
padding: 0;
}
button {
user-select: none;
}
html {
font-size: 16px;
font-variant: none;
font-smooth: always;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
}
/* Use this to apply network-specific gradient backgrounds, in RadialGradientByChainUpdater.ts */
#background-radial-gradient {
position: fixed;
top: 0;
left: 0;
right: 0;
pointer-events: none;
width: 200vw;
height: 200vh;
transform: translate(-50vw, -100vh);
z-index: -1;
}
html,
body,
#root {
min-height: 100%;
}
@media (prefers-color-scheme: dark) {
html {
background: linear-gradient(180deg, #202738 0%, #070816 100%);
}
}
@media (prefers-color-scheme: light) {
html {
background: radial-gradient(100% 100% at 50% 0%, rgba(255, 184, 226, 0.51) 0%, rgba(255, 255, 255, 0) 100%), #FFFFFF
}
}
</style>
<script defer src="./static/js/bundle.js"></script><meta property="og:title" content="Azuki #2550"/><meta property="og:image" content="https://cdn.center.app/1/0xED5AF388653567Af2F388E6224dC7C4b3241C544/2550/d268b7f60a56306ced68b9762709ceaff4f1ee939f3150e7363fae300a59da12.png"/><meta property="og:image:width" content="1200"/><meta property="og:image:height" content="630"/><meta property="og:image:alt" content="Azuki #2550"/><meta property="og:type" content="website"/><meta property="og:url" content="http://127.0.0.1:3000/nfts/asset/0xed5af388653567af2f388e6224dc7c4b3241c544/2550"/><meta property="twitter:card" content="summary_large_image"/><meta property="twitter:title" content="Azuki #2550"/><meta property="twitter:image" content="https://cdn.center.app/1/0xED5AF388653567Af2F388E6224dC7C4b3241C544/2550/d268b7f60a56306ced68b9762709ceaff4f1ee939f3150e7363fae300a59da12.png"/><meta property="twitter:image:alt" content="Azuki #2550"/></head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root">
<!-- Triggers the font to load immediately and then is replaced by the app -->
<div>&emsp;</div>
</div>
<div id="background-radial-gradient"></div>
</body>
</html>
"
`;
exports[`should inject metadata for valid assets 2`] = `
"<!DOCTYPE html>
<html translate="no">
<head>
<meta charset="utf-8" />
<title>Uniswap Interface</title>
<meta name="description" content="Swap or provide liquidity on the Uniswap Protocol" />
<!--
. will be replaced with the URL of the \`public\` folder during build.
Only files inside the \`public\` folder can be referenced from the HTML.
-->
<link rel="shortcut icon" type="image/png" href="./favicon.png" />
<link rel="apple-touch-icon" sizes="192x192" href="./images/192x192_App_Icon.png" />
<link rel="apple-touch-icon" sizes="512x512" href="./images/512x512_App_Icon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<meta name="theme-color" content="#FC72FF" />
<meta
http-equiv="Content-Security-Policy"
content="script-src 'self' 'unsafe-inline'"
/>
<!--
Apple Smart App Banner for Safari on iOS
https://developer.apple.com/documentation/webkit/promoting_apps_with_smart_app_banners
-->
<meta name="apple-itunes-app" content="app-id=6443944476">
<!--
manifest.json provides metadata used when the app is installed as a PWA.
See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href="./manifest.json" />
<link rel="preload" href="./fonts/Inter-roman.var.woff2" as="font" type="font/woff2" crossorigin />
<style>
* {
font-family: 'Inter', sans-serif;
box-sizing: border-box;
}
/**
Explicitly load Inter var from public/ so it does not block LCP's critical path.
*/
@font-face {
font-family: 'Inter custom';
font-weight: 100 900;
font-style: normal;
font-display: block;
font-named-instance: 'Regular';
src: url(./fonts/Inter-roman.var.woff2) format('woff2 supports variations(gvar)'),
url(./fonts/Inter-roman.var.woff2) format('woff2-variations'),
url(./fonts/Inter-roman.var.woff2) format('woff2');
}
@supports (font-variation-settings: normal) {
* {
font-family: 'Inter custom', sans-serif;
}
}
html,
body {
margin: 0;
padding: 0;
}
button {
user-select: none;
}
html {
font-size: 16px;
font-variant: none;
font-smooth: always;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
}
/* Use this to apply network-specific gradient backgrounds, in RadialGradientByChainUpdater.ts */
#background-radial-gradient {
position: fixed;
top: 0;
left: 0;
right: 0;
pointer-events: none;
width: 200vw;
height: 200vh;
transform: translate(-50vw, -100vh);
z-index: -1;
}
html,
body,
#root {
min-height: 100%;
}
@media (prefers-color-scheme: dark) {
html {
background: linear-gradient(180deg, #202738 0%, #070816 100%);
}
}
@media (prefers-color-scheme: light) {
html {
background: radial-gradient(100% 100% at 50% 0%, rgba(255, 184, 226, 0.51) 0%, rgba(255, 255, 255, 0) 100%), #FFFFFF
}
}
</style>
<script defer src="./static/js/bundle.js"></script><meta property="og:title" content="Bored Ape Yacht Club #3735"/><meta property="og:image" content="https://cdn.center.app/v2/1/697f69bb495aaa24c66638cae921977354f0b8274fc2e2814e455f355e67f01d/88c2ac6b73288e41051d3fd58ff3cef1f4908403f05f4a7d2a8435d003758529.png"/><meta property="og:image:width" content="1200"/><meta property="og:image:height" content="630"/><meta property="og:image:alt" content="Bored Ape Yacht Club #3735"/><meta property="og:type" content="website"/><meta property="og:url" content="http://127.0.0.1:3000/nfts/asset/0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d/3735"/><meta property="twitter:card" content="summary_large_image"/><meta property="twitter:title" content="Bored Ape Yacht Club #3735"/><meta property="twitter:image" content="https://cdn.center.app/v2/1/697f69bb495aaa24c66638cae921977354f0b8274fc2e2814e455f355e67f01d/88c2ac6b73288e41051d3fd58ff3cef1f4908403f05f4a7d2a8435d003758529.png"/><meta property="twitter:image:alt" content="Bored Ape Yacht Club #3735"/></head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root">
<!-- Triggers the font to load immediately and then is replaced by the app -->
<div>&emsp;</div>
</div>
<div id="background-radial-gradient"></div>
</body>
</html>
"
`;
exports[`should inject metadata for valid assets 3`] = `
"<!DOCTYPE html>
<html translate="no">
<head>
<meta charset="utf-8" />
<title>Uniswap Interface</title>
<meta name="description" content="Swap or provide liquidity on the Uniswap Protocol" />
<!--
. will be replaced with the URL of the \`public\` folder during build.
Only files inside the \`public\` folder can be referenced from the HTML.
-->
<link rel="shortcut icon" type="image/png" href="./favicon.png" />
<link rel="apple-touch-icon" sizes="192x192" href="./images/192x192_App_Icon.png" />
<link rel="apple-touch-icon" sizes="512x512" href="./images/512x512_App_Icon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<meta name="theme-color" content="#FC72FF" />
<meta
http-equiv="Content-Security-Policy"
content="script-src 'self' 'unsafe-inline'"
/>
<!--
Apple Smart App Banner for Safari on iOS
https://developer.apple.com/documentation/webkit/promoting_apps_with_smart_app_banners
-->
<meta name="apple-itunes-app" content="app-id=6443944476">
<!--
manifest.json provides metadata used when the app is installed as a PWA.
See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href="./manifest.json" />
<link rel="preload" href="./fonts/Inter-roman.var.woff2" as="font" type="font/woff2" crossorigin />
<style>
* {
font-family: 'Inter', sans-serif;
box-sizing: border-box;
}
/**
Explicitly load Inter var from public/ so it does not block LCP's critical path.
*/
@font-face {
font-family: 'Inter custom';
font-weight: 100 900;
font-style: normal;
font-display: block;
font-named-instance: 'Regular';
src: url(./fonts/Inter-roman.var.woff2) format('woff2 supports variations(gvar)'),
url(./fonts/Inter-roman.var.woff2) format('woff2-variations'),
url(./fonts/Inter-roman.var.woff2) format('woff2');
}
@supports (font-variation-settings: normal) {
* {
font-family: 'Inter custom', sans-serif;
}
}
html,
body {
margin: 0;
padding: 0;
}
button {
user-select: none;
}
html {
font-size: 16px;
font-variant: none;
font-smooth: always;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
}
/* Use this to apply network-specific gradient backgrounds, in RadialGradientByChainUpdater.ts */
#background-radial-gradient {
position: fixed;
top: 0;
left: 0;
right: 0;
pointer-events: none;
width: 200vw;
height: 200vh;
transform: translate(-50vw, -100vh);
z-index: -1;
}
html,
body,
#root {
min-height: 100%;
}
@media (prefers-color-scheme: dark) {
html {
background: linear-gradient(180deg, #202738 0%, #070816 100%);
}
}
@media (prefers-color-scheme: light) {
html {
background: radial-gradient(100% 100% at 50% 0%, rgba(255, 184, 226, 0.51) 0%, rgba(255, 255, 255, 0) 100%), #FFFFFF
}
}
</style>
<script defer src="./static/js/bundle.js"></script><meta property="og:title" content="CryptoPunk #3947"/><meta property="og:image" content="https://cdn.center.app/1/0xb47e3cd837dDF8e4c57F05d70Ab865de6e193BBB/3947/62319d784e7a816d190aa184ffe58550d6ed8eb2e117b218e2ac02f126538ee6.png"/><meta property="og:image:width" content="1200"/><meta property="og:image:height" content="630"/><meta property="og:image:alt" content="CryptoPunk #3947"/><meta property="og:type" content="website"/><meta property="og:url" content="http://127.0.0.1:3000/nfts/asset/0xb47e3cd837ddf8e4c57f05d70ab865de6e193bbb/3947"/><meta property="twitter:card" content="summary_large_image"/><meta property="twitter:title" content="CryptoPunk #3947"/><meta property="twitter:image" content="https://cdn.center.app/1/0xb47e3cd837dDF8e4c57F05d70Ab865de6e193BBB/3947/62319d784e7a816d190aa184ffe58550d6ed8eb2e117b218e2ac02f126538ee6.png"/><meta property="twitter:image:alt" content="CryptoPunk #3947"/></head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root">
<!-- Triggers the font to load immediately and then is replaced by the app -->
<div>&emsp;</div>
</div>
<div id="background-radial-gradient"></div>
</body>
</html>
"
`;

View File

@@ -0,0 +1,64 @@
const assets = [
{
address: '0xed5af388653567af2f388e6224dc7c4b3241c544',
assetId: '2550',
collectionName: 'Azuki',
image:
'https://cdn.center.app/1/0xED5AF388653567Af2F388E6224dC7C4b3241C544/2550/d268b7f60a56306ced68b9762709ceaff4f1ee939f3150e7363fae300a59da12.png',
},
{
address: '0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d',
assetId: '3735',
collectionName: 'Bored Ape Yacht Club',
image:
'https://cdn.center.app/v2/1/697f69bb495aaa24c66638cae921977354f0b8274fc2e2814e455f355e67f01d/88c2ac6b73288e41051d3fd58ff3cef1f4908403f05f4a7d2a8435d003758529.png',
},
{
address: '0xb47e3cd837ddf8e4c57f05d70ab865de6e193bbb',
assetId: '3947',
collectionName: 'CryptoPunk',
image:
'https://cdn.center.app/1/0xb47e3cd837dDF8e4c57F05d70Ab865de6e193BBB/3947/62319d784e7a816d190aa184ffe58550d6ed8eb2e117b218e2ac02f126538ee6.png',
},
]
test.each(assets)('should inject metadata for valid assets', async (nft) => {
const url = 'http://127.0.0.1:3000/nfts/asset/' + nft.address + '/' + nft.assetId
const body = await fetch(new Request(url)).then((res) => res.text())
expect(body).toMatchSnapshot()
expect(body).toContain(`<meta property="og:title" content="${nft.collectionName} #${nft.assetId}"/>`)
expect(body).toContain(`<meta property="og:image" content="${nft.image}"/>`)
expect(body).toContain(`<meta property="og:image:width" content="1200"/>`)
expect(body).toContain(`<meta property="og:image:height" content="630"/>`)
expect(body).toContain(`<meta property="og:type" content="website"/>`)
expect(body).toContain(`<meta property="og:url" content="${url}"/>`)
expect(body).toContain(`<meta property="og:image:alt" content="${nft.collectionName} #${nft.assetId}"/>`)
expect(body).toContain(`<meta property="twitter:card" content="summary_large_image"/>`)
expect(body).toContain(`<meta property="twitter:title" content="${nft.collectionName} #${nft.assetId}"/>`)
expect(body).toContain(`<meta property="twitter:image" content="${nft.image}"/>`)
expect(body).toContain(`<meta property="twitter:image:alt" content="${nft.collectionName} #${nft.assetId}"/>`)
})
const invalidAssets = [
'http://127.0.0.1:3000/nfts/asset/0xed5af388653567af2f388e6224dc7c4b3241c544/100000',
'http://127.0.0.1:3000/nfts/asset/0xed5af388653567af2f388e6224dc7c4b3241c544',
'http://127.0.0.1:3000/nfts/asset/0xed5af388653567af2f388e6224dc7c4b3241c545',
'http://127.0.0.1:3000/nfts/asset/0xed5af388653567af2f388e6224dc7c4b3241c544/-1',
'http://127.0.0.1:3000/nfts/asset/0xed5af388653567af2f388e6224dc7c4b3241c544//',
'http://127.0.0.1:3000/nfts/asset/0xed5af388653567af2f388e6224dc7c4b3241c544//2550',
]
test.each(invalidAssets)('should not inject metadata for invalid asset calls', async (url) => {
const body = await fetch(new Request(url)).then((res) => res.text())
expect(body).not.toContain('og:title')
expect(body).not.toContain('og:image')
expect(body).not.toContain('og:image:width')
expect(body).not.toContain('og:image:height')
expect(body).not.toContain('og:type')
expect(body).not.toContain('og:url')
expect(body).not.toContain('og:image:alt')
expect(body).not.toContain('twitter:card')
expect(body).not.toContain('twitter:title')
expect(body).not.toContain('twitter:image')
expect(body).not.toContain('twitter:image:alt')
})

View File

@@ -0,0 +1,14 @@
/* eslint-disable import/no-unused-modules */
import getCollection from '../../utils/getCollection'
import getRequest from '../../utils/getRequest'
export const onRequest: PagesFunction = async ({ params, request, next }) => {
const res = next()
try {
const { index } = params
const collectionAddress = index?.toString()
return getRequest(res, request.url, () => getCollection(collectionAddress, request.url))
} catch (e) {
return res
}
}

View File

@@ -0,0 +1,397 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`should inject metadata for valid collections 1`] = `
"<!DOCTYPE html>
<html translate="no">
<head>
<meta charset="utf-8" />
<title>Uniswap Interface</title>
<meta name="description" content="Swap or provide liquidity on the Uniswap Protocol" />
<!--
. will be replaced with the URL of the \`public\` folder during build.
Only files inside the \`public\` folder can be referenced from the HTML.
-->
<link rel="shortcut icon" type="image/png" href="./favicon.png" />
<link rel="apple-touch-icon" sizes="192x192" href="./images/192x192_App_Icon.png" />
<link rel="apple-touch-icon" sizes="512x512" href="./images/512x512_App_Icon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<meta name="theme-color" content="#FC72FF" />
<meta
http-equiv="Content-Security-Policy"
content="script-src 'self' 'unsafe-inline'"
/>
<!--
Apple Smart App Banner for Safari on iOS
https://developer.apple.com/documentation/webkit/promoting_apps_with_smart_app_banners
-->
<meta name="apple-itunes-app" content="app-id=6443944476">
<!--
manifest.json provides metadata used when the app is installed as a PWA.
See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href="./manifest.json" />
<link rel="preload" href="./fonts/Inter-roman.var.woff2" as="font" type="font/woff2" crossorigin />
<style>
* {
font-family: 'Inter', sans-serif;
box-sizing: border-box;
}
/**
Explicitly load Inter var from public/ so it does not block LCP's critical path.
*/
@font-face {
font-family: 'Inter custom';
font-weight: 100 900;
font-style: normal;
font-display: block;
font-named-instance: 'Regular';
src: url(./fonts/Inter-roman.var.woff2) format('woff2 supports variations(gvar)'),
url(./fonts/Inter-roman.var.woff2) format('woff2-variations'),
url(./fonts/Inter-roman.var.woff2) format('woff2');
}
@supports (font-variation-settings: normal) {
* {
font-family: 'Inter custom', sans-serif;
}
}
html,
body {
margin: 0;
padding: 0;
}
button {
user-select: none;
}
html {
font-size: 16px;
font-variant: none;
font-smooth: always;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
}
/* Use this to apply network-specific gradient backgrounds, in RadialGradientByChainUpdater.ts */
#background-radial-gradient {
position: fixed;
top: 0;
left: 0;
right: 0;
pointer-events: none;
width: 200vw;
height: 200vh;
transform: translate(-50vw, -100vh);
z-index: -1;
}
html,
body,
#root {
min-height: 100%;
}
@media (prefers-color-scheme: dark) {
html {
background: linear-gradient(180deg, #202738 0%, #070816 100%);
}
}
@media (prefers-color-scheme: light) {
html {
background: radial-gradient(100% 100% at 50% 0%, rgba(255, 184, 226, 0.51) 0%, rgba(255, 255, 255, 0) 100%), #FFFFFF
}
}
</style>
<script defer src="./static/js/bundle.js"></script><meta property="og:title" content="Azuki on Uniswap"/><meta property="og:image" content="https://i.seadn.io/gae/H8jOCJuQokNqGBpkBN5wk1oZwO7LM8bNnrHCaekV2nKjnCqw6UB5oaH8XyNeBDj6bA_n1mjejzhFQUP3O1NfjFLHr3FOaeHcTOOT?w=500&auto=format"/><meta property="og:image:width" content="1200"/><meta property="og:image:height" content="630"/><meta property="og:image:alt" content="Azuki on Uniswap"/><meta property="og:type" content="website"/><meta property="og:url" content="http://127.0.0.1:3000/nfts/collection/0xed5af388653567af2f388e6224dc7c4b3241c544"/><meta property="twitter:card" content="summary_large_image"/><meta property="twitter:title" content="Azuki on Uniswap"/><meta property="twitter:image" content="https://i.seadn.io/gae/H8jOCJuQokNqGBpkBN5wk1oZwO7LM8bNnrHCaekV2nKjnCqw6UB5oaH8XyNeBDj6bA_n1mjejzhFQUP3O1NfjFLHr3FOaeHcTOOT?w=500&auto=format"/><meta property="twitter:image:alt" content="Azuki on Uniswap"/></head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root">
<!-- Triggers the font to load immediately and then is replaced by the app -->
<div>&emsp;</div>
</div>
<div id="background-radial-gradient"></div>
</body>
</html>
"
`;
exports[`should inject metadata for valid collections 2`] = `
"<!DOCTYPE html>
<html translate="no">
<head>
<meta charset="utf-8" />
<title>Uniswap Interface</title>
<meta name="description" content="Swap or provide liquidity on the Uniswap Protocol" />
<!--
. will be replaced with the URL of the \`public\` folder during build.
Only files inside the \`public\` folder can be referenced from the HTML.
-->
<link rel="shortcut icon" type="image/png" href="./favicon.png" />
<link rel="apple-touch-icon" sizes="192x192" href="./images/192x192_App_Icon.png" />
<link rel="apple-touch-icon" sizes="512x512" href="./images/512x512_App_Icon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<meta name="theme-color" content="#FC72FF" />
<meta
http-equiv="Content-Security-Policy"
content="script-src 'self' 'unsafe-inline'"
/>
<!--
Apple Smart App Banner for Safari on iOS
https://developer.apple.com/documentation/webkit/promoting_apps_with_smart_app_banners
-->
<meta name="apple-itunes-app" content="app-id=6443944476">
<!--
manifest.json provides metadata used when the app is installed as a PWA.
See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href="./manifest.json" />
<link rel="preload" href="./fonts/Inter-roman.var.woff2" as="font" type="font/woff2" crossorigin />
<style>
* {
font-family: 'Inter', sans-serif;
box-sizing: border-box;
}
/**
Explicitly load Inter var from public/ so it does not block LCP's critical path.
*/
@font-face {
font-family: 'Inter custom';
font-weight: 100 900;
font-style: normal;
font-display: block;
font-named-instance: 'Regular';
src: url(./fonts/Inter-roman.var.woff2) format('woff2 supports variations(gvar)'),
url(./fonts/Inter-roman.var.woff2) format('woff2-variations'),
url(./fonts/Inter-roman.var.woff2) format('woff2');
}
@supports (font-variation-settings: normal) {
* {
font-family: 'Inter custom', sans-serif;
}
}
html,
body {
margin: 0;
padding: 0;
}
button {
user-select: none;
}
html {
font-size: 16px;
font-variant: none;
font-smooth: always;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
}
/* Use this to apply network-specific gradient backgrounds, in RadialGradientByChainUpdater.ts */
#background-radial-gradient {
position: fixed;
top: 0;
left: 0;
right: 0;
pointer-events: none;
width: 200vw;
height: 200vh;
transform: translate(-50vw, -100vh);
z-index: -1;
}
html,
body,
#root {
min-height: 100%;
}
@media (prefers-color-scheme: dark) {
html {
background: linear-gradient(180deg, #202738 0%, #070816 100%);
}
}
@media (prefers-color-scheme: light) {
html {
background: radial-gradient(100% 100% at 50% 0%, rgba(255, 184, 226, 0.51) 0%, rgba(255, 255, 255, 0) 100%), #FFFFFF
}
}
</style>
<script defer src="./static/js/bundle.js"></script><meta property="og:title" content="Bored Ape Yacht Club on Uniswap"/><meta property="og:image" content="https://i.seadn.io/gae/Ju9CkWtV-1Okvf45wo8UctR-M9He2PjILP0oOvxE89AyiPPGtrR3gysu1Zgy0hjd2xKIgjJJtWIc0ybj4Vd7wv8t3pxDGHoJBzDB?w=500&auto=format"/><meta property="og:image:width" content="1200"/><meta property="og:image:height" content="630"/><meta property="og:image:alt" content="Bored Ape Yacht Club on Uniswap"/><meta property="og:type" content="website"/><meta property="og:url" content="http://127.0.0.1:3000/nfts/collection/0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d"/><meta property="twitter:card" content="summary_large_image"/><meta property="twitter:title" content="Bored Ape Yacht Club on Uniswap"/><meta property="twitter:image" content="https://i.seadn.io/gae/Ju9CkWtV-1Okvf45wo8UctR-M9He2PjILP0oOvxE89AyiPPGtrR3gysu1Zgy0hjd2xKIgjJJtWIc0ybj4Vd7wv8t3pxDGHoJBzDB?w=500&auto=format"/><meta property="twitter:image:alt" content="Bored Ape Yacht Club on Uniswap"/></head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root">
<!-- Triggers the font to load immediately and then is replaced by the app -->
<div>&emsp;</div>
</div>
<div id="background-radial-gradient"></div>
</body>
</html>
"
`;
exports[`should inject metadata for valid collections 3`] = `
"<!DOCTYPE html>
<html translate="no">
<head>
<meta charset="utf-8" />
<title>Uniswap Interface</title>
<meta name="description" content="Swap or provide liquidity on the Uniswap Protocol" />
<!--
. will be replaced with the URL of the \`public\` folder during build.
Only files inside the \`public\` folder can be referenced from the HTML.
-->
<link rel="shortcut icon" type="image/png" href="./favicon.png" />
<link rel="apple-touch-icon" sizes="192x192" href="./images/192x192_App_Icon.png" />
<link rel="apple-touch-icon" sizes="512x512" href="./images/512x512_App_Icon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<meta name="theme-color" content="#FC72FF" />
<meta
http-equiv="Content-Security-Policy"
content="script-src 'self' 'unsafe-inline'"
/>
<!--
Apple Smart App Banner for Safari on iOS
https://developer.apple.com/documentation/webkit/promoting_apps_with_smart_app_banners
-->
<meta name="apple-itunes-app" content="app-id=6443944476">
<!--
manifest.json provides metadata used when the app is installed as a PWA.
See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href="./manifest.json" />
<link rel="preload" href="./fonts/Inter-roman.var.woff2" as="font" type="font/woff2" crossorigin />
<style>
* {
font-family: 'Inter', sans-serif;
box-sizing: border-box;
}
/**
Explicitly load Inter var from public/ so it does not block LCP's critical path.
*/
@font-face {
font-family: 'Inter custom';
font-weight: 100 900;
font-style: normal;
font-display: block;
font-named-instance: 'Regular';
src: url(./fonts/Inter-roman.var.woff2) format('woff2 supports variations(gvar)'),
url(./fonts/Inter-roman.var.woff2) format('woff2-variations'),
url(./fonts/Inter-roman.var.woff2) format('woff2');
}
@supports (font-variation-settings: normal) {
* {
font-family: 'Inter custom', sans-serif;
}
}
html,
body {
margin: 0;
padding: 0;
}
button {
user-select: none;
}
html {
font-size: 16px;
font-variant: none;
font-smooth: always;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
}
/* Use this to apply network-specific gradient backgrounds, in RadialGradientByChainUpdater.ts */
#background-radial-gradient {
position: fixed;
top: 0;
left: 0;
right: 0;
pointer-events: none;
width: 200vw;
height: 200vh;
transform: translate(-50vw, -100vh);
z-index: -1;
}
html,
body,
#root {
min-height: 100%;
}
@media (prefers-color-scheme: dark) {
html {
background: linear-gradient(180deg, #202738 0%, #070816 100%);
}
}
@media (prefers-color-scheme: light) {
html {
background: radial-gradient(100% 100% at 50% 0%, rgba(255, 184, 226, 0.51) 0%, rgba(255, 255, 255, 0) 100%), #FFFFFF
}
}
</style>
<script defer src="./static/js/bundle.js"></script><meta property="og:title" content="CLONE X - X TAKASHI MURAKAMI on Uniswap"/><meta property="og:image" content="https://i.seadn.io/gae/XN0XuD8Uh3jyRWNtPTFeXJg_ht8m5ofDx6aHklOiy4amhFuWUa0JaR6It49AH8tlnYS386Q0TW_-Lmedn0UET_ko1a3CbJGeu5iHMg?w=500&auto=format"/><meta property="og:image:width" content="1200"/><meta property="og:image:height" content="630"/><meta property="og:image:alt" content="CLONE X - X TAKASHI MURAKAMI on Uniswap"/><meta property="og:type" content="website"/><meta property="og:url" content="http://127.0.0.1:3000/nfts/collection/0x49cf6f5d44e70224e2e23fdcdd2c053f30ada28b"/><meta property="twitter:card" content="summary_large_image"/><meta property="twitter:title" content="CLONE X - X TAKASHI MURAKAMI on Uniswap"/><meta property="twitter:image" content="https://i.seadn.io/gae/XN0XuD8Uh3jyRWNtPTFeXJg_ht8m5ofDx6aHklOiy4amhFuWUa0JaR6It49AH8tlnYS386Q0TW_-Lmedn0UET_ko1a3CbJGeu5iHMg?w=500&auto=format"/><meta property="twitter:image:alt" content="CLONE X - X TAKASHI MURAKAMI on Uniswap"/></head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root">
<!-- Triggers the font to load immediately and then is replaced by the app -->
<div>&emsp;</div>
</div>
<div id="background-radial-gradient"></div>
</body>
</html>
"
`;

View File

@@ -0,0 +1,63 @@
const collections = [
{
address: '0xed5af388653567af2f388e6224dc7c4b3241c544',
collectionName: 'Azuki',
image:
'https://i.seadn.io/gae/H8jOCJuQokNqGBpkBN5wk1oZwO7LM8bNnrHCaekV2nKjnCqw6UB5oaH8XyNeBDj6bA_n1mjejzhFQUP3O1NfjFLHr3FOaeHcTOOT?w=500&auto=format',
},
{
address: '0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d',
collectionName: 'Bored Ape Yacht Club',
image:
'https://i.seadn.io/gae/Ju9CkWtV-1Okvf45wo8UctR-M9He2PjILP0oOvxE89AyiPPGtrR3gysu1Zgy0hjd2xKIgjJJtWIc0ybj4Vd7wv8t3pxDGHoJBzDB?w=500&auto=format',
},
{
address: '0x49cf6f5d44e70224e2e23fdcdd2c053f30ada28b',
collectionName: 'CLONE X - X TAKASHI MURAKAMI',
image:
'https://i.seadn.io/gae/XN0XuD8Uh3jyRWNtPTFeXJg_ht8m5ofDx6aHklOiy4amhFuWUa0JaR6It49AH8tlnYS386Q0TW_-Lmedn0UET_ko1a3CbJGeu5iHMg?w=500&auto=format',
},
]
test.each(collections)('should inject metadata for valid collections', async (collection) => {
const url = 'http://127.0.0.1:3000/nfts/collection/' + collection.address
const body = await fetch(new Request(url)).then((res) => res.text())
expect(body).toMatchSnapshot()
expect(body).toContain(`<meta property="og:title" content="${collection.collectionName} on Uniswap"/>`)
expect(body).toContain(`<meta property="og:image" content="${collection.image}"/>`)
expect(body).toContain(`<meta property="og:image:width" content="1200"/>`)
expect(body).toContain(`<meta property="og:image:height" content="630"/>`)
expect(body).toContain(`<meta property="og:type" content="website"/>`)
expect(body).toContain(`<meta property="og:url" content="${url}"/>`)
expect(body).toContain(`<meta property="og:image:alt" content="${collection.collectionName} on Uniswap"/>`)
expect(body).toContain(`<meta property="twitter:card" content="summary_large_image"/>`)
expect(body).toContain(`<meta property="twitter:title" content="${collection.collectionName} on Uniswap"/>`)
expect(body).toContain(`<meta property="twitter:image" content="${collection.image}"/>`)
expect(body).toContain(`<meta property="twitter:image:alt" content="${collection.collectionName} on Uniswap"/>`)
})
const invalidCollections = [
'http://127.0.0.1:3000/nfts/collection/0xed5af388653567af2f388e6224dc7c4b3241c545',
'http://127.0.0.1:3000/nfts/collection/0xed5af388653567af2f388e6224dc7c4b3241c545/10',
'http://127.0.0.1:3000/nfts/collection/0xed5af388653567af2f388e6224dc7c4b3241c545//',
'http://127.0.0.1:3000/nfts/collection',
]
test.each(invalidCollections)(
'should not inject metadata for invalid collection urls',
async (url) => {
const body = await fetch(new Request(url)).then((res) => res.text())
expect(body).not.toContain('og:title')
expect(body).not.toContain('og:image')
expect(body).not.toContain('og:image:width')
expect(body).not.toContain('og:image:height')
expect(body).not.toContain('og:type')
expect(body).not.toContain('og:url')
expect(body).not.toContain('og:image:alt')
expect(body).not.toContain('twitter:card')
expect(body).not.toContain('twitter:title')
expect(body).not.toContain('twitter:image')
expect(body).not.toContain('twitter:image:alt')
},
50000
)

View File

@@ -0,0 +1,34 @@
/* eslint-disable import/no-unused-modules */
import { Chain } from '../../src/graphql/data/__generated__/types-and-hooks'
import getRequest from '../utils/getRequest'
import getToken from '../utils/getToken'
const convertTokenAddress = (tokenAddress: string, networkName: string) => {
if (tokenAddress === 'NATIVE') {
switch (networkName) {
case Chain.Celo:
return '0x471EcE3750Da237f93B8E339c536989b8978a438'
case Chain.Polygon:
return '0x0000000000000000000000000000000000001010'
default:
return undefined
}
}
return tokenAddress
}
export const onRequest: PagesFunction = async ({ params, request, next }) => {
const res = next()
try {
const { index } = params
const networkName = index[0]?.toString().toUpperCase()
const tokenString = index[1]?.toString()
if (!tokenString) {
return res
}
const tokenAddress = convertTokenAddress(tokenString, networkName)
return getRequest(res, request.url, () => getToken(networkName, tokenAddress, request.url))
} catch (e) {
return res
}
}

View File

@@ -0,0 +1,529 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`should inject metadata for valid tokens 1`] = `
"<!DOCTYPE html>
<html translate="no">
<head>
<meta charset="utf-8" />
<title>Uniswap Interface</title>
<meta name="description" content="Swap or provide liquidity on the Uniswap Protocol" />
<!--
. will be replaced with the URL of the \`public\` folder during build.
Only files inside the \`public\` folder can be referenced from the HTML.
-->
<link rel="shortcut icon" type="image/png" href="./favicon.png" />
<link rel="apple-touch-icon" sizes="192x192" href="./images/192x192_App_Icon.png" />
<link rel="apple-touch-icon" sizes="512x512" href="./images/512x512_App_Icon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<meta name="theme-color" content="#FC72FF" />
<meta
http-equiv="Content-Security-Policy"
content="script-src 'self' 'unsafe-inline'"
/>
<!--
Apple Smart App Banner for Safari on iOS
https://developer.apple.com/documentation/webkit/promoting_apps_with_smart_app_banners
-->
<meta name="apple-itunes-app" content="app-id=6443944476">
<!--
manifest.json provides metadata used when the app is installed as a PWA.
See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href="./manifest.json" />
<link rel="preload" href="./fonts/Inter-roman.var.woff2" as="font" type="font/woff2" crossorigin />
<style>
* {
font-family: 'Inter', sans-serif;
box-sizing: border-box;
}
/**
Explicitly load Inter var from public/ so it does not block LCP's critical path.
*/
@font-face {
font-family: 'Inter custom';
font-weight: 100 900;
font-style: normal;
font-display: block;
font-named-instance: 'Regular';
src: url(./fonts/Inter-roman.var.woff2) format('woff2 supports variations(gvar)'),
url(./fonts/Inter-roman.var.woff2) format('woff2-variations'),
url(./fonts/Inter-roman.var.woff2) format('woff2');
}
@supports (font-variation-settings: normal) {
* {
font-family: 'Inter custom', sans-serif;
}
}
html,
body {
margin: 0;
padding: 0;
}
button {
user-select: none;
}
html {
font-size: 16px;
font-variant: none;
font-smooth: always;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
}
/* Use this to apply network-specific gradient backgrounds, in RadialGradientByChainUpdater.ts */
#background-radial-gradient {
position: fixed;
top: 0;
left: 0;
right: 0;
pointer-events: none;
width: 200vw;
height: 200vh;
transform: translate(-50vw, -100vh);
z-index: -1;
}
html,
body,
#root {
min-height: 100%;
}
@media (prefers-color-scheme: dark) {
html {
background: linear-gradient(180deg, #202738 0%, #070816 100%);
}
}
@media (prefers-color-scheme: light) {
html {
background: radial-gradient(100% 100% at 50% 0%, rgba(255, 184, 226, 0.51) 0%, rgba(255, 255, 255, 0) 100%), #FFFFFF
}
}
</style>
<script defer src="./static/js/bundle.js"></script><meta property="og:title" content="Get USDC on Uniswap"/><meta property="og:image" content="https://raw.githubusercontent.com/Uniswap/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png"/><meta property="og:image:width" content="1200"/><meta property="og:image:height" content="630"/><meta property="og:image:alt" content="Get USDC on Uniswap"/><meta property="og:type" content="website"/><meta property="og:url" content="http://127.0.0.1:3000/tokens/ethereum/0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48"/><meta property="twitter:card" content="summary_large_image"/><meta property="twitter:title" content="Get USDC on Uniswap"/><meta property="twitter:image" content="https://raw.githubusercontent.com/Uniswap/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png"/><meta property="twitter:image:alt" content="Get USDC on Uniswap"/></head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root">
<!-- Triggers the font to load immediately and then is replaced by the app -->
<div>&emsp;</div>
</div>
<div id="background-radial-gradient"></div>
</body>
</html>
"
`;
exports[`should inject metadata for valid tokens 2`] = `
"<!DOCTYPE html>
<html translate="no">
<head>
<meta charset="utf-8" />
<title>Uniswap Interface</title>
<meta name="description" content="Swap or provide liquidity on the Uniswap Protocol" />
<!--
. will be replaced with the URL of the \`public\` folder during build.
Only files inside the \`public\` folder can be referenced from the HTML.
-->
<link rel="shortcut icon" type="image/png" href="./favicon.png" />
<link rel="apple-touch-icon" sizes="192x192" href="./images/192x192_App_Icon.png" />
<link rel="apple-touch-icon" sizes="512x512" href="./images/512x512_App_Icon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<meta name="theme-color" content="#FC72FF" />
<meta
http-equiv="Content-Security-Policy"
content="script-src 'self' 'unsafe-inline'"
/>
<!--
Apple Smart App Banner for Safari on iOS
https://developer.apple.com/documentation/webkit/promoting_apps_with_smart_app_banners
-->
<meta name="apple-itunes-app" content="app-id=6443944476">
<!--
manifest.json provides metadata used when the app is installed as a PWA.
See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href="./manifest.json" />
<link rel="preload" href="./fonts/Inter-roman.var.woff2" as="font" type="font/woff2" crossorigin />
<style>
* {
font-family: 'Inter', sans-serif;
box-sizing: border-box;
}
/**
Explicitly load Inter var from public/ so it does not block LCP's critical path.
*/
@font-face {
font-family: 'Inter custom';
font-weight: 100 900;
font-style: normal;
font-display: block;
font-named-instance: 'Regular';
src: url(./fonts/Inter-roman.var.woff2) format('woff2 supports variations(gvar)'),
url(./fonts/Inter-roman.var.woff2) format('woff2-variations'),
url(./fonts/Inter-roman.var.woff2) format('woff2');
}
@supports (font-variation-settings: normal) {
* {
font-family: 'Inter custom', sans-serif;
}
}
html,
body {
margin: 0;
padding: 0;
}
button {
user-select: none;
}
html {
font-size: 16px;
font-variant: none;
font-smooth: always;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
}
/* Use this to apply network-specific gradient backgrounds, in RadialGradientByChainUpdater.ts */
#background-radial-gradient {
position: fixed;
top: 0;
left: 0;
right: 0;
pointer-events: none;
width: 200vw;
height: 200vh;
transform: translate(-50vw, -100vh);
z-index: -1;
}
html,
body,
#root {
min-height: 100%;
}
@media (prefers-color-scheme: dark) {
html {
background: linear-gradient(180deg, #202738 0%, #070816 100%);
}
}
@media (prefers-color-scheme: light) {
html {
background: radial-gradient(100% 100% at 50% 0%, rgba(255, 184, 226, 0.51) 0%, rgba(255, 255, 255, 0) 100%), #FFFFFF
}
}
</style>
<script defer src="./static/js/bundle.js"></script><meta property="og:title" content="Get ETH on Uniswap"/><meta property="og:image" content="https://token-icons.s3.amazonaws.com/eth.png"/><meta property="og:image:width" content="1200"/><meta property="og:image:height" content="630"/><meta property="og:image:alt" content="Get ETH on Uniswap"/><meta property="og:type" content="website"/><meta property="og:url" content="http://127.0.0.1:3000/tokens/ethereum/NATIVE"/><meta property="twitter:card" content="summary_large_image"/><meta property="twitter:title" content="Get ETH on Uniswap"/><meta property="twitter:image" content="https://token-icons.s3.amazonaws.com/eth.png"/><meta property="twitter:image:alt" content="Get ETH on Uniswap"/></head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root">
<!-- Triggers the font to load immediately and then is replaced by the app -->
<div>&emsp;</div>
</div>
<div id="background-radial-gradient"></div>
</body>
</html>
"
`;
exports[`should inject metadata for valid tokens 3`] = `
"<!DOCTYPE html>
<html translate="no">
<head>
<meta charset="utf-8" />
<title>Uniswap Interface</title>
<meta name="description" content="Swap or provide liquidity on the Uniswap Protocol" />
<!--
. will be replaced with the URL of the \`public\` folder during build.
Only files inside the \`public\` folder can be referenced from the HTML.
-->
<link rel="shortcut icon" type="image/png" href="./favicon.png" />
<link rel="apple-touch-icon" sizes="192x192" href="./images/192x192_App_Icon.png" />
<link rel="apple-touch-icon" sizes="512x512" href="./images/512x512_App_Icon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<meta name="theme-color" content="#FC72FF" />
<meta
http-equiv="Content-Security-Policy"
content="script-src 'self' 'unsafe-inline'"
/>
<!--
Apple Smart App Banner for Safari on iOS
https://developer.apple.com/documentation/webkit/promoting_apps_with_smart_app_banners
-->
<meta name="apple-itunes-app" content="app-id=6443944476">
<!--
manifest.json provides metadata used when the app is installed as a PWA.
See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href="./manifest.json" />
<link rel="preload" href="./fonts/Inter-roman.var.woff2" as="font" type="font/woff2" crossorigin />
<style>
* {
font-family: 'Inter', sans-serif;
box-sizing: border-box;
}
/**
Explicitly load Inter var from public/ so it does not block LCP's critical path.
*/
@font-face {
font-family: 'Inter custom';
font-weight: 100 900;
font-style: normal;
font-display: block;
font-named-instance: 'Regular';
src: url(./fonts/Inter-roman.var.woff2) format('woff2 supports variations(gvar)'),
url(./fonts/Inter-roman.var.woff2) format('woff2-variations'),
url(./fonts/Inter-roman.var.woff2) format('woff2');
}
@supports (font-variation-settings: normal) {
* {
font-family: 'Inter custom', sans-serif;
}
}
html,
body {
margin: 0;
padding: 0;
}
button {
user-select: none;
}
html {
font-size: 16px;
font-variant: none;
font-smooth: always;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
}
/* Use this to apply network-specific gradient backgrounds, in RadialGradientByChainUpdater.ts */
#background-radial-gradient {
position: fixed;
top: 0;
left: 0;
right: 0;
pointer-events: none;
width: 200vw;
height: 200vh;
transform: translate(-50vw, -100vh);
z-index: -1;
}
html,
body,
#root {
min-height: 100%;
}
@media (prefers-color-scheme: dark) {
html {
background: linear-gradient(180deg, #202738 0%, #070816 100%);
}
}
@media (prefers-color-scheme: light) {
html {
background: radial-gradient(100% 100% at 50% 0%, rgba(255, 184, 226, 0.51) 0%, rgba(255, 255, 255, 0) 100%), #FFFFFF
}
}
</style>
<script defer src="./static/js/bundle.js"></script><meta property="og:title" content="Get MATIC on Uniswap"/><meta property="og:image" content="https://raw.githubusercontent.com/Uniswap/assets/master/blockchains/ethereum/assets/0x7D1AfA7B718fb893dB30A3aBc0Cfc608AaCfeBB0/logo.png"/><meta property="og:image:width" content="1200"/><meta property="og:image:height" content="630"/><meta property="og:image:alt" content="Get MATIC on Uniswap"/><meta property="og:type" content="website"/><meta property="og:url" content="http://127.0.0.1:3000/tokens/polygon/NATIVE"/><meta property="twitter:card" content="summary_large_image"/><meta property="twitter:title" content="Get MATIC on Uniswap"/><meta property="twitter:image" content="https://raw.githubusercontent.com/Uniswap/assets/master/blockchains/ethereum/assets/0x7D1AfA7B718fb893dB30A3aBc0Cfc608AaCfeBB0/logo.png"/><meta property="twitter:image:alt" content="Get MATIC on Uniswap"/></head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root">
<!-- Triggers the font to load immediately and then is replaced by the app -->
<div>&emsp;</div>
</div>
<div id="background-radial-gradient"></div>
</body>
</html>
"
`;
exports[`should inject metadata for valid tokens 4`] = `
"<!DOCTYPE html>
<html translate="no">
<head>
<meta charset="utf-8" />
<title>Uniswap Interface</title>
<meta name="description" content="Swap or provide liquidity on the Uniswap Protocol" />
<!--
. will be replaced with the URL of the \`public\` folder during build.
Only files inside the \`public\` folder can be referenced from the HTML.
-->
<link rel="shortcut icon" type="image/png" href="./favicon.png" />
<link rel="apple-touch-icon" sizes="192x192" href="./images/192x192_App_Icon.png" />
<link rel="apple-touch-icon" sizes="512x512" href="./images/512x512_App_Icon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<meta name="theme-color" content="#FC72FF" />
<meta
http-equiv="Content-Security-Policy"
content="script-src 'self' 'unsafe-inline'"
/>
<!--
Apple Smart App Banner for Safari on iOS
https://developer.apple.com/documentation/webkit/promoting_apps_with_smart_app_banners
-->
<meta name="apple-itunes-app" content="app-id=6443944476">
<!--
manifest.json provides metadata used when the app is installed as a PWA.
See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href="./manifest.json" />
<link rel="preload" href="./fonts/Inter-roman.var.woff2" as="font" type="font/woff2" crossorigin />
<style>
* {
font-family: 'Inter', sans-serif;
box-sizing: border-box;
}
/**
Explicitly load Inter var from public/ so it does not block LCP's critical path.
*/
@font-face {
font-family: 'Inter custom';
font-weight: 100 900;
font-style: normal;
font-display: block;
font-named-instance: 'Regular';
src: url(./fonts/Inter-roman.var.woff2) format('woff2 supports variations(gvar)'),
url(./fonts/Inter-roman.var.woff2) format('woff2-variations'),
url(./fonts/Inter-roman.var.woff2) format('woff2');
}
@supports (font-variation-settings: normal) {
* {
font-family: 'Inter custom', sans-serif;
}
}
html,
body {
margin: 0;
padding: 0;
}
button {
user-select: none;
}
html {
font-size: 16px;
font-variant: none;
font-smooth: always;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
}
/* Use this to apply network-specific gradient backgrounds, in RadialGradientByChainUpdater.ts */
#background-radial-gradient {
position: fixed;
top: 0;
left: 0;
right: 0;
pointer-events: none;
width: 200vw;
height: 200vh;
transform: translate(-50vw, -100vh);
z-index: -1;
}
html,
body,
#root {
min-height: 100%;
}
@media (prefers-color-scheme: dark) {
html {
background: linear-gradient(180deg, #202738 0%, #070816 100%);
}
}
@media (prefers-color-scheme: light) {
html {
background: radial-gradient(100% 100% at 50% 0%, rgba(255, 184, 226, 0.51) 0%, rgba(255, 255, 255, 0) 100%), #FFFFFF
}
}
</style>
<script defer src="./static/js/bundle.js"></script><meta property="og:title" content="Get PEPE on Uniswap"/><meta property="og:image" content="https://raw.githubusercontent.com/Uniswap/assets/master/blockchains/ethereum/assets/0x6982508145454Ce325dDbE47a25d4ec3d2311933/logo.png"/><meta property="og:image:width" content="1200"/><meta property="og:image:height" content="630"/><meta property="og:image:alt" content="Get PEPE on Uniswap"/><meta property="og:type" content="website"/><meta property="og:url" content="http://127.0.0.1:3000/tokens/ethereum/0x6982508145454ce325ddbe47a25d4ec3d2311933"/><meta property="twitter:card" content="summary_large_image"/><meta property="twitter:title" content="Get PEPE on Uniswap"/><meta property="twitter:image" content="https://raw.githubusercontent.com/Uniswap/assets/master/blockchains/ethereum/assets/0x6982508145454Ce325dDbE47a25d4ec3d2311933/logo.png"/><meta property="twitter:image:alt" content="Get PEPE on Uniswap"/></head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root">
<!-- Triggers the font to load immediately and then is replaced by the app -->
<div>&emsp;</div>
</div>
<div id="background-radial-gradient"></div>
</body>
</html>
"
`;

View File

@@ -0,0 +1,70 @@
const tokens = [
{
address: '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48',
network: 'ethereum',
symbol: 'USDC',
image:
'https://raw.githubusercontent.com/Uniswap/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png',
},
{
address: 'NATIVE',
network: 'ethereum',
symbol: 'ETH',
image: 'https://token-icons.s3.amazonaws.com/eth.png',
},
{
address: 'NATIVE',
network: 'polygon',
symbol: 'MATIC',
image:
'https://raw.githubusercontent.com/Uniswap/assets/master/blockchains/ethereum/assets/0x7D1AfA7B718fb893dB30A3aBc0Cfc608AaCfeBB0/logo.png',
},
{
address: '0x6982508145454ce325ddbe47a25d4ec3d2311933',
network: 'ethereum',
symbol: 'PEPE',
image:
'https://raw.githubusercontent.com/Uniswap/assets/master/blockchains/ethereum/assets/0x6982508145454Ce325dDbE47a25d4ec3d2311933/logo.png',
},
]
test.each(tokens)('should inject metadata for valid tokens', async (token) => {
const url = 'http://127.0.0.1:3000/tokens/' + token.network + '/' + token.address
const body = await fetch(new Request(url)).then((res) => res.text())
expect(body).toMatchSnapshot()
expect(body).toContain(`<meta property="og:title" content="Get ${token.symbol} on Uniswap"/>`)
expect(body).toContain(`<meta property="og:image" content="${token.image}"/>`)
expect(body).toContain(`<meta property="og:image:width" content="1200"/>`)
expect(body).toContain(`<meta property="og:image:height" content="630"/>`)
expect(body).toContain(`<meta property="og:type" content="website"/>`)
expect(body).toContain(`<meta property="og:url" content="${url}"/>`)
expect(body).toContain(`<meta property="og:image:alt" content="Get ${token.symbol} on Uniswap"/>`)
expect(body).toContain(`<meta property="twitter:card" content="summary_large_image"/>`)
expect(body).toContain(`<meta property="twitter:title" content="Get ${token.symbol} on Uniswap"/>`)
expect(body).toContain(`<meta property="twitter:image" content="${token.image}"/>`)
expect(body).toContain(`<meta property="twitter:image:alt" content="Get ${token.symbol} on Uniswap"/>`)
})
const invalidTokens = [
'http://127.0.0.1:3000/tokens/ethereum/0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb49',
'http://127.0.0.1:3000/tokens/ethereum',
'http://127.0.0.1:3000/tokens/ethereun',
'http://127.0.0.1:3000/tokens/ethereum/0x0',
'http://127.0.0.1:3000/tokens/ethereum//',
'http://127.0.0.1:3000/tokens/potato/?potato=1',
]
test.each(invalidTokens)('should not inject metadata for invalid tokens', async (url) => {
const body = await fetch(new Request(url)).then((res) => res.text())
expect(body).not.toContain('og:title')
expect(body).not.toContain('og:image')
expect(body).not.toContain('og:image:width')
expect(body).not.toContain('og:image:height')
expect(body).not.toContain('og:type')
expect(body).not.toContain('og:url')
expect(body).not.toContain('og:image:alt')
expect(body).not.toContain('twitter:card')
expect(body).not.toContain('twitter:title')
expect(body).not.toContain('twitter:image')
expect(body).not.toContain('twitter:image:alt')
})

19
functions/tsconfig.json Normal file
View File

@@ -0,0 +1,19 @@
{
"compilerOptions": {
"esModuleInterop": true,
"incremental": true,
"lib": ["DOM", "DOM.Iterable", "ESNext"],
"moduleResolution": "node",
"module": "esnext",
"noEmit": true,
"strict": true,
"target": "ESNext",
"tsBuildInfoFile": "../node_modules/.cache/tsbuildinfo/functions", // avoid clobbering the build tsbuildinfo
"types": ["jest", "node", "@cloudflare/workers-types"],
"jsx": "react",
"skipLibCheck": true,
"baseUrl": "functions"
},
"exclude": ["../node_modules", "../src"],
"include": ["**/*.ts"],
}

View File

@@ -0,0 +1,43 @@
import CacheMock from 'browser-cache-mock'
import { mocked } from '../../src/test-utils/mocked'
import Cache from './cache'
const cacheMock = new CacheMock()
const data = {
title: 'test',
image: 'testImage',
url: 'testUrl',
}
beforeAll(() => {
const globalAny: any = global
globalAny.caches = {
open: async () => cacheMock,
...cacheMock,
}
})
test('Should put cache properly', async () => {
jest.spyOn(cacheMock, 'put')
await Cache.put(data, 'https://example.com')
expect(cacheMock.put).toHaveBeenCalledWith('https://example.com', expect.anything())
const call = mocked(cacheMock.put).mock.calls[0]
const response = JSON.parse(await (call[1] as Response).clone().text())
expect(response).toStrictEqual(data)
await expect(Cache.match('https://example.com')).resolves.toStrictEqual(data)
})
test('Should match cache properly', async () => {
jest.spyOn(cacheMock, 'match').mockResolvedValueOnce(new Response(JSON.stringify(data)))
const response = await Cache.match('https://example.com')
expect(response).toStrictEqual(data)
})
test('Should return undefined if not all data is present', async () => {
jest.spyOn(cacheMock, 'match').mockResolvedValueOnce(new Response(JSON.stringify({ ...data, title: undefined })))
const response = await Cache.match('https://example.com')
expect(response).toBeUndefined()
})

28
functions/utils/cache.ts Normal file
View File

@@ -0,0 +1,28 @@
interface Data {
title: string
image: string
url: string
}
const CACHE_NAME = 'functions-cache' as const
class Cache {
async match(request: string): Promise<Data | undefined> {
const cache = await caches.open(CACHE_NAME)
const response = await cache.match(request)
if (!response) return undefined
const data: Data = JSON.parse(await response.text())
if (!data.title || !data.image || !data.url) return undefined
return data
}
async put(data: Data, request: string) {
// Set max age to 1 week
const response = new Response(JSON.stringify(data))
response.headers.set('Cache-Control', 'max-age=604800')
const cache = await caches.open(CACHE_NAME)
await cache.put(request, response)
}
}
export default new Cache()

View File

@@ -0,0 +1,38 @@
import { AssetDocument } from '../../src/graphql/data/__generated__/types-and-hooks'
import client from '../client'
function formatTitleName(name: string, collectionName: string, tokenId: string) {
if (name) {
return name
}
if (collectionName && tokenId) {
return collectionName + ' #' + tokenId
}
if (tokenId) {
return 'Asset #' + tokenId
}
return 'View NFT on Uniswap'
}
export default async function getAsset(collectionAddress: string, tokenId: string, url: string) {
const { data } = await client.query({
query: AssetDocument,
variables: {
address: collectionAddress,
filter: {
tokenIds: [tokenId],
},
},
})
const asset = data?.nftAssets?.edges[0]?.node
if (!asset) {
return undefined
}
const title = formatTitleName(asset.name, asset.collection?.name, asset.tokenId)
const formattedAsset = {
title,
image: asset.image?.url,
url,
}
return formattedAsset
}

View File

@@ -0,0 +1,21 @@
import { CollectionDocument } from '../../src/graphql/data/__generated__/types-and-hooks'
import client from '../client'
export default async function getCollection(collectionAddress: string, url: string) {
const { data } = await client.query({
query: CollectionDocument,
variables: {
addresses: collectionAddress,
},
})
const collection = data?.nftCollections?.edges[0]?.node
if (!collection || !collection.name) {
return undefined
}
const formattedAsset = {
title: collection.name + ' on Uniswap',
image: collection.image?.url,
url,
}
return formattedAsset
}

View File

@@ -0,0 +1,38 @@
import * as matchers from 'jest-extended'
expect.extend(matchers)
import { mocked } from '../../src/test-utils/mocked'
import Cache from './cache'
import getRequest from './getRequest'
jest.mock('./cache', () => ({
match: jest.fn(),
put: jest.fn(),
}))
test('should call Cache.match before calling getData when request is not cached', async () => {
const url = 'https://example.com'
const getData = jest.fn().mockResolvedValueOnce({
title: 'test',
image: 'testImage',
url: 'testUrl',
})
await getRequest(Promise.resolve(new Response()), url, getData)
expect(Cache.match).toHaveBeenCalledWith(url)
expect(getData).toHaveBeenCalled()
expect(Cache.match).toHaveBeenCalledBefore(getData)
expect(Cache.put).toHaveBeenCalledAfter(getData)
})
test('getData should not be called when request is cached', async () => {
const url = 'https://example.com'
mocked(Cache.match).mockResolvedValueOnce({
title: 'test',
image: 'testImage',
url: 'testUrl',
})
const getData = jest.fn()
await getRequest(Promise.resolve(new Response()), url, getData)
expect(Cache.match).toHaveBeenCalledWith(url)
expect(getData).not.toHaveBeenCalled()
})

View File

@@ -0,0 +1,31 @@
import { MetaTagInjector } from '../components/metaTagInjector'
import Cache from './cache'
export default async function getRequest(
res: Promise<Response>,
url: string,
getData: () => Promise<
| {
title: string
image: string
url: string
}
| undefined
>
) {
try {
const cachedData = await Cache.match(url)
if (cachedData) {
return new HTMLRewriter().on('head', new MetaTagInjector(cachedData)).transform(await res)
} else {
const data = await getData()
if (!data) {
return res
}
await Cache.put(data, url)
return new HTMLRewriter().on('head', new MetaTagInjector(data)).transform(await res)
}
} catch (e) {
return res
}
}

View File

@@ -0,0 +1,33 @@
import { TokenDocument } from '../../src/graphql/data/__generated__/types-and-hooks'
import client from '../client'
function formatTitleName(symbol: string, name: string) {
if (symbol) {
return 'Get ' + symbol + ' on Uniswap'
}
if (name) {
return 'Get ' + name + ' on Uniswap'
}
return 'View Token on Uniswap'
}
export default async function getToken(networkName: string, tokenAddress: string | undefined, url: string) {
const { data } = await client.query({
query: TokenDocument,
variables: {
chain: networkName,
address: tokenAddress,
},
})
const asset = data?.token
if (!asset) {
return undefined
}
const title = formatTitleName(asset.symbol, asset.name)
const formattedAsset = {
title,
image: asset.project?.logoUrl,
url,
}
return formattedAsset
}

View File

@@ -15,6 +15,7 @@ const config: CodegenConfig = {
withHooks: true,
// This avoid all generated schemas being wrapped in Maybe https://the-guild.dev/graphql/codegen/plugins/typescript/typescript#maybevalue-string-default-value-t--null
maybeValue: 'T',
immutableTypes: true,
},
},
},

View File

@@ -1,14 +1,9 @@
import { SupportedChainId } from '@uniswap/sdk-core'
import { ChainId } from '@uniswap/sdk-core'
import { UNIVERSAL_ROUTER_CREATION_BLOCK } from '@uniswap/universal-router-sdk'
/* eslint-env node */
require('dotenv').config()
// Block selection is arbitrary, as e2e tests will build up their own state.
// The only requirement is that all infrastructure under test (eg Permit2 contracts) are already deployed.
// TODO(WEB-2187): Make more dynamic to avoid manually updating
const BLOCK_NUMBER = 17388567
const POLYGON_BLOCK_NUMBER = 43600000
const forkingConfig = {
httpHeaders: {
Origin: 'localhost:3000', // infura allowlists requests by origin
@@ -16,14 +11,14 @@ const forkingConfig = {
}
const forks = {
[SupportedChainId.MAINNET]: {
[ChainId.MAINNET]: {
url: `https://mainnet.infura.io/v3/${process.env.REACT_APP_INFURA_KEY}`,
blockNumber: BLOCK_NUMBER,
blockNumber: UNIVERSAL_ROUTER_CREATION_BLOCK(ChainId.MAINNET),
...forkingConfig,
},
[SupportedChainId.POLYGON]: {
[ChainId.POLYGON]: {
url: `https://polygon-mainnet.infura.io/v3/${process.env.REACT_APP_INFURA_KEY}`,
blockNumber: POLYGON_BLOCK_NUMBER,
blockNumber: UNIVERSAL_ROUTER_CREATION_BLOCK(ChainId.POLYGON),
...forkingConfig,
},
}
@@ -32,8 +27,8 @@ module.exports = {
forks,
networks: {
hardhat: {
chainId: SupportedChainId.MAINNET,
forking: forks[SupportedChainId.MAINNET],
chainId: ChainId.MAINNET,
forking: forks[ChainId.MAINNET],
accounts: {
count: 2,
},

View File

@@ -19,13 +19,17 @@
"i18n": "yarn i18n:extract --clean && yarn i18n:compile",
"prepare": "concurrently \"npm:ajv\" \"npm:contracts\" \"npm:graphql\" \"npm:i18n\"",
"start": "craco start",
"start:cloud": "NODE_OPTIONS=--dns-result-order=ipv4first PORT=3001 npx wrangler pages dev --node-compat --proxy=3001 --port=3000 -- yarn start",
"build": "craco build",
"build:e2e": "REACT_APP_CSP_ALLOW_UNSAFE_EVAL=true REACT_APP_ADD_COVERAGE_INSTRUMENTATION=true craco build",
"analyze": "source-map-explorer 'build/static/js/*.js' --only-mapped",
"serve": "serve build -l 3000",
"lint": "yarn eslint --ignore-path .gitignore --cache --cache-location node_modules/.cache/eslint/ .",
"typecheck": "tsc",
"typecheck:cloud": "tsc -p functions/tsconfig.json",
"typecheck:cypress": "tsc -p cypress/tsconfig.json",
"test": "craco test",
"test:cloud": "NODE_OPTIONS=--experimental-vm-modules yarn jest functions --config=functions/jest.config.json",
"cypress:open": "cypress open --browser chrome --e2e",
"cypress:run": "cypress run --browser chrome --e2e",
"deduplicate": "yarn-deduplicate --strategy=highest"
@@ -66,6 +70,9 @@
]
},
"devDependencies": {
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
"@babel/preset-env": "^7.22.7",
"@cloudflare/workers-types": "^4.20230710.1",
"@craco/craco": "^7.1.0",
"@ethersproject/experimental": "^5.4.0",
"@lingui/cli": "^3.9.0",
@@ -97,36 +104,44 @@
"@types/ua-parser-js": "^0.7.36",
"@types/uuid": "^8.3.4",
"@types/wcag-contrast": "^3.0.0",
"@uniswap/default-token-list": "^9.6.0",
"@uniswap/default-token-list": "^11.2.0",
"@uniswap/eslint-config": "^1.2.0",
"@vanilla-extract/babel-plugin": "^1.1.7",
"@vanilla-extract/jest-transform": "^1.1.1",
"@vanilla-extract/webpack-plugin": "^2.1.11",
"@walletconnect/types": "^2.8.6",
"babel-jest": "^29.6.1",
"babel-plugin-istanbul": "^6.1.1",
"browser-cache-mock": "^0.1.7",
"buffer": "^6.0.3",
"concurrently": "^8.0.1",
"cypress": "12.12.0",
"cypress-hardhat": "^2.4.1",
"cypress-hardhat": "^2.5.0",
"env-cmd": "^10.1.0",
"eslint": "^7.11.0",
"eslint-plugin-import": "^2.27",
"eslint-plugin-rulesdir": "^0.2.2",
"hardhat": "^2.14.0",
"jest": "^29.6.1",
"jest-dev-server": "^9.0.0",
"jest-extended": "^4.0.1",
"jest-fail-on-console": "^3.1.1",
"jest-fetch-mock": "^3.0.3",
"jest-styled-components": "^7.0.8",
"ms.macro": "^2.0.0",
"path-browserify": "^1.0.1",
"prettier": "^2.7.1",
"prettier": "^2.8.8",
"process": "^0.11.10",
"react-scripts": "^5.0.1",
"resize-observer-polyfill": "^1.5.1",
"serve": "^11.3.2",
"source-map-explorer": "^2.5.3",
"ts-jest": "^29.1.1",
"ts-transform-graphql-tag": "^0.2.1",
"typechain": "^5.0.0",
"typescript": "^4.4.3",
"webpack-retry-chunk-load-plugin": "^3.1.1",
"wrangler": "^3.4.0",
"yarn-deduplicate": "^6.0.0"
},
"dependencies": {
@@ -156,25 +171,26 @@
"@sentry/tracing": "^7.45.0",
"@sentry/types": "^7.45.0",
"@types/react-window-infinite-loader": "^1.0.6",
"@uniswap/analytics": "^1.3.1",
"@uniswap/analytics-events": "^2.13.0",
"@uniswap/conedison": "^1.7.1",
"@uniswap/analytics": "^1.4.0",
"@uniswap/analytics-events": "^2.14.0",
"@uniswap/conedison": "^1.8.0",
"@uniswap/governance": "^1.0.2",
"@uniswap/liquidity-staker": "^1.0.2",
"@uniswap/merkle-distributor": "1.0.1",
"@uniswap/permit2-sdk": "1.2.0",
"@uniswap/merkle-distributor": "^1.0.1",
"@uniswap/permit2-sdk": "^1.2.0",
"@uniswap/redux-multicall": "^1.1.8",
"@uniswap/router-sdk": "^1.3.0",
"@uniswap/sdk-core": "^3.2.3",
"@uniswap/smart-order-router": "^3.12.1",
"@uniswap/token-lists": "^1.0.0-beta.31",
"@uniswap/universal-router-sdk": "^1.5.1",
"@uniswap/v2-core": "1.0.0",
"@uniswap/router-sdk": "^1.6.0",
"@uniswap/sdk-core": "^4.0.3",
"@uniswap/smart-order-router": "^3.15.0",
"@uniswap/token-lists": "^1.0.0-beta.33",
"@uniswap/uniswapx-sdk": "^1.1.0",
"@uniswap/universal-router-sdk": "^1.5.6",
"@uniswap/v2-core": "^1.0.1",
"@uniswap/v2-periphery": "^1.1.0-beta.0",
"@uniswap/v2-sdk": "^3.0.1",
"@uniswap/v3-core": "1.0.0",
"@uniswap/v2-sdk": "^3.2.0",
"@uniswap/v3-core": "^1.0.1",
"@uniswap/v3-periphery": "^1.1.1",
"@uniswap/v3-sdk": "^3.9.0",
"@uniswap/v3-sdk": "^3.10.0",
"@vanilla-extract/css": "^1.7.2",
"@vanilla-extract/css-utils": "^0.1.2",
"@vanilla-extract/dynamic": "^2.0.2",
@@ -186,7 +202,6 @@
"@visx/react-spring": "^2.12.2",
"@visx/responsive": "^2.10.0",
"@visx/shape": "^2.11.1",
"@walletconnect/modal": "^2.5.4",
"@web3-react/coinbase-wallet": "^8.2.0",
"@web3-react/core": "^8.2.0",
"@web3-react/eip1193": "^8.2.0",
@@ -196,7 +211,6 @@
"@web3-react/network": "^8.2.0",
"@web3-react/types": "^8.2.0",
"@web3-react/url": "^8.2.0",
"@web3-react/walletconnect": "^8.2.0",
"@web3-react/walletconnect-v2": "^8.3.7",
"ajv": "^8.11.0",
"ajv-formats": "^2.1.1",

View File

@@ -0,0 +1,33 @@
[
{
"relation": ["delegate_permission/common.handle_all_urls"],
"target": {
"namespace": "android_app",
"package_name": "com.uniswap",
"sha256_cert_fingerprints":
["97:A5:81:51:DA:AF:8F:6E:65:3A:90:1E:82:12:6C:FB:61:2D:36:C7:CF:20:61:6B:A3:4C:52:CA:BC:58:43:8E"]
}
},
[
{
"relation": ["delegate_permission/common.handle_all_urls"],
"target": {
"namespace": "android_app",
"package_name": "com.uniswap.beta",
"sha256_cert_fingerprints":
["E5:39:87:DC:4D:FD:4C:1B:A6:74:36:7D:3A:3B:6B:ED:9E:B3:66:89:92:8A:1B:B8:FC:1B:22:56:56:B4:46:A3"]
}
}
],
[
{
"relation": ["delegate_permission/common.handle_all_urls"],
"target": {
"namespace": "android_app",
"package_name": "com.uniswap.dev",
"sha256_cert_fingerprints":
["A8:A7:D4:DE:46:8E:BE:F6:DE:3B:62:2B:A7:26:60:F2:9A:4C:CD:AF:A6:96:C9:E5:7C:91:68:A1:29:2A:48:D3"]
}
}
]
]

View File

@@ -0,0 +1,30 @@
{
"applinks": {
"details": [
{
"appIDs": [
"JH3UHGZD75.com.uniswap.mobile",
"JH3UHGZD75.com.uniswap.mobile.dev"
],
"components": [
{
"#": "/nfts/asset/*",
"comment": "NFT Item"
},
{
"#": "/nfts/collection/*",
"comment": "NFT Collection"
},
{
"#": "/tokens/*",
"comment": "Token address"
},
{
"#": "/address/*",
"comment": "Wallet address"
}
]
}
]
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

View File

@@ -0,0 +1,3 @@
<svg width="55" height="55" viewBox="0 0 55 55" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M8.48223 46.7073C7.39664 45.6264 6.85526 43.9605 6.85814 41.7096V36.8327C6.8693 36.41 6.71466 35.9999 6.42734 35.69L2.981 32.2403C1.37846 30.6564 0.577148 29.0983 0.577148 27.5661C0.577148 26.0338 1.36838 24.4786 2.95082 22.9004L6.39716 19.4508C6.68527 19.1414 6.84004 18.7309 6.82795 18.3081L6.82795 13.4182C6.82795 11.1501 7.36932 9.47985 8.45205 8.40759C9.53477 7.33532 11.1876 6.79776 13.4105 6.79488L18.3128 6.79488C18.5176 6.80189 18.7217 6.7673 18.9128 6.69319C19.1038 6.61908 19.2778 6.50698 19.4243 6.36368L22.9138 2.91403C24.4991 1.34732 26.0527 0.559659 27.5749 0.551034C29.097 0.54241 30.6508 1.33007 32.2361 2.91403L35.6824 6.36368C35.834 6.50867 36.0132 6.6216 36.2093 6.69569C36.4055 6.76978 36.6145 6.80351 36.824 6.79488H41.6963C43.9421 6.79488 45.605 7.33677 46.6849 8.42054C47.7647 9.5043 48.3061 11.1702 48.309 13.4182V18.2951C48.2969 18.7179 48.4517 19.1285 48.7398 19.4378L52.1861 22.8875C53.7686 24.4686 54.5655 26.0238 54.577 27.5531C54.5885 29.0825 53.7915 30.6362 52.1861 32.2145L48.7398 35.6641C48.4525 35.974 48.2978 36.3842 48.309 36.8068V41.6837C48.309 43.9289 47.7633 45.5948 46.6719 46.6814C45.5806 47.768 43.922 48.3099 41.6963 48.3071H36.824C36.6144 48.2977 36.4052 48.3311 36.2089 48.4052C36.0127 48.4793 35.8336 48.5927 35.6824 48.7383L32.2361 52.1879C30.6508 53.7546 29.097 54.5423 27.5749 54.5509C26.0527 54.5595 24.4991 53.7719 22.9138 52.1879L19.4243 48.7383C19.2782 48.5943 19.1042 48.4818 18.9131 48.4077C18.7219 48.3335 18.5177 48.2993 18.3128 48.3071H13.4105C11.2077 48.3243 9.56496 47.791 8.48223 46.7073ZM25.225 37.9942C26.0971 37.9942 26.848 37.5581 27.3567 36.783L38.1848 19.8505C38.4997 19.3418 38.7904 18.7604 38.7904 18.2033C38.7904 17.0163 37.7245 16.2169 36.586 16.2169C35.8593 16.2169 35.2052 16.6045 34.6965 17.4281L25.1281 32.786L20.6225 27.045C20.0411 26.294 19.4597 26.0276 18.733 26.0276C17.5461 26.0276 16.6255 26.9723 16.6255 28.1835C16.6255 28.7649 16.8436 29.2978 17.2554 29.8065L22.9722 36.783C23.6262 37.6308 24.3287 37.9942 25.225 37.9942Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

@@ -19,9 +19,9 @@
<meta
http-equiv="Content-Security-Policy"
<% if (process.env.REACT_APP_CSP_ALLOW_UNSAFE_EVAL) { %>
content="script-src 'self' https://www.google-analytics.com https://www.googletagmanager.com 'unsafe-inline' 'unsafe-eval'"
content="script-src 'self' 'unsafe-inline' 'unsafe-eval'"
<% } else { %>
content="script-src 'self' https://www.google-analytics.com https://www.googletagmanager.com 'unsafe-inline'"
content="script-src 'self' 'unsafe-inline'"
<% } %>
/>
@@ -37,8 +37,6 @@
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<link rel="preconnect" href="https://www.google-analytics.com/" />
<link rel="preload" href="%PUBLIC_URL%/fonts/Inter-roman.var.woff2" as="font" type="font/woff2" crossorigin />
<style>

47
src/analytics/index.tsx Normal file
View File

@@ -0,0 +1,47 @@
import {
sendAnalyticsEvent as sendAnalyticsTraceEvent,
Trace as AnalyticsTrace,
TraceEvent as AnalyticsEvent,
} from '@uniswap/analytics'
import { atomWithStorage, useAtomValue } from 'jotai/utils'
import { memo } from 'react'
export { getDeviceId, initializeAnalytics, OriginApplication, user, useTrace } from '@uniswap/analytics'
const allowAnalyticsAtomKey = 'allow_analytics'
export const allowAnalyticsAtom = atomWithStorage<boolean>(allowAnalyticsAtomKey, true)
export const Trace = memo((props: React.ComponentProps<typeof AnalyticsTrace>) => {
const allowAnalytics = useAtomValue(allowAnalyticsAtom)
const shouldLogImpression = allowAnalytics ? props.shouldLogImpression : false
return <AnalyticsTrace {...props} shouldLogImpression={shouldLogImpression} />
})
Trace.displayName = 'Trace'
export const TraceEvent = memo((props: React.ComponentProps<typeof AnalyticsEvent>) => {
const allowAnalytics = useAtomValue(allowAnalyticsAtom)
const shouldLogImpression = allowAnalytics ? props.shouldLogImpression : false
return <AnalyticsEvent {...props} shouldLogImpression={shouldLogImpression} />
})
TraceEvent.displayName = 'TraceEvent'
export const sendAnalyticsEvent: typeof sendAnalyticsTraceEvent = (event, properties) => {
let allowAnalytics = true
try {
const value = localStorage.getItem(allowAnalyticsAtomKey)
if (typeof value === 'string') {
allowAnalytics = JSON.parse(value)
}
// eslint-disable-next-line no-empty
} catch {}
if (allowAnalytics) {
sendAnalyticsTraceEvent(event, properties)
}
}

View File

@@ -0,0 +1,11 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_13871_12533)">
<path d="M12.9341 2.74118H3.05524V11.7259H12.9341V2.74118Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M15.9947 8C15.9947 12.4154 12.4154 15.9947 7.99999 15.9947C3.58465 15.9947 0.00531006 12.4154 0.00531006 8C0.00531006 3.58466 3.58465 0.00532532 7.99999 0.00532532C12.4154 0.00532532 15.9947 3.58466 15.9947 8ZM5.73452 11.1815H4.18298C3.85696 11.1815 3.69591 11.1815 3.59772 11.1187C3.49166 11.0499 3.42685 10.936 3.41899 10.8103C3.4131 10.6945 3.49363 10.553 3.65467 10.2702L7.48562 3.51761C7.64864 3.23086 7.73112 3.08749 7.83521 3.03447C7.94715 2.97751 8.08071 2.97751 8.19266 3.03447C8.29675 3.08749 8.37924 3.23086 8.54224 3.51761L9.32981 4.89239L9.33382 4.89941C9.50989 5.20703 9.59917 5.36303 9.63815 5.52675C9.68135 5.70548 9.68135 5.89402 9.63815 6.07274C9.59887 6.23771 9.51049 6.39484 9.33177 6.70711L7.31946 10.2643L7.31426 10.2734C7.13703 10.5836 7.04722 10.7408 6.92274 10.8594C6.78722 10.989 6.62421 11.0832 6.44549 11.1363C6.28247 11.1815 6.09983 11.1815 5.73452 11.1815ZM9.65268 11.1815H11.8759C12.2038 11.1815 12.3689 11.1815 12.4671 11.1168C12.5731 11.048 12.6399 10.9321 12.6458 10.8064C12.6515 10.6943 12.5727 10.5584 12.4184 10.292C12.413 10.283 12.4077 10.2737 12.4023 10.2643L11.2887 8.35928L11.276 8.33783C11.1195 8.07321 11.0405 7.93958 10.9391 7.88793C10.8272 7.83096 10.6955 7.83096 10.5836 7.88793C10.4815 7.94095 10.399 8.0804 10.236 8.36124L9.12633 10.2663L9.12253 10.2729C8.96009 10.5533 8.87891 10.6934 8.88477 10.8084C8.89262 10.9341 8.95743 11.0499 9.06348 11.1187C9.15973 11.1815 9.3247 11.1815 9.65268 11.1815Z" fill="#E84142"/>
</g>
<defs>
<clipPath id="clip0_13871_12533">
<rect width="16" height="16" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

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

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

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

After

Width:  |  Height:  |  Size: 745 B

View File

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

After

Width:  |  Height:  |  Size: 651 B

9
src/assets/svg/bolt.svg Normal file
View File

@@ -0,0 +1,9 @@
<svg width="10" height="14" viewBox="0 0 10 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9.97119 6.19815C9.91786 6.07749 9.79854 6.00016 9.66654 6.00016H6.66654V1.00016C6.66654 0.862156 6.58189 0.738159 6.45255 0.688826C6.32255 0.638826 6.17787 0.674818 6.0852 0.776818L0.0852016 7.44349C-0.00279838 7.54149 -0.025439 7.68149 0.028561 7.80216C0.0818943 7.92283 0.201208 8.00016 0.333208 8.00016H3.33321V13.0002C3.33321 13.1382 3.41786 13.2622 3.5472 13.3115C3.58653 13.3262 3.62654 13.3335 3.66654 13.3335C3.75921 13.3335 3.84988 13.2948 3.91455 13.2228L9.91455 6.55616C10.0025 6.45882 10.0245 6.31815 9.97119 6.19815Z" fill="url(#paint0_linear_1816_1801)"/>
<defs>
<linearGradient id="paint0_linear_1816_1801" x1="-10.1808" y1="-12.0005" x2="10.6572" y2="-11.6015" gradientUnits="userSpaceOnUse">
<stop stop-color="#4673FA"/>
<stop offset="1" stop-color="#9646FA"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 917 B

View File

@@ -0,0 +1,5 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M20.83 14.6C19.9 14.06 19.33 13.07 19.33 12C19.33 10.93 19.9 9.93999 20.83 9.39999C20.99 9.29999 21.05 9.1 20.95 8.94L19.28 6.06C19.22 5.95 19.11 5.89001 19 5.89001C18.94 5.89001 18.88 5.91 18.83 5.94C18.37 6.2 17.85 6.34 17.33 6.34C16.8 6.34 16.28 6.19999 15.81 5.92999C14.88 5.38999 14.31 4.41 14.31 3.34C14.31 3.15 14.16 3 13.98 3H10.02C9.83999 3 9.69 3.15 9.69 3.34C9.69 4.41 9.12 5.38999 8.19 5.92999C7.72 6.19999 7.20001 6.34 6.67001 6.34C6.15001 6.34 5.63001 6.2 5.17001 5.94C5.01001 5.84 4.81 5.9 4.72 6.06L3.04001 8.94C3.01001 8.99 3 9.05001 3 9.10001C3 9.22001 3.06001 9.32999 3.17001 9.39999C4.10001 9.93999 4.67001 10.92 4.67001 11.99C4.67001 13.07 4.09999 14.06 3.17999 14.6H3.17001C3.01001 14.7 2.94999 14.9 3.04999 15.06L4.72 17.94C4.78 18.05 4.89 18.11 5 18.11C5.06 18.11 5.12001 18.09 5.17001 18.06C6.11001 17.53 7.26 17.53 8.19 18.07C9.11 18.61 9.67999 19.59 9.67999 20.66C9.67999 20.85 9.82999 21 10.02 21H13.98C14.16 21 14.31 20.85 14.31 20.66C14.31 19.59 14.88 18.61 15.81 18.07C16.28 17.8 16.8 17.66 17.33 17.66C17.85 17.66 18.37 17.8 18.83 18.06C18.99 18.16 19.19 18.1 19.28 17.94L20.96 15.06C20.99 15.01 21 14.95 21 14.9C21 14.78 20.94 14.67 20.83 14.6ZM12 15C10.34 15 9 13.66 9 12C9 10.34 10.34 9 12 9C13.66 9 15 10.34 15 12C15 13.66 13.66 15 12 15Z"
fill="currentColor" />
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -0,0 +1,37 @@
<svg width="74" height="72" viewBox="0 0 74 72" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M24.3747 21.5043C24.671 21.5043 24.9274 21.7105 24.991 21.9998C25.1664 22.7982 25.1791 24.1098 24.6141 25.1601C24.322 25.703 23.8683 26.1865 23.2031 26.4658C22.5432 26.7429 21.7375 26.7931 20.7823 26.5871C19.5609 26.3237 18.3363 25.6271 17.1951 24.7335C16.0477 23.8349 14.9472 22.7077 13.9724 21.5319C12.9967 20.3551 12.137 19.1177 11.4739 17.9903C10.8173 16.8739 10.3306 15.8269 10.132 15.0366C10.059 14.7459 10.2005 14.444 10.4706 14.3141C10.7407 14.1843 11.0649 14.2624 11.2463 14.501C12.087 15.6072 14.0213 17.3714 16.4539 18.8598C18.8877 20.3489 21.7272 21.5043 24.3747 21.5043Z" fill="white" stroke="black" stroke-width="1.26191" stroke-linejoin="round"/>
<mask id="path-2-outside-1_1930_115608" maskUnits="userSpaceOnUse" x="0.0805664" y="18.0469" width="56" height="43" fill="black">
<rect fill="white" x="0.0805664" y="18.0469" width="56" height="43"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M48.4887 52.6749C51.8537 49.2159 53.8899 44.7033 53.8899 39.7671C53.8899 28.8759 43.9775 20.0469 31.7499 20.0469C19.5224 20.0469 9.60998 28.8759 9.60998 39.7671C9.60998 40.805 9.69999 41.8241 9.87343 42.8186C5.25341 44.2149 2.08057 47.1039 2.08057 50.4413C2.08057 55.1444 8.38116 58.9569 16.1533 58.9569C18.6361 58.9569 20.9687 58.5679 22.9937 57.885C25.6793 58.9161 28.6397 59.4874 31.7499 59.4874C38.4356 59.4874 44.4292 56.8479 48.4887 52.6749Z"/>
</mask>
<path fill-rule="evenodd" clip-rule="evenodd" d="M48.4887 52.6749C51.8537 49.2159 53.8899 44.7033 53.8899 39.7671C53.8899 28.8759 43.9775 20.0469 31.7499 20.0469C19.5224 20.0469 9.60998 28.8759 9.60998 39.7671C9.60998 40.805 9.69999 41.8241 9.87343 42.8186C5.25341 44.2149 2.08057 47.1039 2.08057 50.4413C2.08057 55.1444 8.38116 58.9569 16.1533 58.9569C18.6361 58.9569 20.9687 58.5679 22.9937 57.885C25.6793 58.9161 28.6397 59.4874 31.7499 59.4874C38.4356 59.4874 44.4292 56.8479 48.4887 52.6749Z" fill="white"/>
<path d="M48.4887 52.6749L49.3932 53.5549L48.4887 52.6749ZM9.87343 42.8186L10.2385 44.0265L11.3086 43.7031L11.1166 42.6018L9.87343 42.8186ZM22.9937 57.885L23.446 56.707L23.0214 56.544L22.5905 56.6893L22.9937 57.885ZM52.628 39.7671C52.628 44.3432 50.743 48.548 47.5842 51.795L49.3932 53.5549C52.9645 49.8838 55.1518 45.0634 55.1518 39.7671H52.628ZM31.7499 21.3088C43.4219 21.3088 52.628 29.7062 52.628 39.7671H55.1518C55.1518 28.0457 44.5332 18.785 31.7499 18.785V21.3088ZM10.8719 39.7671C10.8719 29.7062 20.078 21.3088 31.7499 21.3088V18.785C18.9667 18.785 8.34807 28.0457 8.34807 39.7671H10.8719ZM11.1166 42.6018C10.9555 41.6784 10.8719 40.7318 10.8719 39.7671H8.34807C8.34807 40.8781 8.44444 41.9697 8.63029 43.0354L11.1166 42.6018ZM3.34248 50.4413C3.34248 47.9707 5.77784 45.3747 10.2385 44.0265L9.50835 41.6106C4.72899 43.0551 0.818657 46.2371 0.818657 50.4413H3.34248ZM16.1533 57.695C12.4585 57.695 9.17438 56.7862 6.85568 55.3831C4.5135 53.9658 3.34248 52.1807 3.34248 50.4413H0.818657C0.818657 53.405 2.79793 55.8776 5.54909 57.5424C8.32373 59.2214 12.076 60.2188 16.1533 60.2188V57.695ZM22.5905 56.6893C20.7028 57.3258 18.5071 57.695 16.1533 57.695V60.2188C18.7651 60.2188 21.2345 59.8099 23.3969 59.0808L22.5905 56.6893ZM31.7499 58.2255C28.7946 58.2255 25.9875 57.6827 23.446 56.707L22.5414 59.0631C25.3711 60.1496 28.4849 60.7493 31.7499 60.7493V58.2255ZM47.5842 51.795C43.7692 55.7165 38.1051 58.2255 31.7499 58.2255V60.7493C38.7662 60.7493 45.0891 57.9792 49.3932 53.5549L47.5842 51.795Z" fill="black" mask="url(#path-2-outside-1_1930_115608)"/>
<path d="M39.0719 21.5043C38.7762 21.5043 38.5201 21.7097 38.456 21.9984C38.2783 22.7979 38.2654 24.1112 38.8376 25.1627C39.1334 25.7063 39.592 26.1886 40.2622 26.4668C40.9266 26.7426 41.7385 26.793 42.7026 26.5874C43.9355 26.3246 45.1727 25.6291 46.3268 24.7356C47.487 23.8374 48.6 22.7104 49.586 21.5347C50.5728 20.3579 51.4423 19.1205 52.1131 17.993C52.7773 16.8766 53.2698 15.8293 53.4709 15.0382C53.5446 14.7481 53.4043 14.4461 53.135 14.3153C52.8657 14.1845 52.5416 14.2609 52.3592 14.4982C51.5085 15.6046 49.5517 17.3692 47.0903 18.8581C44.6282 20.3475 41.7536 21.5043 39.0719 21.5043Z" fill="white" stroke="black" stroke-width="1.26191" stroke-linejoin="round"/>
<path d="M9.27317 46.8448L9.50259 46.4782C9.93019 45.7949 9.38096 45.0323 8.64133 45.3527C7.81405 45.7111 6.82196 46.2689 5.87019 47.1255C5.44297 47.51 5.12141 47.8905 4.88077 48.2545C3.88061 49.7674 5.42411 51.2381 7.23768 51.2541C8.19162 51.2626 9.28694 51.2174 10.3799 51.0555C11.8292 50.8408 13.0434 50.1513 13.916 49.4901C14.561 49.0012 14.1621 48.1301 13.3534 48.1626L10.1134 48.2925C9.35325 48.323 8.8696 47.4897 9.27317 46.8448Z" fill="black"/>
<path d="M22.3604 55.8761C23.8648 55.9852 25.2756 55.8261 26.3472 55.4561C26.8819 55.2714 27.362 55.024 27.7263 54.7048C28.0935 54.3831 28.3749 53.9571 28.4127 53.4357C28.4505 52.9142 28.2335 52.4521 27.9166 52.0807C27.6022 51.7123 27.1628 51.3982 26.6604 51.1384C25.6534 50.6176 24.2803 50.2565 22.7758 50.1474C21.2714 50.0383 19.8606 50.1974 18.789 50.5675C18.2544 50.7521 17.7742 50.9995 17.4099 51.3187C17.0428 51.6404 16.7613 52.0665 16.7235 52.5879C16.6857 53.1093 16.9027 53.5714 17.2196 53.9428C17.534 54.3112 17.9735 54.6253 18.4759 54.8852C19.4829 55.4059 20.8559 55.767 22.3604 55.8761Z" fill="white" stroke="black" stroke-width="1.26191"/>
<line y1="-0.630955" x2="10.458" y2="-0.630955" transform="matrix(0.99738 0.0723377 -0.0723344 0.99738 17.2944 53.4453)" stroke="black" stroke-width="1.26191"/>
<line y1="-0.630955" x2="5.19898" y2="-0.630955" transform="matrix(-0.0723344 0.99738 -0.99738 -0.0723377 23.6921 50.0703)" stroke="black" stroke-width="1.26191"/>
<line y1="-0.630955" x2="5.19898" y2="-0.630955" transform="matrix(-0.0723344 0.99738 -0.99738 -0.0723377 19.9366 50.5742)" stroke="black" stroke-width="1.26191"/>
<path d="M23.6113 12.1879C23.4573 11.9346 23.1478 11.8226 22.8674 11.9188C22.587 12.015 22.4114 12.2934 22.4453 12.5878L23.4514 21.3197C23.618 22.7653 24.9556 23.7808 26.3928 23.5526C28.1885 23.2675 29.1214 21.2523 28.1769 19.6986L23.6113 12.1879Z" fill="url(#paint0_linear_1930_115608)" stroke="black" stroke-width="1.26191" stroke-linejoin="round"/>
<mask id="path-11-outside-2_1930_115608" maskUnits="userSpaceOnUse" x="53.3934" y="34.8259" width="22.6706" height="24.8274" fill="black">
<rect fill="white" x="53.3934" y="34.8259" width="22.6706" height="24.8274"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M57.6261 54.012C57.135 53.9721 56.7516 53.8134 56.5049 53.5249C55.2906 52.1043 57.8521 48.0728 62.2261 44.5205C66.6002 40.9681 71.1305 39.24 72.3449 40.6606C73.078 41.5183 72.435 43.3274 70.8479 45.4082C71.665 45.4935 72.234 45.7574 72.4517 46.2109C73.1324 47.6291 70.1196 50.3641 65.7223 52.3195C62.4196 53.7883 59.2752 54.3996 57.6261 54.012Z"/>
</mask>
<path fill-rule="evenodd" clip-rule="evenodd" d="M57.6261 54.012C57.135 53.9721 56.7516 53.8134 56.5049 53.5249C55.2906 52.1043 57.8521 48.0728 62.2261 44.5205C66.6002 40.9681 71.1305 39.24 72.3449 40.6606C73.078 41.5183 72.435 43.3274 70.8479 45.4082C71.665 45.4935 72.234 45.7574 72.4517 46.2109C73.1324 47.6291 70.1196 50.3641 65.7223 52.3195C62.4196 53.7883 59.2752 54.3996 57.6261 54.012Z" fill="url(#paint1_linear_1930_115608)"/>
<path d="M57.6261 54.012L57.9148 52.7835C57.8534 52.7691 57.791 52.7593 57.7282 52.7542L57.6261 54.012ZM56.5049 53.5249L55.5457 54.3449L55.5457 54.3449L56.5049 53.5249ZM62.2261 44.5205L61.4306 43.5409L62.2261 44.5205ZM72.3449 40.6606L71.3857 41.4806L71.3857 41.4806L72.3449 40.6606ZM70.8479 45.4082L69.8445 44.6428C69.5678 45.0056 69.5085 45.4895 69.6893 45.9084C69.8702 46.3272 70.2631 46.6159 70.7169 46.6633L70.8479 45.4082ZM72.4517 46.2109L71.314 46.757L72.4517 46.2109ZM65.7223 52.3195L66.2351 53.4726L65.7223 52.3195ZM57.7282 52.7542C57.5875 52.7428 57.5078 52.7177 57.4715 52.7021C57.44 52.6886 57.4475 52.6855 57.4642 52.705L55.5457 54.3449C56.0816 54.9718 56.8316 55.2135 57.524 55.2697L57.7282 52.7542ZM57.4642 52.705C57.5255 52.7768 57.4073 52.7335 57.5045 52.2864C57.5986 51.8541 57.859 51.248 58.335 50.4988C59.279 49.0131 60.9099 47.2151 63.0217 45.5L61.4306 43.5409C59.1683 45.3782 57.3314 47.3721 56.2048 49.1453C55.6454 50.0257 55.2198 50.9159 55.0384 51.75C54.8602 52.5693 54.8772 53.5627 55.5457 54.3449L57.4642 52.705ZM63.0217 45.5C65.1329 43.7855 67.244 42.5441 68.9202 41.9047C69.7645 41.5827 70.4326 41.4388 70.9004 41.4263C71.3906 41.4132 71.4384 41.5422 71.3857 41.4806L73.3041 39.8407C72.6442 39.0687 71.6703 38.881 70.8329 38.9034C69.973 38.9264 69.0075 39.1702 68.0207 39.5466C66.0346 40.3042 63.6935 41.7031 61.4306 43.5409L63.0217 45.5ZM71.3857 41.4806C71.3132 41.3958 71.4919 41.4945 71.2668 42.1842C71.0603 42.8169 70.5933 43.6612 69.8445 44.6428L71.8512 46.1735C72.6896 45.0744 73.3376 43.9737 73.6661 42.9673C73.976 42.0178 74.1096 40.7831 73.3041 39.8407L71.3857 41.4806ZM70.7169 46.6633C71.0333 46.6963 71.2227 46.7572 71.3185 46.8042C71.4057 46.8471 71.3576 46.8477 71.314 46.757L73.5893 45.6649C73.0698 44.5825 71.8893 44.2481 70.9789 44.1531L70.7169 46.6633ZM71.314 46.757C71.2372 46.5968 71.3566 46.582 71.1885 46.902C71.0285 47.2068 70.6953 47.6281 70.1431 48.1294C69.05 49.1216 67.321 50.2275 65.2095 51.1665L66.2351 53.4726C68.5208 52.4561 70.4969 51.2168 71.8394 49.9981C72.5049 49.394 73.0722 48.7434 73.423 48.0755C73.7658 47.4227 74.0066 46.5342 73.5893 45.6649L71.314 46.757ZM65.2095 51.1665C63.6302 51.8688 62.1053 52.3583 60.8007 52.6257C59.4622 52.9 58.4759 52.9154 57.9148 52.7835L57.3374 55.2404C58.4254 55.4961 59.8358 55.3997 61.3075 55.0981C62.813 54.7896 64.5117 54.239 66.2351 53.4726L65.2095 51.1665Z" fill="black" mask="url(#path-11-outside-2_1930_115608)"/>
<ellipse cx="14.8446" cy="37.3595" rx="3.31248" ry="4.96883" fill="black"/>
<ellipse cx="14.9235" cy="37.3605" rx="1.4985" ry="2.60272" fill="white"/>
<ellipse cx="27.7795" cy="37.3595" rx="3.31248" ry="4.96883" fill="black"/>
<ellipse cx="27.8581" cy="37.3605" rx="1.4985" ry="2.60272" fill="white"/>
<path d="M34.2463 54.3156C34.2463 51.2381 35.3257 47.5016 37.9302 45.6216C40.1969 43.9853 43.0541 43.828 45.7611 44.284" stroke="black" stroke-width="1.26191"/>
<defs>
<linearGradient id="paint0_linear_1930_115608" x1="27.4989" y1="4.1016" x2="19.0152" y2="20.2113" gradientUnits="userSpaceOnUse">
<stop stop-color="#FF24A7"/>
<stop offset="1" stop-color="white"/>
</linearGradient>
<linearGradient id="paint1_linear_1930_115608" x1="66.7203" y1="38.9751" x2="63.2492" y2="55.4808" gradientUnits="userSpaceOnUse">
<stop stop-color="#FF24A7"/>
<stop offset="1" stop-color="white"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 10 KiB

View File

@@ -1,24 +0,0 @@
<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_13571_129912)">
<rect width="40" height="40" fill="url(#paint0_linear_13571_129912)"/>
<path d="M20 40C31.0457 40 40 31.0457 40 20C40 8.9543 31.0457 0 20 0C8.9543 0 0 8.9543 0 20C0 31.0457 8.9543 40 20 40Z" fill="url(#paint1_linear_13571_129912)"/>
<path d="M34.5575 20.2857H30.9819C30.9819 13.0516 25.0541 7.1875 17.7414 7.1875C10.5191 7.1875 4.64737 12.908 4.50368 20.0182C4.35499 27.3678 11.3253 33.75 18.7558 33.75H19.6904C26.2413 33.75 35.0216 28.6771 36.3934 22.4961C36.6469 21.3567 35.7369 20.2857 34.5575 20.2857ZM12.4278 20.6079C12.4278 21.5753 11.628 22.3665 10.6501 22.3665C9.67215 22.3665 8.87237 21.575 8.87237 20.6079V17.7629C8.87237 16.7955 9.67215 16.0043 10.6501 16.0043C11.628 16.0043 12.4278 16.7955 12.4278 17.7629V20.6079ZM18.6007 20.6079C18.6007 21.5753 17.801 22.3665 16.8231 22.3665C15.8451 22.3665 15.0453 21.575 15.0453 20.6079V17.7629C15.0453 16.7955 15.8455 16.0043 16.8231 16.0043C17.801 16.0043 18.6007 16.7955 18.6007 17.7629V20.6079Z" fill="url(#paint2_linear_13571_129912)"/>
</g>
<defs>
<linearGradient id="paint0_linear_13571_129912" x1="20" y1="0" x2="20" y2="40" gradientUnits="userSpaceOnUse">
<stop stop-color="#534BB1"/>
<stop offset="1" stop-color="#551BF9"/>
</linearGradient>
<linearGradient id="paint1_linear_13571_129912" x1="20" y1="0" x2="20" y2="40" gradientUnits="userSpaceOnUse">
<stop stop-color="#534BB1"/>
<stop offset="1" stop-color="#551BF9"/>
</linearGradient>
<linearGradient id="paint2_linear_13571_129912" x1="20.4687" y1="7.1875" x2="20.4687" y2="33.75" gradientUnits="userSpaceOnUse">
<stop stop-color="white"/>
<stop offset="1" stop-color="white" stop-opacity="0.82"/>
</linearGradient>
<clipPath id="clip0_13571_129912">
<rect width="40" height="40" fill="white"/>
</clipPath>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 1.8 KiB

View File

@@ -1,5 +1,6 @@
import { TraceEvent } from '@uniswap/analytics'
import { BrowserEvent, InterfaceElementName, SharedEventName } from '@uniswap/analytics-events'
import { TraceEvent } from 'analytics'
import { useDisableNFTRoutes } from 'hooks/useDisableNFTRoutes'
import styled from 'styled-components/macro'
import { BREAKPOINTS, ExternalLink, StyledRouterLink } from 'theme'
import { useIsDarkMode } from 'theme/components/ThemeToggle'
@@ -137,6 +138,7 @@ const LogoSectionContent = () => {
}
export const AboutFooter = () => {
const shouldDisableNFTRoutes = useDisableNFTRoutes()
return (
<Footer>
<LogoSectionLeft>
@@ -148,7 +150,7 @@ export const AboutFooter = () => {
<LinkGroupTitle>App</LinkGroupTitle>
<TextLink to="/swap">Swap</TextLink>
<TextLink to="/tokens">Tokens</TextLink>
<TextLink to="/nfts">NFTs</TextLink>
{!shouldDisableNFTRoutes && <TextLink to="/nfts">NFTs</TextLink>}
<TextLink to="/pools">Pools</TextLink>
</LinkGroup>
<LinkGroup>

View File

@@ -1,5 +1,5 @@
import { TraceEvent } from '@uniswap/analytics'
import { BrowserEvent, SharedEventName } from '@uniswap/analytics-events'
import { TraceEvent } from 'analytics'
import { Link } from 'react-router-dom'
import styled, { DefaultTheme } from 'styled-components/macro'
import { BREAKPOINTS } from 'theme'

View File

@@ -283,7 +283,7 @@ function SwapSummary({ info }: { info: ExactInputSwapTransactionInfo | ExactOutp
/>{' '}
for{' '}
<FormattedCurrencyAmountManaged
rawAmount={info.expectedOutputCurrencyAmountRaw}
rawAmount={info.settledOutputCurrencyAmountRaw ?? info.expectedOutputCurrencyAmountRaw}
currencyId={info.outputCurrencyId}
sigFigs={6}
/>

View File

@@ -0,0 +1,18 @@
import { t } from '@lingui/macro'
import { allowAnalyticsAtom } from 'analytics'
import { useAtom } from 'jotai'
import { SettingsToggle } from './SettingsToggle'
export function AnalyticsToggle() {
const [allowAnalytics, updateAllowAnalytics] = useAtom(allowAnalyticsAtom)
return (
<SettingsToggle
title={t`Allow analytics`}
description={t`We use anonymized data to enhance your experience with Uniswap Labs products.`}
isActive={allowAnalytics}
toggle={() => void updateAllowAnalytics((value) => !value)}
/>
)
}

View File

@@ -1,9 +1,9 @@
import { Trans } from '@lingui/macro'
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 { sendAnalyticsEvent, TraceEvent } from 'analytics'
import { ButtonEmphasis, ButtonSize, LoadingButtonSpinner, ThemeButton } from 'components/Button'
import Column from 'components/Column'
import { AutoRow } from 'components/Row'
@@ -11,15 +11,14 @@ import { LoadingBubble } from 'components/Tokens/loading'
import { formatDelta } from 'components/Tokens/TokenDetails/PriceChart'
import Tooltip from 'components/Tooltip'
import { getConnection } from 'connection'
import { usePortfolioBalancesQuery } from 'graphql/data/__generated__/types-and-hooks'
import { useAtomValue } from 'jotai/utils'
import { useDisableNFTRoutes } from 'hooks/useDisableNFTRoutes'
import useENSName from 'hooks/useENSName'
import { useProfilePageState, useSellAsset, useWalletCollections } from 'nft/hooks'
import { useIsNftClaimAvailable } from 'nft/hooks/useIsNftClaimAvailable'
import { ProfilePageStateType } from 'nft/types'
import { useCallback, useState } from 'react'
import { ArrowDownRight, ArrowUpRight, CreditCard, IconProps, Info, LogOut, Settings } from 'react-feather'
import { useNavigate } from 'react-router-dom'
import { shouldDisableNFTRoutesAtom } from 'state/application/atoms'
import { useAppDispatch } from 'state/hooks'
import { updateSelectedWallet } from 'state/user/reducer'
import styled, { useTheme } from 'styled-components/macro'
@@ -34,13 +33,13 @@ import { useToggleAccountDrawer } from '.'
import IconButton, { IconHoverText, IconWithConfirmTextButton } from './IconButton'
import MiniPortfolio from './MiniPortfolio'
import { portfolioFadeInAnimation } from './MiniPortfolio/PortfolioRow'
import { useCachedPortfolioBalancesQuery } from './PrefetchBalancesWrapper'
const AuthenticatedHeaderWrapper = styled.div`
padding: 20px 16px;
display: flex;
flex-direction: column;
flex: 1;
overflow: auto;
`
const HeaderButton = styled(ThemeButton)`
@@ -105,7 +104,7 @@ const StatusWrapper = styled.div`
display: inline-block;
width: 70%;
max-width: 70%;
padding-right: 14px;
padding-right: 8px;
display: inline-flex;
`
@@ -162,7 +161,8 @@ const LogOutCentered = styled(LogOut)`
`
export default function AuthenticatedHeader({ account, openSettings }: { account: string; openSettings: () => void }) {
const { connector, ENSName } = useWeb3React()
const { connector } = useWeb3React()
const { ENSName } = useENSName(account)
const dispatch = useAppDispatch()
const navigate = useNavigate()
const closeModal = useCloseModal()
@@ -171,7 +171,7 @@ export default function AuthenticatedHeader({ account, openSettings }: { account
const clearCollectionFilters = useWalletCollections((state) => state.clearCollectionFilters)
const isClaimAvailable = useIsNftClaimAvailable((state) => state.isClaimAvailable)
const shouldDisableNFTRoutes = useAtomValue(shouldDisableNFTRoutesAtom)
const shouldDisableNFTRoutes = useDisableNFTRoutes()
const unclaimedAmount: CurrencyAmount<Token> | undefined = useUserUnclaimedAmount(account)
const isUnclaimed = useUserHasAvailableClaim(account)
@@ -226,10 +226,7 @@ export default function AuthenticatedHeader({ account, openSettings }: { account
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 { data: portfolioBalances } = useCachedPortfolioBalancesQuery({ account })
const portfolio = portfolioBalances?.portfolios?.[0]
const totalBalance = portfolio?.tokensTotalDenominatedValue?.value
const absoluteChange = portfolio?.tokensTotalDenominatedValueChange?.absolute?.value
@@ -256,9 +253,12 @@ export default function AuthenticatedHeader({ account, openSettings }: { account
)}
</StatusWrapper>
<IconContainer>
{!showDisconnectConfirm && (
<IconButton data-testid="wallet-settings" onClick={openSettings} Icon={Settings} />
)}
<IconButton
hideHorizontal={showDisconnectConfirm}
data-testid="wallet-settings"
onClick={openSettings}
Icon={Settings}
/>
<TraceEvent
events={[BrowserEvent.onClick]}
name={SharedEventName.ELEMENT_CLICKED}
@@ -270,6 +270,7 @@ export default function AuthenticatedHeader({ account, openSettings }: { account
onShowConfirm={setShowDisconnectConfirm}
Icon={LogOutCentered}
text="Disconnect"
dismissOnHoverOut
/>
</TraceEvent>
</IconContainer>

View File

@@ -1,7 +1,7 @@
import { useWeb3React } from '@web3-react/core'
import Column from 'components/Column'
import WalletModal from 'components/WalletModal'
import { useCallback, useState } from 'react'
import { useCallback, useEffect, useState } from 'react'
import styled from 'styled-components/macro'
import AuthenticatedHeader from './AuthenticatedHeader'
@@ -17,7 +17,7 @@ enum MenuState {
SETTINGS,
}
function DefaultMenu() {
function DefaultMenu({ drawerOpen }: { drawerOpen: boolean }) {
const { account } = useWeb3React()
const isAuthenticated = !!account
@@ -25,6 +25,17 @@ function DefaultMenu() {
const openSettings = useCallback(() => setMenu(MenuState.SETTINGS), [])
const closeSettings = useCallback(() => setMenu(MenuState.DEFAULT), [])
useEffect(() => {
if (!drawerOpen && menu === MenuState.SETTINGS) {
// wait for the drawer to close before resetting the menu
const timer = setTimeout(() => {
closeSettings()
}, 250)
return () => clearTimeout(timer)
}
return
}, [drawerOpen, menu, closeSettings])
return (
<DefaultMenuWrap>
{menu === MenuState.DEFAULT &&

View File

@@ -1,9 +1,8 @@
import { sendAnalyticsEvent } from '@uniswap/analytics'
import { InterfaceElementName, InterfaceEventName, SharedEventName } from '@uniswap/analytics-events'
import { InterfaceElementName } from '@uniswap/analytics-events'
import { PropsWithChildren, useCallback } from 'react'
import styled from 'styled-components/macro'
import { ClickableStyle } from 'theme'
import { isIOS } from 'utils/userAgent'
import { openDownloadApp } from 'utils/openDownloadApp'
const StyledButton = styled.button<{ padded?: boolean; branded?: boolean }>`
${ClickableStyle}
@@ -32,23 +31,6 @@ function BaseButton({ onClick, branded, children }: PropsWithChildren<{ onClick?
)
}
const APP_STORE_LINK = 'https://apps.apple.com/app/apple-store/id6443944476?pt=123625782&ct=In-App-Banners&mt=8'
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,
@@ -62,7 +44,7 @@ export function DownloadButton({
const onButtonClick = useCallback(() => {
// handles any actions required by the parent, i.e. cancelling wallet connection attempt or dismissing an ad
onClick?.()
openDownloadApp(element)
openDownloadApp({ element })
}, [element, onClick])
return (

View File

@@ -1,8 +1,9 @@
import React, { forwardRef, useCallback, useEffect, useRef, useState } from 'react'
import { Icon } from 'react-feather'
import styled, { css } from 'styled-components/macro'
import styled, { css, DefaultTheme } from 'styled-components/macro'
import useResizeObserver from 'use-resize-observer'
import { TRANSITION_DURATIONS } from '../../theme/styles'
import Row from '../Row'
export const IconHoverText = styled.span`
@@ -17,11 +18,12 @@ export const IconHoverText = styled.span`
left: 10px;
`
const widthTransition = `width ease-in 80ms`
const getWidthTransition = ({ theme }: { theme: DefaultTheme }) =>
`width ${theme.transition.timing.inOut} ${theme.transition.duration.fast}`
const IconStyles = css`
const IconStyles = css<{ hideHorizontal?: boolean }>`
background-color: ${({ theme }) => theme.backgroundInteractive};
transition: ${widthTransition};
transition: ${getWidthTransition};
border-radius: 12px;
display: flex;
padding: 0;
@@ -29,7 +31,7 @@ const IconStyles = css`
position: relative;
overflow: hidden;
height: 32px;
width: 32px;
width: ${({ hideHorizontal }) => (hideHorizontal ? '0px' : '32px')};
color: ${({ theme }) => theme.textPrimary};
:hover {
background-color: ${({ theme }) => theme.hoverState};
@@ -37,7 +39,7 @@ const IconStyles = css`
theme: {
transition: { duration, timing },
},
}) => `${duration.fast} background-color ${timing.in}, ${widthTransition}`};
}) => `${duration.fast} background-color ${timing.in}, ${getWidthTransition}`};
${IconHoverText} {
opacity: 1;
@@ -45,7 +47,7 @@ const IconStyles = css`
}
:active {
background-color: ${({ theme }) => theme.backgroundSurface};
transition: background-color 50ms linear, ${widthTransition};
transition: background-color ${({ theme }) => theme.transition.duration.fast} linear, ${getWidthTransition};
}
`
@@ -67,6 +69,7 @@ const IconWrapper = styled.span`
`
interface BaseProps {
Icon: Icon
hideHorizontal?: boolean
children?: React.ReactNode
}
@@ -96,6 +99,8 @@ type IconWithTextProps = (IconButtonProps | IconLinkProps) & {
text: string
onConfirm?: () => void
onShowConfirm?: (on: boolean) => void
dismissOnHoverOut?: boolean
dismissOnHoverDurationMs?: number
}
const TextWrapper = styled.div`
@@ -107,6 +112,8 @@ const TextWrapper = styled.div`
const TextHide = styled.div`
overflow: hidden;
transition: width ${({ theme }) => theme.transition.timing.inOut} ${({ theme }) => theme.transition.duration.fast},
max-width ${({ theme }) => theme.transition.timing.inOut} ${({ theme }) => theme.transition.duration.fast};
`
/**
@@ -120,9 +127,12 @@ export const IconWithConfirmTextButton = ({
onConfirm,
onShowConfirm,
onClick,
dismissOnHoverOut,
dismissOnHoverDurationMs = TRANSITION_DURATIONS.slow,
...rest
}: IconWithTextProps) => {
const [showText, setShowTextWithoutCallback] = useState(false)
const [frame, setFrame] = useState<HTMLElement | null>()
const frameObserver = useResizeObserver<HTMLElement>()
const hiddenObserver = useResizeObserver<HTMLElement>()
@@ -136,41 +146,60 @@ export const IconWithConfirmTextButton = ({
const dimensionsRef = useRef({
frame: 0,
hidden: 0,
innerText: 0,
})
const dimensions = (() => {
// once opened, we avoid updating it to prevent constant resize loop
if (!showText) {
dimensionsRef.current = { frame: frameObserver.width || 0, hidden: hiddenObserver.width || 0 }
dimensionsRef.current = { frame: frameObserver.width || 0, innerText: hiddenObserver.width || 0 }
}
return dimensionsRef.current
})()
// keyboard action to cancel
useEffect(() => {
if (!showText) return
const isClient = typeof window !== 'undefined'
if (!isClient) return
if (!showText) return
const keyHandler = (e: KeyboardEvent) => {
if (e.key === 'Escape') {
setShowText(false)
e.preventDefault()
e.stopPropagation()
if (typeof window === 'undefined') return
if (!showText || !frame) return
const closeAndPrevent = (e: Event) => {
setShowText(false)
e.preventDefault()
e.stopPropagation()
}
const clickHandler = (e: MouseEvent) => {
const { target } = e
const shouldClose = !(target instanceof HTMLElement) || !frame.contains(target)
if (shouldClose) {
closeAndPrevent(e)
}
}
const keyHandler = (e: KeyboardEvent) => {
if (e.key === 'Escape') {
closeAndPrevent(e)
}
}
window.addEventListener('click', clickHandler, { capture: true })
window.addEventListener('keydown', keyHandler, { capture: true })
return () => {
window.removeEventListener('click', clickHandler, { capture: true })
window.removeEventListener('keydown', keyHandler, { capture: true })
}
}, [setShowText, showText])
}, [frame, setShowText, showText])
const xPad = showText ? 12 : 0
const width = showText ? dimensions.frame + dimensions.hidden + xPad : 32
const xPad = showText ? 8 : 0
const width = showText ? dimensions.frame + dimensions.innerText + xPad : 32
const mouseLeaveTimeout = useRef<NodeJS.Timeout>()
return (
<IconBlock
ref={frameObserver.ref}
ref={(node) => {
frameObserver.ref(node)
setFrame(node)
}}
{...rest}
style={{
width,
@@ -187,6 +216,18 @@ export const IconWithConfirmTextButton = ({
setShowText(!showText)
}
}}
{...(dismissOnHoverOut && {
onMouseLeave() {
mouseLeaveTimeout.current = setTimeout(() => {
setShowText(false)
}, dismissOnHoverDurationMs)
},
onMouseEnter() {
if (mouseLeaveTimeout.current) {
clearTimeout(mouseLeaveTimeout.current)
}
},
})}
>
<Row height="100%" gap="xs">
<IconWrapper>
@@ -196,8 +237,11 @@ export const IconWithConfirmTextButton = ({
{/* this outer div is so we can cut it off but keep the inner text width full-width so we can measure it */}
<TextHide
style={{
maxWidth: showText ? dimensions.hidden : 0,
minWidth: showText ? dimensions.hidden : 0,
maxWidth: showText ? dimensions.innerText : 0,
width: showText ? dimensions.innerText : 0,
// this negative transform offsets for the shift it does due to being 0 width
transform: showText ? undefined : `translateX(-8px)`,
minWidth: showText ? dimensions.innerText : 0,
}}
>
<TextWrapper ref={hiddenObserver.ref}>{text}</TextWrapper>

View File

@@ -1,10 +1,12 @@
import { TraceEvent } from '@uniswap/analytics'
import { BrowserEvent, InterfaceElementName, SharedEventName } from '@uniswap/analytics-events'
import { TraceEvent } from 'analytics'
import Column from 'components/Column'
import AlertTriangleFilled from 'components/Icons/AlertTriangleFilled'
import { LoaderV2 } from 'components/Icons/LoadingSpinner'
import Row from 'components/Row'
import { TransactionStatus } from 'graphql/data/__generated__/types-and-hooks'
import useENSName from 'hooks/useENSName'
import { useCallback } from 'react'
import styled from 'styled-components/macro'
import { EllipsisStyle, ThemedText } from 'theme'
import { shortenAddress } from 'utils'
@@ -12,6 +14,7 @@ import { ExplorerDataType, getExplorerLink } from 'utils/getExplorerLink'
import { PortfolioLogo } from '../PortfolioLogo'
import PortfolioRow from '../PortfolioRow'
import { useOpenOffchainActivityModal } from './OffchainActivityModal'
import { useTimeSince } from './parseRemote'
import { Activity } from './types'
@@ -26,16 +29,36 @@ const StyledTimestamp = styled(ThemedText.Caption)`
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)
function StatusIndicator({ activity: { status, timestamp } }: { activity: Activity }) {
const timeSince = useTimeSince(timestamp)
switch (status) {
case TransactionStatus.Pending:
return <LoaderV2 />
case TransactionStatus.Confirmed:
return <StyledTimestamp>{timeSince}</StyledTimestamp>
case TransactionStatus.Failed:
return <AlertTriangleFilled />
}
}
export function ActivityRow({ activity }: { activity: Activity }) {
const { chainId, title, descriptor, logos, otherAccount, currencies, hash, prefixIconSrc, offchainOrderStatus } =
activity
const openOffchainActivityModal = useOpenOffchainActivityModal()
const { ENSName } = useENSName(otherAccount)
const explorerUrl = getExplorerLink(chainId, hash, ExplorerDataType.TRANSACTION)
const onClick = useCallback(() => {
if (offchainOrderStatus) {
openOffchainActivityModal({ orderHash: hash, status: offchainOrderStatus })
return
}
window.open(getExplorerLink(chainId, hash, ExplorerDataType.TRANSACTION), '_blank')
}, [offchainOrderStatus, chainId, hash, openOffchainActivityModal])
return (
<TraceEvent
events={[BrowserEvent.onClick]}
@@ -49,23 +72,20 @@ export function ActivityRow({
<PortfolioLogo chainId={chainId} currencies={currencies} images={logos} accountAddress={otherAccount} />
</Column>
}
title={<ThemedText.SubHeader>{title}</ThemedText.SubHeader>}
title={
<Row gap="4px">
{prefixIconSrc && <img height="14px" width="14px" src={prefixIconSrc} alt="" />}
<ThemedText.SubHeader>{title}</ThemedText.SubHeader>
</Row>
}
descriptor={
<ActivityRowDescriptor color="textSecondary">
{descriptor}
{ENSName ?? shortenAddress(otherAccount)}
</ActivityRowDescriptor>
}
right={
status === TransactionStatus.Pending ? (
<LoaderV2 />
) : status === TransactionStatus.Confirmed ? (
<StyledTimestamp>{timeSince}</StyledTimestamp>
) : (
<AlertTriangleFilled />
)
}
onClick={() => window.open(explorerUrl, '_blank')}
right={<StatusIndicator activity={activity} />}
onClick={onClick}
/>
</TraceEvent>
)

View File

@@ -0,0 +1,258 @@
import { t, Trans } from '@lingui/macro'
import { CurrencyAmount, TradeType } from '@uniswap/sdk-core'
import { ReactComponent as ErrorContent } from 'assets/svg/uniswapx_error.svg'
import Column, { AutoColumn } from 'components/Column'
import { OpacityHoverState } from 'components/Common'
import { LoaderV3 } from 'components/Icons/LoadingSpinner'
import Modal from 'components/Modal'
import { AnimatedEntranceConfirmationIcon, FadePresence } from 'components/swap/PendingModalContent/Logos'
import { TradeSummary } from 'components/swap/PendingModalContent/TradeSummary'
import { useCurrency } from 'hooks/Tokens'
import { atom } from 'jotai'
import { useAtomValue, useUpdateAtom } from 'jotai/utils'
import { UniswapXOrderStatus } from 'lib/hooks/orders/types'
import { useCallback, useMemo } from 'react'
import { X } from 'react-feather'
import { InterfaceTrade } from 'state/routing/types'
import { useOrder } from 'state/signatures/hooks'
import { UniswapXOrderDetails } from 'state/signatures/types'
import styled from 'styled-components/macro'
import { ExternalLink, ThemedText } from 'theme'
import { ExplorerDataType, getExplorerLink } from 'utils/getExplorerLink'
type SelectedOrderInfo = {
modalOpen?: boolean
orderHash: string
status: UniswapXOrderStatus
details?: UniswapXOrderDetails
}
const selectedOrderAtom = atom<SelectedOrderInfo | undefined>(undefined)
export function useOpenOffchainActivityModal() {
const setSelectedOrder = useUpdateAtom(selectedOrderAtom)
return useCallback(
(order: { orderHash: string; status: UniswapXOrderStatus }) => setSelectedOrder({ ...order, modalOpen: true }),
[setSelectedOrder]
)
}
const Wrapper = styled(AutoColumn).attrs({ gap: 'md', grow: true })`
padding: 16px;
`
const ContentContainer = styled(AutoColumn).attrs({ justify: 'center', gap: 'md' })`
padding: 28px 44px 24px 44px;
`
const StyledXButton = styled(X)`
cursor: pointer;
justify-self: flex-end;
color: ${({ theme }) => theme.textPrimary};
${OpacityHoverState};
`
const LoadingWrapper = styled.div`
width: 52px;
height: 52px;
position: relative;
margin-bottom: 8px;
`
const LoadingIndicator = styled(LoaderV3)`
width: 100%;
height: 100%;
position: absolute;
`
function Loader() {
return (
<LoadingWrapper>
<FadePresence>
<LoadingIndicator />
</FadePresence>
</LoadingWrapper>
)
}
const Success = styled(AnimatedEntranceConfirmationIcon)`
position: relative;
margin-bottom: 10px;
`
const LearnMoreLink = styled(ExternalLink)`
font-weight: 600;
`
const DescriptionText = styled(ThemedText.LabelMicro)`
text-align: center;
`
function useOrderAmounts(
orderDetails?: UniswapXOrderDetails
): Pick<InterfaceTrade, 'inputAmount' | 'outputAmount'> | undefined {
const inputCurrency = useCurrency(orderDetails?.swapInfo?.inputCurrencyId, orderDetails?.chainId)
const outputCurrency = useCurrency(orderDetails?.swapInfo?.outputCurrencyId, orderDetails?.chainId)
if (!orderDetails) return undefined
if (!inputCurrency || !outputCurrency) {
console.error(`Could not find token(s) for order ${orderDetails.orderHash}`)
return undefined
}
const { swapInfo } = orderDetails
if (swapInfo.tradeType === TradeType.EXACT_INPUT) {
return {
inputAmount: CurrencyAmount.fromRawAmount(inputCurrency, swapInfo.inputCurrencyAmountRaw),
outputAmount: CurrencyAmount.fromRawAmount(
outputCurrency,
swapInfo.settledOutputCurrencyAmountRaw ?? swapInfo.expectedOutputCurrencyAmountRaw
),
}
} else {
return {
inputAmount: CurrencyAmount.fromRawAmount(inputCurrency, swapInfo.expectedInputCurrencyAmountRaw),
outputAmount: CurrencyAmount.fromRawAmount(outputCurrency, swapInfo.outputCurrencyAmountRaw),
}
}
}
export function OrderContent({ order }: { order: SelectedOrderInfo }) {
const amounts = useOrderAmounts(order.details)
const explorerLink = order?.details?.txHash
? getExplorerLink(order.details.chainId, order.details.txHash, ExplorerDataType.TRANSACTION)
: undefined
switch (order.status) {
case UniswapXOrderStatus.OPEN: {
return (
<ContentContainer>
<Loader />
<ThemedText.SubHeaderLarge>
<Trans>Swapping</Trans>
</ThemedText.SubHeaderLarge>
<Column>
{amounts && <TradeSummary trade={amounts} />}
<ThemedText.Caption paddingTop="48px" textAlign="center">
<ExternalLink href="https://support.uniswap.org/hc/en-us/articles/17515415311501">
<Trans>Learn more about swapping with UniswapX</Trans>
</ExternalLink>
</ThemedText.Caption>
</Column>
</ContentContainer>
)
}
case UniswapXOrderStatus.FILLED:
return (
<ContentContainer>
<Success />
<ThemedText.SubHeaderLarge>
<Trans>Swapped</Trans>
</ThemedText.SubHeaderLarge>
<Column>
{amounts && <TradeSummary trade={amounts} />}
<ThemedText.Caption paddingTop="48px" textAlign="center">
{explorerLink && (
<ExternalLink href={explorerLink}>
<Trans>View on Explorer</Trans>
</ExternalLink>
)}
</ThemedText.Caption>
</Column>
</ContentContainer>
)
case UniswapXOrderStatus.CANCELLED:
return (
<ContentContainer>
<ErrorContent />
<ThemedText.SubHeaderLarge>
<Trans>Cancelled</Trans>
</ThemedText.SubHeaderLarge>
<ThemedText.LabelSmall textAlign="center">
<Trans>This order was cancelled</Trans>
</ThemedText.LabelSmall>
</ContentContainer>
)
case UniswapXOrderStatus.EXPIRED:
return (
<ContentContainer>
<ErrorContent />
<ThemedText.SubHeaderLarge>
<Trans>Swap expired</Trans>
</ThemedText.SubHeaderLarge>
<DescriptionText>
{/* TODO: Improve translation grammar by not having to break up the string */}
<Trans>Your swap expired before it could be filled. Try again or</Trans>{' '}
<LearnMoreLink href="https://support.uniswap.org/hc/en-us/articles/17515426867213">
<Trans>learn more.</Trans>
</LearnMoreLink>
</DescriptionText>
</ContentContainer>
)
case UniswapXOrderStatus.ERROR:
return (
<ContentContainer>
<ErrorContent />
<ThemedText.SubHeaderLarge>
<Trans>Error</Trans>
</ThemedText.SubHeaderLarge>
<ThemedText.LabelSmall textAlign="center">
{/* TODO: Improve translation grammar by not having to break up the string */}
<Trans>Your swap couldn&apos;t be filled at this time. Try again or </Trans>{' '}
<LearnMoreLink href="https://support.uniswap.org/hc/en-us/articles/17515489874189">
<Trans>learn more.</Trans>
</LearnMoreLink>
</ThemedText.LabelSmall>
</ContentContainer>
)
case UniswapXOrderStatus.INSUFFICIENT_FUNDS:
return (
<ContentContainer>
<ErrorContent />
<ThemedText.SubHeaderLarge>
<Trans>Insufficient funds for swap</Trans>
</ThemedText.SubHeaderLarge>
<ThemedText.LabelSmall textAlign="center">{t`You didn't have enough ${
amounts?.inputAmount.currency.symbol ?? amounts?.inputAmount.currency.name ?? t`of the input token`
} to complete this swap.`}</ThemedText.LabelSmall>
</ContentContainer>
)
}
}
/* Returns the order currently selected in the UI synced with updates from order status polling */
function useSyncedSelectedOrder(): SelectedOrderInfo | undefined {
const selectedOrder = useAtomValue(selectedOrderAtom)
const localPendingOrder = useOrder(selectedOrder?.orderHash ?? '')
return useMemo(() => {
if (!selectedOrder) return undefined
return {
...selectedOrder,
status: localPendingOrder?.status ?? selectedOrder.status,
details: localPendingOrder,
}
}, [localPendingOrder, selectedOrder])
}
export function OffchainActivityModal() {
const syncedSelectedOrder = useSyncedSelectedOrder()
const setSelectedOrder = useUpdateAtom(selectedOrderAtom)
const reset = useCallback(() => {
setSelectedOrder((order) => order && { ...order, modalOpen: false })
}, [setSelectedOrder])
return (
<Modal isOpen={!!syncedSelectedOrder?.modalOpen} onDismiss={reset}>
<Wrapper>
<StyledXButton onClick={reset} />
{syncedSelectedOrder && <OrderContent order={syncedSelectedOrder} />}
</Wrapper>
</Modal>
)
}

View File

@@ -0,0 +1,97 @@
import { TransactionStatus, useActivityQuery } from 'graphql/data/__generated__/types-and-hooks'
import { useEffect, useMemo } from 'react'
import { usePendingOrders } from 'state/signatures/hooks'
import { usePendingTransactions, useTransactionCanceller } from 'state/transactions/hooks'
import { useLocalActivities } from './parseLocal'
import { parseRemoteActivities } from './parseRemote'
import { Activity, ActivityMap } from './types'
/** Detects transactions from same account with the same nonce and different hash */
function findCancelTx(localActivity: Activity, remoteMap: ActivityMap, account: string): string | undefined {
// handles locally cached tx's that were stored before we started tracking nonces
if (!localActivity.nonce || localActivity.status !== TransactionStatus.Pending) return undefined
for (const remoteTx of Object.values(remoteMap)) {
if (!remoteTx) continue
// A pending tx is 'cancelled' when another tx with the same account & nonce but different hash makes it on chain
if (
remoteTx.nonce === localActivity.nonce &&
remoteTx.from.toLowerCase() === account.toLowerCase() &&
remoteTx.hash.toLowerCase() !== localActivity.hash.toLowerCase() &&
remoteTx.chainId === localActivity.chainId
) {
return remoteTx.hash
}
}
return undefined
}
/** Deduplicates local and remote activities */
function combineActivities(localMap: ActivityMap = {}, remoteMap: ActivityMap = {}): Array<Activity> {
const txHashes = [...new Set([...Object.keys(localMap), ...Object.keys(remoteMap)])]
return txHashes.reduce((acc: Array<Activity>, hash) => {
const localActivity = (localMap?.[hash] ?? {}) as Activity
const remoteActivity = (remoteMap?.[hash] ?? {}) as Activity
if (localActivity.cancelled) {
// Hides misleading activities caused by cross-chain nonce collisions previously being incorrectly labelled as cancelled txs in redux
if (localActivity.chainId !== remoteActivity.chainId) {
acc.push(remoteActivity)
return acc
}
// Remote data only contains data of the cancel tx, rather than the original tx, so we prefer local data here
acc.push(localActivity)
} else {
// Generally prefer remote values to local value because i.e. remote swap amounts are on-chain rather than client-estimated
acc.push({ ...localActivity, ...remoteActivity } as Activity)
}
return acc
}, [])
}
export function useAllActivities(account: string) {
const { data, loading, refetch } = useActivityQuery({
variables: { account },
errorPolicy: 'all',
fetchPolicy: 'cache-first',
})
const localMap = useLocalActivities(account)
const remoteMap = useMemo(() => parseRemoteActivities(data?.portfolios?.[0].assetActivities), [data?.portfolios])
const updateCancelledTx = useTransactionCanceller()
/* Updates locally stored pendings tx's when remote data contains a conflicting cancellation tx */
useEffect(() => {
if (!remoteMap) return
Object.values(localMap).forEach((localActivity) => {
if (!localActivity) return
const cancelHash = findCancelTx(localActivity, remoteMap, account)
if (cancelHash) updateCancelledTx(localActivity.hash, localActivity.chainId, cancelHash)
})
}, [account, localMap, remoteMap, updateCancelledTx])
const combinedActivities = useMemo(
() => (remoteMap ? combineActivities(localMap, remoteMap) : undefined),
[localMap, remoteMap]
)
return { loading, activities: combinedActivities, refetch }
}
export function useHasPendingActivity() {
const pendingTransactions = usePendingTransactions()
const pendingOrders = usePendingOrders()
const hasPendingActivity = pendingTransactions.length > 0 || pendingOrders.length > 0
const pendingActivityCount = pendingTransactions.length + pendingOrders.length
return { hasPendingActivity, pendingActivityCount }
}

View File

@@ -3,7 +3,7 @@ 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 { TransactionStatus } from 'graphql/data/__generated__/types-and-hooks'
import { PollingInterval } from 'graphql/data/util'
import { atom, useAtom } from 'jotai'
import { EmptyWalletModule } from 'nft/components/profile/view/EmptyWalletContent'
@@ -13,9 +13,8 @@ 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'
import { useAllActivities } from './hooks'
import { Activity } from './types'
interface ActivityGroup {
title: string
@@ -25,7 +24,7 @@ interface ActivityGroup {
const sortActivities = (a: Activity, b: Activity) => b.timestamp - a.timestamp
const createGroups = (activities?: Array<Activity>) => {
if (!activities || !activities.length) return []
if (!activities) return undefined
const now = Date.now()
const pending: Array<Activity> = []
@@ -82,55 +81,13 @@ const ActivityGroupWrapper = styled(Column)`
gap: 8px;
`
/* Detects transactions from same account with the same nonce and different hash */
function wasTxCancelled(localActivity: Activity, remoteMap: ActivityMap, account: string): boolean {
// handles locally cached tx's that were stored before we started tracking nonces
if (!localActivity.nonce || localActivity.status !== TransactionStatus.Pending) return false
return Object.values(remoteMap).some((remoteTx) => {
if (!remoteTx) return false
// Cancellations are only possible when both nonce and tx.from are the same
if (remoteTx.nonce === localActivity.nonce && remoteTx.receipt?.from.toLowerCase() === account.toLowerCase()) {
// If the remote tx has a different hash than the local tx, the local tx was cancelled
return remoteTx.hash.toLowerCase() !== localActivity.hash.toLowerCase()
}
return false
})
}
function combineActivities(localMap: ActivityMap = {}, remoteMap: ActivityMap = {}, account: string): Array<Activity> {
const txHashes = [...new Set([...Object.keys(localMap), ...Object.keys(remoteMap)])]
// Merges local and remote activities w/ same hash, preferring remote data
return txHashes.reduce((acc: Array<Activity>, hash) => {
const localActivity = (localMap?.[hash] ?? {}) as Activity
const remoteActivity = (remoteMap?.[hash] ?? {}) as Activity
// TODO(WEB-2064): Display cancelled status in UI rather than completely hiding cancelled TXs
if (wasTxCancelled(localActivity, remoteMap, account)) return acc
// TODO(cartcrom): determine best logic for which fields to prefer from which sources
// i.e.prefer remote exact swap output instead of local estimated output
acc.push({ ...localActivity, ...remoteActivity } as Activity)
return acc
}, [])
}
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',
})
const { activities, loading, refetch } = useAllActivities(account)
// We only refetch remote activity if the user renavigates to the activity tab by changing tabs or opening the drawer
useEffect(() => {
@@ -143,20 +100,16 @@ export function ActivityTab({ account }: { account: string }) {
}
}, [drawerOpen, lastFetched, refetch, setLastFetched])
const activityGroups = useMemo(() => {
const remoteMap = parseRemoteActivities(data?.portfolios?.[0].assetActivities)
const allActivities = combineActivities(localMap, remoteMap, account)
return createGroups(allActivities)
}, [data?.portfolios, localMap, account])
const activityGroups = useMemo(() => createGroups(activities), [activities])
if (!data && loading)
if (!activityGroups && loading) {
return (
<>
<LoadingBubble height="16px" width="80px" margin="16px 16px 8px" />
<PortfolioSkeleton shrinkRight />
</>
)
else if (activityGroups.length === 0) {
} else if (!activityGroups || activityGroups?.length === 0) {
return <EmptyWalletModule type="activity" onNavigateClick={toggleWalletDrawer} />
} else {
return (

View File

@@ -1,4 +1,4 @@
import { SupportedChainId, Token, TradeType as MockTradeType } from '@uniswap/sdk-core'
import { ChainId, 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, USDT as MockUSDT } from 'constants/tokens'
import { TransactionStatus as MockTxStatus } from 'graphql/data/__generated__/types-and-hooks'
@@ -12,7 +12,9 @@ import {
} from 'state/transactions/types'
import { renderHook } from 'test-utils/render'
import { parseLocalActivity, useLocalActivities } from './parseLocal'
import { UniswapXOrderStatus } from '../../../../lib/hooks/orders/types'
import { SignatureDetails, SignatureType } from '../../../../state/signatures/types'
import { signatureToActivity, transactionToActivity, useLocalActivities } from './parseLocal'
function mockSwapInfo(
type: MockTradeType,
@@ -30,6 +32,7 @@ function mockSwapInfo(
outputCurrencyId: outputCurrency.address,
expectedOutputCurrencyAmountRaw: outputCurrencyAmountRaw,
minimumOutputCurrencyAmountRaw: outputCurrencyAmountRaw,
isUniswapXOrder: false,
}
} else {
return {
@@ -40,13 +43,14 @@ function mockSwapInfo(
maximumInputCurrencyAmountRaw: inputCurrencyAmountRaw,
outputCurrencyId: outputCurrency.address,
outputCurrencyAmountRaw,
isUniswapXOrder: false,
}
}
}
const mockAccount1 = '0x000000000000000000000000000000000000000001'
const mockAccount2 = '0x000000000000000000000000000000000000000002'
const mockChainId = SupportedChainId.MAINNET
const mockChainId = ChainId.MAINNET
const mockSpenderAddress = PERMIT2_ADDRESS[mockChainId]
const mockCurrencyAmountRaw = '1000000000000000000'
const mockCurrencyAmountRawUSDC = '1000000'
@@ -246,27 +250,13 @@ describe('parseLocalActivity', () => {
status: 1,
},
} as TransactionDetails
const chainId = SupportedChainId.MAINNET
expect(parseLocalActivity(details, chainId, mockTokenAddressMap)).toEqual({
const chainId = ChainId.MAINNET
expect(transactionToActivity(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',
},
from: undefined,
status: 'CONFIRMED',
timestamp: NaN,
title: 'Swapped',
@@ -287,8 +277,8 @@ describe('parseLocalActivity', () => {
status: 1,
},
} as TransactionDetails
const chainId = SupportedChainId.MAINNET
expect(parseLocalActivity(details, chainId, mockTokenAddressMap)).toMatchObject({
const chainId = ChainId.MAINNET
expect(transactionToActivity(details, chainId, mockTokenAddressMap)).toMatchObject({
chainId: 1,
currencies: [MockUSDC_MAINNET, MockDAI],
descriptor: '1.00 USDC for 1.00 DAI',
@@ -311,9 +301,9 @@ describe('parseLocalActivity', () => {
status: 1,
},
} as TransactionDetails
const chainId = SupportedChainId.MAINNET
const chainId = ChainId.MAINNET
const tokens = {} as ChainTokenMap
expect(parseLocalActivity(details, chainId, tokens)).toMatchObject({
expect(transactionToActivity(details, chainId, tokens)).toMatchObject({
chainId: 1,
currencies: [undefined, undefined],
descriptor: 'Unknown for Unknown',
@@ -349,10 +339,7 @@ describe('parseLocalActivity', () => {
descriptor: `1.00 ${MockUSDC_MAINNET.symbol} for 1.00 ${MockDAI.symbol}`,
hash,
status: MockTxStatus.Confirmed,
receipt: {
id: hash,
status: MockTxStatus.Confirmed,
},
from: mockAccount2,
})
})
@@ -367,10 +354,7 @@ describe('parseLocalActivity', () => {
descriptor: `1.00 ${MockUSDC_MAINNET.symbol} for 1.00 ${MockDAI.symbol}`,
hash,
status: MockTxStatus.Confirmed,
receipt: {
id: hash,
status: MockTxStatus.Confirmed,
},
from: mockAccount2,
})
})
@@ -385,10 +369,7 @@ describe('parseLocalActivity', () => {
descriptor: MockDAI.symbol,
hash,
status: MockTxStatus.Confirmed,
receipt: {
id: hash,
status: MockTxStatus.Confirmed,
},
from: mockAccount2,
})
})
@@ -402,10 +383,6 @@ describe('parseLocalActivity', () => {
descriptor: MockUSDT.symbol,
hash,
status: MockTxStatus.Confirmed,
receipt: {
id: hash,
status: MockTxStatus.Confirmed,
},
})
})
@@ -422,10 +399,7 @@ describe('parseLocalActivity', () => {
descriptor: `1.00 ${native.symbol} for 1.00 ${native.wrapped.symbol}`,
hash,
status: MockTxStatus.Confirmed,
receipt: {
id: hash,
status: MockTxStatus.Confirmed,
},
from: mockAccount2,
})
})
@@ -442,10 +416,7 @@ describe('parseLocalActivity', () => {
descriptor: `1.00 ${native.wrapped.symbol} for 1.00 ${native.symbol}`,
hash,
status: MockTxStatus.Confirmed,
receipt: {
id: hash,
status: MockTxStatus.Confirmed,
},
from: mockAccount2,
})
})
@@ -460,10 +431,7 @@ describe('parseLocalActivity', () => {
descriptor: `1.00 ${MockUSDC_MAINNET.symbol} and 1.00 ${MockDAI.symbol}`,
hash,
status: MockTxStatus.Confirmed,
receipt: {
id: hash,
status: MockTxStatus.Confirmed,
},
from: mockAccount2,
})
})
@@ -478,10 +446,7 @@ describe('parseLocalActivity', () => {
descriptor: `1.00 ${MockUSDC_MAINNET.symbol} and 1.00 ${MockDAI.symbol}`,
hash,
status: MockTxStatus.Confirmed,
receipt: {
id: hash,
status: MockTxStatus.Confirmed,
},
from: mockAccount2,
})
})
@@ -496,10 +461,7 @@ describe('parseLocalActivity', () => {
descriptor: `1.00 ${MockUSDC_MAINNET.symbol} and 1.00 ${MockDAI.symbol}`,
hash,
status: MockTxStatus.Confirmed,
receipt: {
id: hash,
status: MockTxStatus.Confirmed,
},
from: mockAccount2,
})
})
@@ -514,10 +476,7 @@ describe('parseLocalActivity', () => {
descriptor: `1.00 ${MockUSDC_MAINNET.symbol} and 1.00 ${MockDAI.symbol}`,
hash,
status: MockTxStatus.Confirmed,
receipt: {
id: hash,
status: MockTxStatus.Confirmed,
},
from: mockAccount2,
})
})
@@ -532,10 +491,29 @@ describe('parseLocalActivity', () => {
descriptor: `${MockUSDC_MAINNET.symbol} and ${MockDAI.symbol}`,
hash,
status: MockTxStatus.Confirmed,
receipt: {
id: hash,
status: MockTxStatus.Confirmed,
},
from: mockAccount2,
})
})
it('Signature to activity - returns undefined if is on chain order', () => {
expect(
signatureToActivity(
{
type: SignatureType.SIGN_UNISWAPX_ORDER,
status: UniswapXOrderStatus.FILLED,
} as SignatureDetails,
{}
)
).toBeUndefined()
expect(
signatureToActivity(
{
type: SignatureType.SIGN_UNISWAPX_ORDER,
status: UniswapXOrderStatus.CANCELLED,
} as SignatureDetails,
{}
)
).toBeUndefined()
})
})

View File

@@ -1,12 +1,14 @@
import { BigNumber } from '@ethersproject/bignumber'
import { t } from '@lingui/macro'
import { formatCurrencyAmount } from '@uniswap/conedison/format'
import { Currency, CurrencyAmount, TradeType } from '@uniswap/sdk-core'
import { ChainId, Currency, CurrencyAmount, TradeType } from '@uniswap/sdk-core'
import { nativeOnChain } from '@uniswap/smart-order-router'
import { SupportedChainId } from 'constants/chains'
import UniswapXBolt from 'assets/svg/bolt.svg'
import { TransactionStatus } from 'graphql/data/__generated__/types-and-hooks'
import { ChainTokenMap, useAllTokensMultichain } from 'hooks/Tokens'
import { useMemo } from 'react'
import { isOnChainOrder, useAllSignatures } from 'state/signatures/hooks'
import { SignatureDetails, SignatureType } from 'state/signatures/types'
import { useMultichainTransactions } from 'state/transactions/hooks'
import {
AddLiquidityV2PoolTransactionInfo,
@@ -23,10 +25,10 @@ import {
WrapTransactionInfo,
} from 'state/transactions/types'
import { getActivityTitle } from '../constants'
import { CancelledTransactionTitleTable, getActivityTitle, OrderTextTable } from '../constants'
import { Activity, ActivityMap } from './types'
function getCurrency(currencyId: string, chainId: SupportedChainId, tokens: ChainTokenMap): Currency | undefined {
function getCurrency(currencyId: string, chainId: ChainId, tokens: ChainTokenMap): Currency | undefined {
return currencyId === 'ETH' ? nativeOnChain(chainId) : tokens[chainId]?.[currencyId]
}
@@ -46,23 +48,24 @@ function buildCurrencyDescriptor(
function parseSwap(
swap: ExactInputSwapTransactionInfo | ExactOutputSwapTransactionInfo,
chainId: SupportedChainId,
chainId: ChainId,
tokens: ChainTokenMap
): 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.inputCurrencyAmountRaw, swap.settledOutputCurrencyAmountRaw ?? swap.expectedOutputCurrencyAmountRaw]
: [swap.expectedInputCurrencyAmountRaw, swap.outputCurrencyAmountRaw]
return {
descriptor: buildCurrencyDescriptor(tokenIn, inputRaw, tokenOut, outputRaw),
currencies: [tokenIn, tokenOut],
prefixIconSrc: swap.isUniswapXOrder ? UniswapXBolt : undefined,
}
}
function parseWrap(wrap: WrapTransactionInfo, chainId: SupportedChainId, status: TransactionStatus): Partial<Activity> {
function parseWrap(wrap: WrapTransactionInfo, chainId: ChainId, status: TransactionStatus): Partial<Activity> {
const native = nativeOnChain(chainId)
const wrapped = native.wrapped
const [input, output] = wrap.unwrapped ? [wrapped, native] : [native, wrapped]
@@ -76,7 +79,7 @@ function parseWrap(wrap: WrapTransactionInfo, chainId: SupportedChainId, status:
function parseApproval(
approval: ApproveTransactionInfo,
chainId: SupportedChainId,
chainId: ChainId,
tokens: ChainTokenMap,
status: TransactionStatus
): Partial<Activity> {
@@ -97,7 +100,7 @@ type GenericLPInfo = Omit<
AddLiquidityV3PoolTransactionInfo | RemoveLiquidityV3TransactionInfo | AddLiquidityV2PoolTransactionInfo,
'type'
>
function parseLP(lp: GenericLPInfo, chainId: SupportedChainId, tokens: ChainTokenMap): Partial<Activity> {
function parseLP(lp: GenericLPInfo, chainId: ChainId, tokens: ChainTokenMap): Partial<Activity> {
const baseCurrency = getCurrency(lp.baseCurrencyId, chainId, tokens)
const quoteCurrency = getCurrency(lp.quoteCurrencyId, chainId, tokens)
const [baseRaw, quoteRaw] = [lp.expectedAmountBaseRaw, lp.expectedAmountQuoteRaw]
@@ -108,7 +111,7 @@ function parseLP(lp: GenericLPInfo, chainId: SupportedChainId, tokens: ChainToke
function parseCollectFees(
collect: CollectFeesTransactionInfo,
chainId: SupportedChainId,
chainId: ChainId,
tokens: ChainTokenMap
): Partial<Activity> {
// Adapts CollectFeesTransactionInfo to generic LP type
@@ -123,7 +126,7 @@ function parseCollectFees(
function parseMigrateCreateV3(
lp: MigrateV2LiquidityToV3TransactionInfo | CreateV3PoolTransactionInfo,
chainId: SupportedChainId,
chainId: ChainId,
tokens: ChainTokenMap
): Partial<Activity> {
const baseCurrency = getCurrency(lp.baseCurrencyId, chainId, tokens)
@@ -135,26 +138,21 @@ function parseMigrateCreateV3(
return { descriptor, currencies: [baseCurrency, quoteCurrency] }
}
export function parseLocalActivity(
export function getTransactionStatus(details: TransactionDetails): TransactionStatus {
return !details.receipt
? TransactionStatus.Pending
: details.receipt.status === 1 || details.receipt?.status === undefined
? TransactionStatus.Confirmed
: TransactionStatus.Failed
}
export function transactionToActivity(
details: TransactionDetails,
chainId: SupportedChainId,
chainId: ChainId,
tokens: ChainTokenMap
): Activity | undefined {
try {
const status = !details.receipt
? TransactionStatus.Pending
: details.receipt.status === 1 || details.receipt?.status === undefined
? TransactionStatus.Confirmed
: TransactionStatus.Failed
const receipt = details.receipt
? {
id: details.receipt.transactionHash,
...details.receipt,
...details,
status,
}
: undefined
const status = getTransactionStatus(details)
const defaultFields = {
hash: details.hash,
@@ -162,8 +160,9 @@ export function parseLocalActivity(
title: getActivityTitle(details.info.type, status),
status,
timestamp: (details.confirmedTime ?? details.addedTime) / 1000,
receipt,
from: details.from,
nonce: details.nonce,
cancelled: details.cancelled,
}
let additionalFields: Partial<Activity> = {}
@@ -186,24 +185,67 @@ export function parseLocalActivity(
additionalFields = parseMigrateCreateV3(info, chainId, tokens)
}
return { ...defaultFields, ...additionalFields }
const activity = { ...defaultFields, ...additionalFields }
if (details.cancelled) {
activity.title = CancelledTransactionTitleTable[details.info.type]
activity.status = TransactionStatus.Confirmed
}
return activity
} catch (error) {
console.debug(`Failed to parse transaction ${details.hash}`, error)
return undefined
}
}
export function signatureToActivity(signature: SignatureDetails, tokens: ChainTokenMap): Activity | undefined {
switch (signature.type) {
case SignatureType.SIGN_UNISWAPX_ORDER: {
// Only returns Activity items for orders that don't have an on-chain counterpart
if (isOnChainOrder(signature.status)) return undefined
const { title, statusMessage, status } = OrderTextTable[signature.status]
return {
hash: signature.orderHash,
chainId: signature.chainId,
title,
status,
offchainOrderStatus: signature.status,
timestamp: signature.addedTime / 1000,
from: signature.offerer,
statusMessage,
prefixIconSrc: UniswapXBolt,
...parseSwap(signature.swapInfo, signature.chainId, tokens),
}
}
default:
return undefined
}
}
export function useLocalActivities(account: string): ActivityMap {
const allTransactions = useMultichainTransactions()
const allSignatures = useAllSignatures()
const tokens = useAllTokensMultichain()
return useMemo(() => {
const activityByHash: ActivityMap = {}
const activityMap: ActivityMap = {}
for (const [transaction, chainId] of allTransactions) {
if (transaction.from !== account) continue
activityByHash[transaction.hash] = parseLocalActivity(transaction, chainId, tokens)
const activity = transactionToActivity(transaction, chainId, tokens)
if (activity) activityMap[transaction.hash] = activity
}
return activityByHash
}, [account, allTransactions, tokens])
for (const signature of Object.values(allSignatures)) {
if (signature.offerer !== account) continue
const activity = signatureToActivity(signature, tokens)
if (activity) activityMap[signature.id] = activity
}
return activityMap
}, [account, allSignatures, allTransactions, tokens])
}

View File

@@ -1,8 +1,8 @@
import { t } from '@lingui/macro'
import { formatFiatPrice, formatNumberOrString, NumberType } from '@uniswap/conedison/format'
import { SupportedChainId } from '@uniswap/sdk-core'
import { ChainId, NONFUNGIBLE_POSITION_MANAGER_ADDRESSES, UNI_ADDRESSES } from '@uniswap/sdk-core'
import UniswapXBolt from 'assets/svg/bolt.svg'
import moonpayLogoSrc from 'assets/svg/moonpay.svg'
import { NONFUNGIBLE_POSITION_MANAGER_ADDRESSES, UNI_ADDRESS } from 'constants/addresses'
import { nativeOnChain } from 'constants/tokens'
import {
ActivityType,
@@ -11,15 +11,19 @@ import {
NftApprovalPartsFragment,
NftApproveForAllPartsFragment,
NftTransferPartsFragment,
SwapOrderDetailsPartsFragment,
SwapOrderStatus,
TokenApprovalPartsFragment,
TokenAssetPartsFragment,
TokenTransferPartsFragment,
TransactionDetailsPartsFragment,
} from 'graphql/data/__generated__/types-and-hooks'
import { fromGraphQLChain } from 'graphql/data/util'
import { logSentryErrorForUnsupportedChain, supportedChainIdFromGQLChain } from 'graphql/data/util'
import ms from 'ms.macro'
import { useEffect, useState } from 'react'
import { isAddress } from 'utils'
import { MOONPAY_SENDER_ADDRESSES } from '../constants'
import { MOONPAY_SENDER_ADDRESSES, OrderStatusTable, OrderTextTable } from '../constants'
import { Activity } from './types'
type TransactionChanges = {
@@ -38,7 +42,7 @@ 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()]: {
[UNI_ADDRESSES[ChainId.MAINNET].toLowerCase()]: {
title: t`UNI Governance`,
descriptor: t`Contract Interaction`,
logos: [UNI_IMG],
@@ -75,11 +79,10 @@ 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)]
)
function callsPositionManagerContract(assetActivity: TransactionActivity) {
const supportedChain = supportedChainIdFromGQLChain(assetActivity.chain)
if (!supportedChain) return false
return isSameAddress(assetActivity.details.to, NONFUNGIBLE_POSITION_MANAGER_ADDRESSES[supportedChain])
}
// Gets counts for number of NFTs in each collection present
@@ -93,15 +96,24 @@ function getCollectionCounts(nftTransfers: NftTransferPartsFragment[]): { [key:
}, {} as { [key: string]: number | undefined })
}
function getSwapTitle(sent: TokenTransferPartsFragment, received: TokenTransferPartsFragment) {
function getSwapTitle(sent: TokenTransferPartsFragment, received: TokenTransferPartsFragment): string | undefined {
const supportedSentChain = supportedChainIdFromGQLChain(sent.asset.chain)
const supportedReceivedChain = supportedChainIdFromGQLChain(received.asset.chain)
if (!supportedSentChain || !supportedReceivedChain) {
logSentryErrorForUnsupportedChain({
extras: { sentAsset: sent.asset, receivedAsset: received.asset },
errorMessage: 'Invalid activity from unsupported chain received from GQL',
})
return undefined
}
if (
sent.tokenStandard === 'NATIVE' &&
isSameAddress(nativeOnChain(fromGraphQLChain(sent.asset.chain)).wrapped.address, received.asset.address)
isSameAddress(nativeOnChain(supportedSentChain).wrapped.address, received.asset.address)
)
return t`Wrapped`
else if (
received.tokenStandard === 'NATIVE' &&
isSameAddress(nativeOnChain(fromGraphQLChain(received.asset.chain)).wrapped.address, received.asset.address)
isSameAddress(nativeOnChain(supportedReceivedChain).wrapped.address, received.asset.address)
) {
return t`Unwrapped`
} else {
@@ -109,6 +121,20 @@ function getSwapTitle(sent: TokenTransferPartsFragment, received: TokenTransferP
}
}
function getSwapDescriptor({
tokenIn,
inputAmount,
tokenOut,
outputAmount,
}: {
tokenIn: TokenAssetPartsFragment
outputAmount: string
tokenOut: TokenAssetPartsFragment
inputAmount: string
}) {
return `${inputAmount} ${tokenIn.symbol} for ${outputAmount} ${tokenOut.symbol}`
}
/**
*
* @param transactedValue Transacted value amount from TokenTransfer API response
@@ -138,13 +164,17 @@ function parseSwap(changes: TransactionChanges) {
const outputAmount = formatNumberOrString(received.quantity, NumberType.TokenNonTx)
return {
title: getSwapTitle(sent, received),
descriptor: `${inputAmount} ${sent.asset.symbol} for ${outputAmount} ${received.asset.symbol}`,
descriptor: getSwapDescriptor({ tokenIn: sent.asset, inputAmount, tokenOut: received.asset, outputAmount }),
}
}
}
return { title: t`Unknown Swap` }
}
function parseSwapOrder(changes: TransactionChanges) {
return { ...parseSwap(changes), prefixIconSrc: UniswapXBolt }
}
function parseApprove(changes: TransactionChanges) {
if (changes.TokenApproval.length === 1) {
const title = parseInt(changes.TokenApproval[0].quantity) === 0 ? t`Revoked Approval` : t`Approved`
@@ -167,7 +197,10 @@ function parseLPTransfers(changes: TransactionChanges) {
}
}
function parseSendReceive(changes: TransactionChanges, assetActivity: AssetActivityPartsFragment) {
type TransactionActivity = AssetActivityPartsFragment & { details: TransactionDetailsPartsFragment }
type OrderActivity = AssetActivityPartsFragment & { details: SwapOrderDetailsPartsFragment }
function parseSendReceive(changes: TransactionChanges, assetActivity: TransactionActivity) {
// TODO(cartcrom): remove edge cases after backend implements
// Edge case: Receiving two token transfers in interaction w/ V3 manager === removing liquidity. These edge cases should potentially be moved to backend
if (changes.TokenTransfer.length === 2 && callsPositionManagerContract(assetActivity)) {
@@ -214,7 +247,7 @@ function parseSendReceive(changes: TransactionChanges, assetActivity: AssetActiv
return { title: t`Unknown Send` }
}
function parseMint(changes: TransactionChanges, assetActivity: AssetActivityPartsFragment) {
function parseMint(changes: TransactionChanges, assetActivity: TransactionActivity) {
const collectionMap = getCollectionCounts(changes.NftTransfer)
if (Object.keys(collectionMap).length === 1) {
const collectionName = Object.keys(collectionMap)[0]
@@ -228,13 +261,14 @@ function parseMint(changes: TransactionChanges, assetActivity: AssetActivityPart
return { title: t`Unknown Mint` }
}
function parseUnknown(_changes: TransactionChanges, assetActivity: AssetActivityPartsFragment) {
return { title: t`Contract Interaction`, ...COMMON_CONTRACTS[assetActivity.transaction.to.toLowerCase()] }
function parseUnknown(_changes: TransactionChanges, assetActivity: TransactionActivity) {
return { title: t`Contract Interaction`, ...COMMON_CONTRACTS[assetActivity.details.to.toLowerCase()] }
}
type ActivityTypeParser = (changes: TransactionChanges, assetActivity: AssetActivityPartsFragment) => Partial<Activity>
type ActivityTypeParser = (changes: TransactionChanges, assetActivity: TransactionActivity) => Partial<Activity>
const ActivityParserByType: { [key: string]: ActivityTypeParser | undefined } = {
[ActivityType.Swap]: parseSwap,
[ActivityType.SwapOrder]: parseSwapOrder,
[ActivityType.Approve]: parseApprove,
[ActivityType.Send]: parseSendReceive,
[ActivityType.Receive]: parseSendReceive,
@@ -255,8 +289,51 @@ function getLogoSrcs(changes: TransactionChanges): string[] {
return Array.from(logoSet).filter(Boolean) as string[]
}
function parseUniswapXOrder({ details, chain, timestamp }: OrderActivity): Activity | undefined {
// We currently only have a polling mechanism for locally-sent pending orders, so we hide remote pending orders since they won't update upon completion
// TODO(WEB-2487): Add polling mechanism for remote orders to allow displaying remote pending orders
if (details.orderStatus === SwapOrderStatus.Open) return undefined
const { inputToken, inputTokenQuantity, outputToken, outputTokenQuantity, orderStatus } = details
const uniswapXOrderStatus = OrderStatusTable[orderStatus]
const { status, statusMessage, title } = OrderTextTable[uniswapXOrderStatus]
const descriptor = getSwapDescriptor({
tokenIn: inputToken,
inputAmount: inputTokenQuantity,
tokenOut: outputToken,
outputAmount: outputTokenQuantity,
})
const supportedChain = supportedChainIdFromGQLChain(chain)
if (!supportedChain) {
logSentryErrorForUnsupportedChain({
extras: { details },
errorMessage: 'Invalid activity from unsupported chain received from GQL',
})
return undefined
}
return {
hash: details.hash,
chainId: supportedChain,
status,
statusMessage,
offchainOrderStatus: uniswapXOrderStatus,
timestamp,
logos: [inputToken.project?.logo?.url, outputToken.project?.logo?.url],
title,
descriptor,
from: details.offerer,
prefixIconSrc: UniswapXBolt,
}
}
function parseRemoteActivity(assetActivity: AssetActivityPartsFragment): Activity | undefined {
try {
if (assetActivity.details.__typename === 'SwapOrderDetails') {
return parseUniswapXOrder(assetActivity as OrderActivity)
}
const changes = assetActivity.assetChanges.reduce(
(acc: TransactionChanges, assetChange) => {
if (assetChange.__typename === 'NftApproval') acc.NftApproval.push(assetChange)
@@ -269,19 +346,27 @@ function parseRemoteActivity(assetActivity: AssetActivityPartsFragment): Activit
},
{ NftTransfer: [], TokenTransfer: [], TokenApproval: [], NftApproval: [], NftApproveForAll: [] }
)
const supportedChain = supportedChainIdFromGQLChain(assetActivity.chain)
if (!supportedChain) {
logSentryErrorForUnsupportedChain({
extras: { assetActivity },
errorMessage: 'Invalid activity from unsupported chain received from GQL',
})
return undefined
}
const defaultFields = {
hash: assetActivity.transaction.hash,
chainId: fromGraphQLChain(assetActivity.chain),
status: assetActivity.transaction.status,
hash: assetActivity.details.hash,
chainId: supportedChain,
status: assetActivity.details.status,
timestamp: assetActivity.timestamp,
logos: getLogoSrcs(changes),
title: assetActivity.type,
descriptor: assetActivity.transaction.to,
receipt: assetActivity.transaction,
nonce: assetActivity.transaction.nonce,
descriptor: assetActivity.details.to,
from: assetActivity.details.from,
nonce: assetActivity.details.nonce,
}
const parsedFields = ActivityParserByType[assetActivity.type]?.(changes, assetActivity)
const parsedFields = ActivityParserByType[assetActivity.type]?.(changes, assetActivity as TransactionActivity)
return { ...defaultFields, ...parsedFields }
} catch (e) {
console.error('Failed to parse activity', e, assetActivity)
@@ -289,7 +374,7 @@ function parseRemoteActivity(assetActivity: AssetActivityPartsFragment): Activit
}
}
export function parseRemoteActivities(assetActivities?: AssetActivityPartsFragment[]) {
export function parseRemoteActivities(assetActivities?: readonly AssetActivityPartsFragment[]) {
return assetActivities?.reduce((acc: { [hash: string]: Activity }, assetActivity) => {
const activity = parseRemoteActivity(assetActivity)
if (activity) acc[activity.hash] = activity
@@ -319,15 +404,19 @@ 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`)
}
const refreshTime = () =>
setTimeout(() => {
if (Math.floor(Date.now() - timestamp * 1000) / ms`61s` <= 1) {
setTimeSince(getTimeSince(timestamp))
timeout = refreshTime()
}
}, ms`1s`)
let timeout = refreshTime()
return () => {
timeout && clearTimeout(timeout)
}
refreshTime()
}, [timestamp])
return timeSince

View File

@@ -1,21 +1,24 @@
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']
import { ChainId, Currency } from '@uniswap/sdk-core'
import { TransactionStatus } from 'graphql/data/__generated__/types-and-hooks'
import { UniswapXOrderStatus } from 'lib/hooks/orders/types'
export type Activity = {
hash: string
chainId: SupportedChainId
chainId: ChainId
status: TransactionStatus
// TODO (UniswapX): decouple Activity from UniswapXOrderStatus once we can link UniswapXScan instead of needing data for modal
offchainOrderStatus?: UniswapXOrderStatus
statusMessage?: string
timestamp: number
title: string
descriptor?: string
logos?: Array<string | undefined>
currencies?: Array<Currency | undefined>
otherAccount?: string
receipt?: Omit<Receipt, 'nonce'>
from: string
nonce?: number | null
prefixIconSrc?: string
cancelled?: boolean
}
export type ActivityMap = { [hash: string]: Activity | undefined }
export type ActivityMap = { [id: string]: Activity | undefined }

View File

@@ -1,5 +1,5 @@
import { sendAnalyticsEvent, useTrace } from '@uniswap/analytics'
import { InterfaceElementName, SharedEventName } from '@uniswap/analytics-events'
import { sendAnalyticsEvent, useTrace } from 'analytics'
import { useToggleAccountDrawer } from 'components/AccountDrawer'
import Column from 'components/Column'
import Row from 'components/Row'

View File

@@ -1,6 +1,5 @@
import { Token } from '@uniswap/sdk-core'
import { ChainId, 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'
@@ -16,7 +15,7 @@ import { useInterfaceMulticallContracts } from './hooks'
export type PositionInfo = {
owner: string
chainId: SupportedChainId
chainId: ChainId
position: Position
pool: Pool
details: PositionDetails
@@ -59,7 +58,7 @@ export function useCachedPositions(account: string): UseCachedPositionsReturnTyp
return [cachedPositions[account], setPositionsAndStaleTimeout]
}
const poolAddressKey = (details: PositionDetails, chainId: SupportedChainId) =>
const poolAddressKey = (details: PositionDetails, chainId: ChainId) =>
`${chainId}-${details.token0}-${details.token1}-${details.fee}`
type PoolAddressMap = { [key: string]: string | undefined }
@@ -71,11 +70,11 @@ const poolAddressCacheAtom = atomWithStorage<PoolAddressMap>('poolCache', {})
export function usePoolAddressCache() {
const [cache, updateCache] = useAtom(poolAddressCacheAtom)
const get = useCallback(
(details: PositionDetails, chainId: SupportedChainId) => cache[poolAddressKey(details, chainId)],
(details: PositionDetails, chainId: ChainId) => cache[poolAddressKey(details, chainId)],
[cache]
)
const set = useCallback(
(details: PositionDetails, chainId: SupportedChainId, address: string) =>
(details: PositionDetails, chainId: ChainId, address: string) =>
updateCache((c) => ({ ...c, [poolAddressKey(details, chainId)]: address })),
[updateCache]
)
@@ -104,8 +103,8 @@ function useTokenCache() {
return { get, set }
}
type TokenGetterFn = (addresses: string[], chainId: SupportedChainId) => Promise<{ [key: string]: Token | undefined }>
export function useGetCachedTokens(chains: SupportedChainId[]): TokenGetterFn {
type TokenGetterFn = (addresses: string[], chainId: ChainId) => Promise<{ [key: string]: Token | undefined }>
export function useGetCachedTokens(chains: ChainId[]): TokenGetterFn {
const allTokens = useAllTokensMultichain()
const multicallContracts = useInterfaceMulticallContracts(chains)
const tokenCache = useTokenCache()

View File

@@ -1,8 +1,7 @@
import { Token } from '@uniswap/sdk-core'
import { ChainId, 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'
@@ -38,7 +37,7 @@ async function fetchChunk(multicall: UniswapInterfaceMulticall, chunk: Call[]):
}
}
function tryParseToken(address: string, chainId: SupportedChainId, data: CallResult[]) {
function tryParseToken(address: string, chainId: ChainId, data: CallResult[]) {
try {
const [nameData, symbolData, decimalsData, nameDataBytes32, symbolDataBytes32] = data
@@ -61,7 +60,7 @@ function tryParseToken(address: string, chainId: SupportedChainId, data: CallRes
}
}
function parseTokens(addresses: string[], chainId: SupportedChainId, returnData: CallResult[]) {
function parseTokens(addresses: string[], chainId: ChainId, returnData: CallResult[]) {
const tokenDataSlices = arrayToSlices(returnData, 5)
return tokenDataSlices.reduce((acc: TokenMap, slice, index) => {
@@ -90,7 +89,7 @@ const TokenPromiseCache: { [key: CurrencyKey]: Promise<Token | undefined> | unde
// Returns tokens using a single RPC call to the multicall contract
export async function getTokensAsync(
addresses: string[],
chainId: SupportedChainId,
chainId: ChainId,
multicall: UniswapInterfaceMulticall
): Promise<TokenMap> {
if (addresses.length === 0) return {}

View File

@@ -1,12 +1,17 @@
import { Token } from '@uniswap/sdk-core'
import {
ChainId,
MULTICALL_ADDRESSES,
NONFUNGIBLE_POSITION_MANAGER_ADDRESSES as V3NFT_ADDRESSES,
Token,
} from '@uniswap/sdk-core'
import { AddressMap } from '@uniswap/smart-order-router'
import MulticallJSON from '@uniswap/v3-periphery/artifacts/contracts/lens/UniswapInterfaceMulticall.sol/UniswapInterfaceMulticall.json'
import NFTPositionManagerJSON from '@uniswap/v3-periphery/artifacts/contracts/NonfungiblePositionManager.sol/NonfungiblePositionManager.json'
import { useWeb3React } from '@web3-react/core'
import { MULTICALL_ADDRESS, NONFUNGIBLE_POSITION_MANAGER_ADDRESSES as V3NFT_ADDRESSES } from 'constants/addresses'
import { isSupportedChain, SupportedChainId } from 'constants/chains'
import { isSupportedChain } from 'constants/chains'
import { RPC_PROVIDERS } from 'constants/providers'
import { BaseContract } from 'ethers/lib/ethers'
import { useBaseEnabledChains } from 'featureFlags/flags/baseEnabled'
import { ContractInput, useUniswapPricesQuery } from 'graphql/data/__generated__/types-and-hooks'
import { toContractInput } from 'graphql/data/util'
import useStablecoinPrice from 'hooks/useStablecoinPrice'
@@ -23,31 +28,39 @@ type ContractMap<T extends BaseContract> = { [key: number]: T }
function useContractMultichain<T extends BaseContract>(
addressMap: AddressMap,
ABI: any,
chainIds?: SupportedChainId[]
chainIds?: ChainId[]
): ContractMap<T> {
const { chainId: walletChainId, provider: walletProvider } = useWeb3React()
const baseEnabledChains = useBaseEnabledChains()
return useMemo(() => {
const relevantChains =
chainIds ??
Object.keys(addressMap)
.map((chainId) => parseInt(chainId))
.filter(isSupportedChain)
.filter((chainId) => isSupportedChain(chainId, baseEnabledChains))
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
const provider =
walletProvider && walletChainId === chainId
? walletProvider
: isSupportedChain(chainId)
? RPC_PROVIDERS[chainId]
: undefined
if (provider) {
acc[chainId] = getContract(addressMap[chainId] ?? '', ABI, provider) as T
}
return acc
}, {})
}, [ABI, addressMap, chainIds, walletChainId, walletProvider])
}, [ABI, addressMap, baseEnabledChains, chainIds, walletChainId, walletProvider])
}
export function useV3ManagerContracts(chainIds: SupportedChainId[]): ContractMap<NonfungiblePositionManager> {
export function useV3ManagerContracts(chainIds: ChainId[]): ContractMap<NonfungiblePositionManager> {
return useContractMultichain<NonfungiblePositionManager>(V3NFT_ADDRESSES, NFTPositionManagerJSON.abi, chainIds)
}
export function useInterfaceMulticallContracts(chainIds: SupportedChainId[]): ContractMap<UniswapInterfaceMulticall> {
return useContractMultichain<UniswapInterfaceMulticall>(MULTICALL_ADDRESS, MulticallJSON.abi, chainIds)
export function useInterfaceMulticallContracts(chainIds: ChainId[]): ContractMap<UniswapInterfaceMulticall> {
return useContractMultichain<UniswapInterfaceMulticall>(MULTICALL_ADDRESSES, MulticallJSON.abi, chainIds)
}
type PriceMap = { [key: CurrencyKey]: number | undefined }

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