Compare commits

...

131 Commits

Author SHA1 Message Date
Charles Bachmeier
6efe8f3260 feat: [ListV2] Several Small Polish Changes (#5998)
* NFT-1075 update floor and last price to use body primary

* NFT-1080 update image thumbnail size and adjusted row accordingly

* NFT-1081 force page refresh on success screen

* NFT-1089 update same price icons

* NFT-1082 show warning colors over focus

* remove unused state var

* replace margin with padding for sticky header

* NFT-1109 properly calc if listing date is over 6mo

* NFT-1076 change listing button text to light and mobile profile bar to backgroundSurface

* NFT-1079 persist row data when listing to multiple markets

* perf improvement

---------

Co-authored-by: Charles Bachmeier <charlie@genie.xyz>
2023-02-21 13:56:26 -08:00
lynn
9ac28a4571 fix: token selector scroll margin (#6005)
* fix

* update tests

* remove gql file

* fix breakng snapshot
2023-02-21 16:40:36 -05:00
eddie
bde1421ffb fix: use ipfs token list as fallback for unsupported list (#6007) 2023-02-21 12:55:40 -08:00
eddie
3dceb45d9e fix: use ipfs token list temporarily (#6006)
fix: use ipfs token list as fallback
2023-02-21 12:19:39 -08:00
Jack Short
7b589561bc fix: not using stablecoin to calc price impact pwat (#5997)
* feat: implementing graphql endpoint

* changing from hook to function call

* initial gql routing works

* feat: initial pwatRouting setup

* sending correct amount

* removing console

* it is working

* sufficient balance

* 0 if no inputCurrency

* removing value to send if erc20

* removing console

* permit2 optional flag

* removing not necessary stuff

* mobile fixes

* overlay needs to be here

* changing swap amount to pool reserves

* refactoring routing logic

* no route found button state

* better price loading for insufficient liquidity

* refactoring graphql routing code

* overflow

* initial comments

* resetting bag status on input currency change

* locking

* done

* remove helper text for eth

* fix: pay with any token routing bug

* reordering button

* zindex

* price updated

* keeping debounce

* feat: adding amplitude events for pwat

* bumping analytics version

* types and hooks

* moving erc20 flag to useSendTransaction

* why did i put it in a hook

* no return

* fix: not using usdc value to calculate price impact

* refactor + loading state
2023-02-21 12:55:35 -05:00
eddie
c9d3dc36b8 feat: introduce statsig and expose dummy feature gate (#5974)
* feat: introduce statsig and expose dummy feature gate

* feat: use the flag for different copy

* feat: use amplitude user ID

* fix: lock file

* feat: upgrade analytics package

* fix: update snapshots

* fix: unit test snapshot

* fix: fix tests and value check

* feat: switch user ID to device ID

* fix: move statsig wrapper

* fix: move statsig wrapper

* fix: e2e tests

* fix: e2e tests

* fix: env var

* fix: env var

* fix: try testEnv

* fix: undo last commit

* fix: e2e tests

* fix: typo

* fix: dont wait for init

* wip

* fix: undo wip change
2023-02-17 17:42:57 -08:00
eddie
ef4d8fc269 fix: change e2e test language (#5999) 2023-02-17 17:29:59 -08:00
Jack Short
ff9cc5cb69 feat: pwat amp events (#5991)
* feat: implementing graphql endpoint

* changing from hook to function call

* initial gql routing works

* feat: initial pwatRouting setup

* sending correct amount

* removing console

* it is working

* sufficient balance

* 0 if no inputCurrency

* removing value to send if erc20

* removing console

* permit2 optional flag

* removing not necessary stuff

* mobile fixes

* overlay needs to be here

* changing swap amount to pool reserves

* refactoring routing logic

* no route found button state

* better price loading for insufficient liquidity

* refactoring graphql routing code

* overflow

* initial comments

* resetting bag status on input currency change

* locking

* done

* remove helper text for eth

* fix: pay with any token routing bug

* reordering button

* zindex

* price updated

* keeping debounce

* feat: adding amplitude events for pwat

* bumping analytics version

* types and hooks

* moving erc20 flag to useSendTransaction

* why did i put it in a hook

* no return
2023-02-17 16:26:57 -05:00
lynn
719fd524ed fix: update user rejected txn errors to stay up to date with widget (#5996)
init
2023-02-17 16:03:26 -05:00
github-actions[bot]
f15dd1e61e chore(i18n): new Crowdin translations (#5855)
chore(i18n): synchronize translations from crowdin [skip ci]

Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2023-02-17 11:12:47 -05:00
Jack Short
8c372c6142 fix: fixing pwat routing edge case (#5987)
* feat: implementing graphql endpoint

* changing from hook to function call

* initial gql routing works

* feat: initial pwatRouting setup

* sending correct amount

* removing console

* it is working

* sufficient balance

* 0 if no inputCurrency

* removing value to send if erc20

* removing console

* permit2 optional flag

* removing not necessary stuff

* mobile fixes

* overlay needs to be here

* changing swap amount to pool reserves

* refactoring routing logic

* no route found button state

* better price loading for insufficient liquidity

* refactoring graphql routing code

* overflow

* initial comments

* resetting bag status on input currency change

* locking

* done

* remove helper text for eth

* fix: pay with any token routing bug

* reordering button

* zindex

* price updated

* keeping debounce
2023-02-16 18:46:42 -05:00
Charles Bachmeier
8c0998bd59 feat: [ListV2] Updated page responsiveness and breakpoints (#5972)
* updated responsiveness for grid

* add sell nfts back header

* input responsiveness

* lint

* remove shrink property

---------

Co-authored-by: Charles Bachmeier <charlie@genie.xyz>
2023-02-16 14:23:26 -08:00
Charles Bachmeier
ad3a4ea808 feat: [ListV2] Proceeds bar polish (#5966)
* update nft info row height

* update proceeds styling

* update text

* move issues to hook

* update br padding

* mobile bar padding

* fix bug where no expiration wouldn't create issue

* build issue

* update bar padding

* reorganize statement

---------

Co-authored-by: Charles Bachmeier <charlie@genie.xyz>
2023-02-16 14:22:54 -08:00
Charles Bachmeier
d30c5173f5 feat: [ListV2] Dropdown and Modal Polish (#5990)
* update market dropdown border and shadow

* duration modal polish

* update twitter button text color

* reduce height of durection switcher

* NFT-1074 update approval modal text styling

---------

Co-authored-by: Charles Bachmeier <charlie@genie.xyz>
2023-02-16 14:02:33 -08:00
Jordan Frankfurt
689100afa2 feat: support moonpay iframe theme (#5986)
* feat: support moonpay iframe theme

* move theme param to body
2023-02-16 14:52:48 -06:00
Charles Bachmeier
d546ffec1c fix: Market icons displaying over dropdown (#5989)
* remove correct market

* fix market icons displaying overtooltip

---------

Co-authored-by: Charles Bachmeier <charlie@genie.xyz>
2023-02-16 12:11:51 -08:00
Jordan Frankfurt
4a72d8835c chore: remove FoR feature flag (#5985)
remove feature flag
2023-02-16 13:02:02 -06:00
eddie
19c6023601 fix: remove gap on tdp bottom bar (#5973)
* fix: remove gap on tdp bottom bar

* fix: adjust bottom and border
2023-02-16 08:43:19 -08:00
Jordan Frankfurt
a4a954c8af fix: update subgraph query to actually fetch all tick data (#5982) 2023-02-15 20:50:36 -06:00
Charles Bachmeier
82dcdcec55 feat: [ListV2] Updated fee tooltip style (#5976)
* updated tooltip style

* never forget your colon

---------

Co-authored-by: Charles Bachmeier <charlie@genie.xyz>
2023-02-15 14:35:07 -08:00
Jack Short
27e20d7230 feat: pay with any token routing (#5959)
* feat: implementing graphql endpoint

* changing from hook to function call

* initial gql routing works

* feat: initial pwatRouting setup

* sending correct amount

* removing console

* it is working

* sufficient balance

* 0 if no inputCurrency

* removing value to send if erc20

* removing console

* permit2 optional flag

* removing not necessary stuff

* mobile fixes

* overlay needs to be here

* changing swap amount to pool reserves

* refactoring routing logic

* no route found button state

* better price loading for insufficient liquidity

* refactoring graphql routing code

* overflow

* initial comments

* resetting bag status on input currency change

* locking

* done

* remove helper text for eth
2023-02-15 16:59:17 -05:00
eddie
95eafbab7d fix: wait for page to load in e2e tests (#5983)
* fix: make FoR announcement click optional in e2e tests

* fix: optional chaining

* fix: build error

* fix: wait for page to load
2023-02-15 13:55:35 -08:00
Jordan Frankfurt
639fe2f73b fix: add default case to switch (#5984) 2023-02-15 13:55:42 -06:00
Vignesh Mohankumar
72cd27f045 fix: allow signing with smart contract wallets (#5970)
* chore: upgrade universal-router-sdk

* dedupe

* update widgets

* empty
2023-02-15 11:30:09 -05:00
eddie
4e64c0e88f fix: round corners of CurrencySearch (#5969) 2023-02-14 15:17:54 -08:00
Charles Bachmeier
fda28d9be3 fix: Mixed success modal wrapping (#5967)
fix mixed success wrapping

Co-authored-by: Charles Bachmeier <charlie@genie.xyz>
2023-02-14 14:19:45 -08:00
cartcrom
bc92af6c15 fix: use latest version of tokenlists over cache (#5975)
fix: use constants in fetch interval callback to prevent updates
2023-02-14 15:48:53 -05:00
lynn
9a257e0ca8 fix: correct copy on TVL (#5968)
fix copy
2023-02-13 19:28:00 -05:00
cartcrom
82646b77dd feat: migrate search tokens to gql (#5802)
* init

* feat: search tokens hook

* feat: search ordering

* feat: separated FungibleToken parsing into sep function

* refactor: memoized search token sorting

* fix: cache waterfall issue

* fix: removed no longer relevant test

* feat: trending tokens from gql (#5805)

* feat: trending tokens from gql

* fix: reverted out-of-scope change

* refactor: remove trendingTokensFetcher

* fix: linted

* fix: removed fetch policy overrides

* fix: loading state cache

* fix: unwrap native trending tokens

* feat: refetch recently searched (#5894)

* feat: trending tokens from gql

* fix: reverted out-of-scope change

* refactor: remove trendingTokensFetcher

* feat: recently searched tokens query

* fix: linted

* feat: combined query function

* feat: recently searched hooks

* feat: combined query

* fix: removed fetch policy overrides

* fix: loading state cache

* fix: empty history loading state

* fix: revert change

* fix: revert unintended nft query change

* refactor: state functions

* fix: removed unnused query var

* fix: unwrap native trending tokens

* feat: remove fungible token type (#5896)

* feat: trending tokens from gql

* fix: reverted out-of-scope change

* refactor: remove trendingTokensFetcher

* feat: recently searched tokens query

* fix: linted

* feat: combined query function

* feat: recently searched hooks

* feat: combined query

* fix: removed fetch policy overrides

* fix: loading state cache

* fix: empty history loading state

* fix: revert change

* fix: revert unintended nft query change

* refactor: state functions

* fix: removed unnused query var

* refactor: remove FungibleToken type

* refactor: use TokenStandard.Native instead of string

* refactor: improve boolean logic readability

* refactor: removed duplicate code

* fix: unwrap native trending tokens

* fix: type error

* fix: duplicate entry bug

* refactor: use undefined instead of null/string cast

* fix: update apollo types

* fix: polygon edge case & polish
2023-02-13 18:45:13 -05:00
Jordan Frankfurt
1992c5de06 fix: don't render LP position when token name or symbol includes a url (#5961) 2023-02-13 13:45:08 -06:00
Charles Bachmeier
0208ccd7d2 feat: [ListV2] Changes to Multiple Market handling and row hovering (#5953)
* add overlay to row hover

* add remove icon on hover

* working expand and collapse icons

* fixed margin

* don't show collpase on nonhovered rows

* display mutliple markets

* remove market

* 0 margin divider

* hide on mobile

* better mobile check

* margin adjustment and hover border radius

* address comments

---------

Co-authored-by: Charles Bachmeier <charlie@genie.xyz>
2023-02-09 11:58:59 -08:00
Jordan Frankfurt
12df4b3981 fix: update some urls to match current page structure (#5957) 2023-02-09 13:35:12 -06:00
Jack Short
3eaeb65b07 feat: implementing permit2 with pay with any token (#5926)
* feat: implementing permit2 with pay with any token

* permit2 hook

* usePayWithAnyTokenHook

* removing ternary operators

* weird export type bug

* resolving merge

* fixing nft test

* styles

* refactoring styles

* reformatting

* price impact warnings

* forgot trans tag

* responding to comments

* fixes

* disabling pay with any token when on the wrong chain

* missing enabled flag

* vertically centering button
2023-02-09 13:41:30 -05:00
Jordan Frankfurt
6df2f3677e fix: use general sort rank instead of only volume rank for row numbers (#5955) 2023-02-09 12:37:13 -06:00
lynn
80edf5a0d6 fix: follow custom design for token selector scrollbar (#5952)
* fix

* remove unused styled component

* update snapshot

* disable scroll on larger currency search modal

* update snapshot
2023-02-09 13:17:58 -05:00
eddie
96f6929127 fix: showCommonBases for widget token selector (#5951) 2023-02-08 16:50:21 -08:00
eddie
4ec95d0927 fix: URL params for widget (#5943)
* fix: URL params for widget

* fix: remove output token from tokenDetails callsite

* fix: combine props, rename initial state values

* fix: better prop types

* fix: rename prop type
2023-02-08 16:50:11 -08:00
yyip-dev
fba6cc9e02 fix: test pr for github<>jira integration (#5950)
* test pr for github<>jira integration

* remove diff
2023-02-08 18:13:04 -05:00
lynn
bc2f68565b fix: Web 1992 token details update blocked token detail pages (#5948)
* init

* Update src/components/Tokens/TokenDetails/index.tsx

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

* oops last commit leads to error :/

---------

Co-authored-by: eddie <66155195+just-toby@users.noreply.github.com>
2023-02-08 18:03:36 -05:00
Zach Pomerantz
f232643d8e feat: upgrade conedison to improve signing (#5947)
* feat: upgrade conedison to improve signing

* fix: deduplicate conedison
2023-02-08 13:32:19 -08:00
lynn
527270e33f fix: handle wallets that don't send default 4001 error code for user rejected txns (#5941)
* init

* add more to comment

* zzmp comments

* regex

* oops

* fix
2023-02-08 13:34:21 -05:00
cartcrom
18cd5ec9d9 fix: default to null when query address is undefined (#5937) 2023-02-08 00:17:39 -05:00
Jordan Frankfurt
5ddb565805 feat: add feedback link to ... menu (#5933)
add feedback link to ... menu
2023-02-07 18:21:00 -06:00
Jack Short
f0b4b92b88 chore: setting up feature flags and boilerplate gql for routing gql endpoint (#5935)
* feat: routing gql endpoint

* adding feature flags

* adding to modal

* adding mocked provider to test

* adding apollo client to mocked providers

* comment
2023-02-07 18:33:41 -05:00
Charles Bachmeier
4d82f9fb3a fix: Handle Different Listing Failure Responses (#5940)
fix: handle different failure responses

Co-authored-by: Charles Bachmeier <charlie@genie.xyz>
2023-02-07 10:26:36 -08:00
Charles Bachmeier
654b26dc54 feat: [ListV2] Enable ListV2 feature flag (#5939)
enable v2 feature flag

Co-authored-by: Charles Bachmeier <charlie@genie.xyz>
2023-02-07 09:05:43 -08:00
Charles Bachmeier
c0753ae52f fix: [ListV2] Better Collection and Listing Retry and Removal Logic (#5938)
* keep approved collection status when removing

* better map name

* improve callback logic

* improve callback logic for collection approval

* reduce zustand calls

* additional zustand reductions

* add back correct check

---------

Co-authored-by: Charles Bachmeier <charlie@genie.xyz>
2023-02-07 08:29:58 -08:00
Charles Bachmeier
16bb9470ae feat: [ListV2] Changes based off Bug Bash (#5936)
* duration wrap

* better styling a different breakpoints

* throw error on listing same price

* don't show spinner on v2

* remove unused check

---------

Co-authored-by: Charles Bachmeier <charlie@genie.xyz>
2023-02-06 19:04:56 -08:00
Jordan Frankfurt
6dcfca24cb fix: remove roll list (#5934) 2023-02-06 20:41:01 -06:00
Charles Bachmeier
9cac9f8299 feat: [ListV2] error and warning states (#5921)
* update error message for price inputs

* add grid and button warning states

* add list below floor warning modal

* only warn for prices 20% below floor

* highlight unentered price in red on button press

* missing dependency

* updated modal name and mobile height

* add new file

* fix column presence

* rookie mistake

* bulk zustand imports

* below floor threshold

* move issue check higher

* cleanup mouseEvent

* rename color var

---------

Co-authored-by: Charles Bachmeier <charlie@genie.xyz>
2023-02-06 13:00:29 -08:00
Charles Bachmeier
5def0dd166 feat: [ListV2] Add retry and remove logic to listings and collection approvals (#5923)
* add remove/retry buttons

* add retry logic functionality

* add scroll to active row

* properly update rejected status

* replace loadingicon with loader

---------

Co-authored-by: Charles Bachmeier <charlie@genie.xyz>
2023-02-06 12:55:05 -08:00
Charles Bachmeier
7229637c4c feat: [ListV2] Update price and duration dropdowns (#5925)
* add custom option and style market dropdown

* working dropdown for price

* hide dropdown on mobile

* update duration dropdown

* themed opacity hover

---------

Co-authored-by: Charles Bachmeier <charlie@genie.xyz>
2023-02-06 12:54:56 -08:00
Charles Bachmeier
f26b09537d feat: [ListV2] Success modal + Twitter Share (#5924)
* added success screen with images

* add listing proceeds

* working return

* working single tweet

* working tweet multiple

* update how we parse twitterName

* add scrollbar styles

* usestablecoinvalue

* math.min

* add collection name backup to tweet

---------

Co-authored-by: Charles Bachmeier <charlie@genie.xyz>
2023-02-06 12:45:13 -08:00
Charles Bachmeier
8f922b665a feat: [ListV2] Add extra bottom padding to collection assets (#5930)
add extra bottom padding to collection assets

Co-authored-by: Charles Bachmeier <charlie@genie.xyz>
2023-02-06 12:43:59 -08:00
Zach Pomerantz
134b1d708f revert: "fix: surface connection error" (#5932)
Revert "fix: surface connection error (#5931)"

This reverts commit e9bddcb670.
2023-02-06 12:20:11 -08:00
Zach Pomerantz
e9bddcb670 fix: surface connection error (#5931)
* fix: surface connection error

* fix: to spec

* fix: sentence case
2023-02-06 15:01:12 -05:00
Callil Capuozzo
19e45fd119 fix: style collect and create a proposal buttons (#5927)
* Add responsive button styles

* Clean up responsive

* Use SmallButtonPrimary

* Allow button to accept padding props

* Fix vote padding and update link

* Hide position NFT in mobile
2023-02-06 12:49:58 -05:00
Noah Zinsmeister
ae4135fa49 fix: block time is 12 seconds now (#5929)
* block time is 12 seconds now

* fix lint error

* Update governance.ts
2023-02-06 11:18:30 -05:00
Jordan Frankfurt
89e438bcc5 chore: update generated types (#5922) 2023-02-03 15:15:03 -06:00
cartcrom
92af2167ee refactor: L2 icons in component (#5901)
* feat: feature complete

* fix: hide l2 icons on most currencylogos

* linted

* refactor: remove unecessary logo container skeleton

* fix: removed todo comment and linted
2023-02-03 14:15:08 -05:00
eddie
db6084d717 feat: widget on tx success callback (#5916)
feat: log SWAP_TRANSACTION_COMPLETED for widget transactions
2023-02-03 10:23:41 -08:00
Jack Short
927d35d59e fix: useStablecoinPrice switching between WETH and ETH (#5904) 2023-02-03 11:05:47 -05:00
eddie
b4e981b2fd fix: landing page styling of widget (#5917) 2023-02-02 18:08:48 -08:00
Zach Pomerantz
967a698178 fix: upgrade @web3-react connectors (#5920) 2023-02-02 16:24:31 -08:00
Zach Pomerantz
7818426b53 fix: use conedison signTypedData (#5875)
* fix: use conedison signTypedData

* chore: rm old signTypedData

* fix: deduplicate
2023-02-02 14:02:45 -08:00
Jordan Frankfurt
93e0054f10 feat: add jest coverage to github action (#5850)
* add jest coverage to github action

* working on action config

* fix yaml syntax
2023-02-02 14:06:15 -06:00
eddie
661d2b6a33 feat: add zIndex to widget theme (#5915)
* feat: create feature flag for swap widget

* feat: add new flag to modal

* fix: missing defaultField usage

* feat: add zIndex to widget theme
2023-02-02 11:28:15 -08:00
eddie
c560b94366 feat: create feature flag for swap widget (#5909)
* feat: create feature flag for swap widget

* feat: add new flag to modal

* fix: missing defaultField usage
2023-02-02 11:26:24 -08:00
Jack Short
93a4f00287 fix: adding usd_value back to trace event (#5912) 2023-02-02 12:05:29 -07:00
Charles Bachmeier
48833f27e3 feat: [ListV2] Connect modal styling to functionality (#5905)
* dynamic styles based on listing status

* connect collection approval to v2 modal

* import cleanup

* add comments

* connect sign listing logic

* correct pending styles

* correct pending status for collection approval

* correct pending status for os listings

* use check from react-feather

---------

Co-authored-by: Charles Bachmeier <charlie@genie.xyz>
2023-02-01 13:41:50 -08:00
Jack Short
35a03e2681 fix: using the swap token selector for pay with any token (#5902)
* feat: adds input token quote for nfts

* remove eslint

* correct usdc pricing

* fix: uses swap token selector for pay with any token selector

* check when balances are loaded

* removing token selector

* only showing active tokens in pay with any token
2023-02-01 15:59:06 -05:00
Charles Bachmeier
ac0badfb1d feat: [ListV2] Add mobile styles (#5906)
* correct mobile modal width

* better mobile styling for listing page

* add height to modal
2023-02-01 12:54:55 -08:00
eddie
149b18f02e fix: about footer link weights (#5898) 2023-02-01 09:13:15 -08:00
Jack Short
52a43f3db0 chore: removeing fetch usd price for bag footer (#5903)
* chore: removing fetchUsdprice

* removing console
2023-02-01 12:00:39 -05:00
Jack Short
0a2a46d506 feat: quote token price for nfts (#5897)
* feat: adds input token quote for nfts

* remove eslint

* correct usdc pricing
2023-01-31 15:45:02 -05:00
Charles Bachmeier
a7c1bd4391 feat: [ListV2] Setup styles for Listing Modal (#5870)
* init modal title

* active and closed section headers

* added left border to section

* add tooltip

* complete collection row

* enforce max height

* add sign listings section

* fix top margin for section

* file re-org

* make modal section re-useable and move to its own file

* format

* define section props

* use accentAction for icons

* improved index

---------

Co-authored-by: Charles Bachmeier <charlie@genie.xyz>
2023-01-30 12:22:48 -08:00
cartcrom
13221e6935 feat: caching and polling on apollo token queries (#5874)
* fix: caching on apollo token queries

* refactor: rename state variable

* added documentation for state variable purpose

* added documentation for nullish operator usage
2023-01-30 14:53:53 -05:00
lynn
26fc3caa55 fix: Revert "fix: remove gwei indicator", restore block number (#5891)
* Revert "fix: remove gwei indicator (#5873)"

This reverts commit 409ba72f9f.

* add back block number
2023-01-30 14:30:01 -05:00
Charles Bachmeier
6072bb1be0 fix: Broken NFT Details test (#5893)
fix broken NFT test

Co-authored-by: Charles Bachmeier <charlie@genie.xyz>
2023-01-30 10:09:45 -08:00
eddie
302af21a22 fix: widget theme integration (#5880)
* fix: add missing colors to widget theme integration

* feat: upgrade widget version

* fix: conedison in jest tests
2023-01-25 17:12:23 -05:00
Rachel-Eichenberger
b61a2d4111 fix: Update footer Github and Discord links (#5841)
Update footer github and discord links

Discord and github links were reversed
2023-01-25 16:15:23 -05:00
Zach Pomerantz
9be26788a2 test: skip flaky universal search test (#5881)
test: skip flaky test
2023-01-24 12:57:08 -05:00
Zach Pomerantz
ed393de481 fix: use wallet modal for widget-prompted connection (#5879)
* fix: use wallet modal for widget-prompted connection

* fix: prevent widget modal

* fix: invoke modal toggle
2023-01-24 12:36:50 -05:00
Zach Pomerantz
cf5c393d97 fix: sanitize legacy signature format (#5878)
Upgrades @uniswap/universal-router-sdk to fix a bug where legacy wallets submitted unhandled legacy signature formats, with suffixes that failed to execute when calling estimateGas.

See ticket CX-140
2023-01-23 09:37:06 -05:00
lynn
68d81a0040 fix: txn header styling for long ens names (#5872)
done fix
2023-01-22 22:00:29 -05:00
Mike Grabowski
53caa51ac3 fix: disconnection error while switching networks with MetaMask (#5871)
* fix: disconnection error while switching networks with MetaMask

* nit: oops

* fix: tests
2023-01-21 02:20:43 +04:00
lynn
409ba72f9f fix: remove gwei indicator (#5873)
* remove polling

* remove dead code

* lint issues
2023-01-20 15:58:30 -05:00
Mike Grabowski
9d9b3dca78 feat: detect Brave Wallet (#5836)
* feat: support brave better

* fix: apply review comments

* address revieW

* chore: fix lint

* chore: add comment
2023-01-21 00:42:43 +04:00
Jack Short
a11c7e9573 feat: select input token for pay with any token (#5865)
* feat: select input token for pay with any token

* creating zustand state

* updating symbol

* cursor pointer
2023-01-20 13:02:43 -05:00
Charles Bachmeier
31bbcae1ed fix: slideout menu title overlap on firefox (#5869)
fix wallet overlap on firefox

Co-authored-by: Charles Bachmeier <charlie@genie.xyz>
2023-01-20 12:03:42 -05:00
Zach Pomerantz
a1f6c7270e fix: amend gas estimate error message (#5868)
* fix: amend gas estimate error message

* fix: language

* Update src/utils/swapErrorToUserReadableMessage.tsx

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

Co-authored-by: Tina <59578595+tinaszheng@users.noreply.github.com>
2023-01-19 17:48:54 -08:00
Zach Pomerantz
8471d9b46f test: skip token explore filter tests (#5867) 2023-01-19 16:16:00 -08:00
lynn
5fc4d98faa chore: universal search e2e test (#5839)
* init

* change comments

* init

* done

* charlie comments

* fix lint

* fix explore filter test
2023-01-19 18:12:40 -05:00
Zach Pomerantz
8d9ddf36a2 fix: display specific gas estimation error (#5863) 2023-01-19 14:11:01 -08:00
Zach Pomerantz
6cfd5fa475 fix: allow signing from safepal (#5862) 2023-01-19 14:10:03 -08:00
Zach Pomerantz
f2c5a7c09c fix: track universal router swaps and err on wallet-edited calldata (#5864)
fix: track universal router swaps
2023-01-19 13:04:36 -08:00
lynn
fb52770953 chore: token explore filter e2e test (#5835)
* init

* fix

* init

* remove old token test

* fixes

* remove extraneous tokens test

* init

* tests working

* remove unnecessary data-cy
2023-01-19 14:29:05 -05:00
Zach Pomerantz
94aa8ae2c9 fix: avoid error swap state while loading (#5860)
fix: omit price impact warning while loading
2023-01-19 10:41:58 -08:00
Zach Pomerantz
6cb0824a0b fix: rm "approval failed" UI (#5861) 2023-01-19 10:32:04 -08:00
Zach Pomerantz
777887b25d fix: rm faulty revert flow (#5859) 2023-01-19 08:57:14 -08:00
Zach Pomerantz
d15d5d85f5 feat: enable permit2 (#5858) 2023-01-19 10:11:18 -05:00
Zach Pomerantz
43218d5655 feat: retry gas estimate and log to console (#5856) 2023-01-18 20:53:20 -08:00
Zach Pomerantz
a534ba41ed revert: "feat: enable permit2" (#5854)
Revert "feat: enable permit2 (#5833)"

This reverts commit a9ab5717de.
2023-01-18 16:27:09 -08:00
Zach Pomerantz
4715115743 fix: compute price impact off of debounced trade (#5852)
* fix: compute price impact off of debounced trade

* chore: rm console log
2023-01-18 16:23:19 -08:00
Zach Pomerantz
3389d01213 fix: fetch allowances without debouncing (#5853) 2023-01-18 16:04:28 -08:00
Zach Pomerantz
d9a0aa3ff0 fix: use non-warning colors for allowance button (#5851) 2023-01-18 16:03:29 -08:00
github-actions[bot]
e9e5d2e43e chore(i18n): new Crowdin translations (#5627)
chore(i18n): synchronize translations from crowdin [skip ci]

Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2023-01-18 18:57:43 -05:00
Jack Short
d58dc14bd5 feat: initial token selector for pay with any token (#5848)
* feat: initial token selector for pay with any token

* addressing some comments

* addressing comments

* linting
2023-01-18 18:52:17 -05:00
Charles Bachmeier
909e18cb23 chore: convert top level profile page to styled components (#5849)
* move const to shared file

* convert top level page wrappers to styled components

* style empty content

* simplify and wrap empty content and remove unused ve styles

* better margins

* delete unused common style

* isListingNfts bool

* remove comment

* cleanup bools

* import bag_width
2023-01-18 15:22:07 -08:00
Zach Pomerantz
a9ab5717de feat: enable permit2 (#5833) 2023-01-18 12:17:30 -08:00
Zach Pomerantz
94544de74b fix: fallback to eth_sign for permit approval (#5847)
* fix: fallback to eth_sign for permit approval

* chore: clean up comments/logs

* refactor: mirror ethers impl

* chore: add comment re: impl
2023-01-18 12:14:55 -08:00
lynn
96f24d5a9b fix: txn and language dropdown styling fix (#5845)
fix
2023-01-18 14:10:04 -05:00
Charles Bachmeier
8e59a352c0 feat: [NFTListV2] convert old bag to modal (#5838)
* hide bag on list page

* use feature flag

* maxWidth

* convert existing bag to modal'

* add new file

* add overlay

* only show old padding with flag off

* set margin const

Co-authored-by: Charles Bachmeier <charlie@genie.xyz>
2023-01-18 10:58:25 -08:00
lynn
3b765b4f05 chore: token details test (#5830)
* init

* fix

* init

* remove old token test

* fixes

* remove extraneous tokens test

* respond to mike

* oops
2023-01-18 13:36:30 -05:00
cartcrom
9f4a1f48a5 feat: fixed modal positioning on mobile & updated theming (#5790)
* feat: fixed modal positioning on mobile & updated theming

* fix: undid spacing change
2023-01-18 13:27:21 -05:00
Zach Pomerantz
de9533399a build: re-enable releases for Monday (#5844)
Revert "build: skip Monday releases (#5834)"

This reverts commit 6f147c1ff3.
2023-01-18 10:14:35 -08:00
Jordan Frankfurt
a02afd50b5 fix: add react state hookup for fiat announcement dismissal (#5840) 2023-01-18 11:35:33 -06:00
lynn
1f7ba5ae9f chore: add token explore integration test (#5820)
* init

* fix

* fixes

* remove extraneous tokens test
2023-01-17 21:40:08 -05:00
Mike Grabowski
3686803c17 refactor: improve metamask error modal (#5818)
* refactor: improve metamask error modal

* chore: rename and use translations

* chore: fix lint

* chore: Fix test
2023-01-16 20:34:42 +04:00
Zach Pomerantz
6f147c1ff3 build: skip Monday releases (#5834) 2023-01-13 14:41:32 -08:00
Zach Pomerantz
049a09a346 build: upgrade @uniswap/widgets@2.25.1 (#5831)
* build: upgrade @uniswap/widgets@2.25.0

* build: upgrade @uniswap/widgets@2.25.1
2023-01-13 14:18:34 -08:00
Zach Pomerantz
4b9a885a34 test: enable skipped fetchTokenList test (#5822) 2023-01-13 13:53:40 -08:00
Jordan Frankfurt
e3918d039f fix: spacing and other small cosmetic errors in for account dropdown (#5829)
* consolidate padding in connection menu

* reduce button vertical spacing

* reduce button vertical spacing

* add 'ETH Balance' header

* reduce cc icon size to 20px
2023-01-13 13:42:28 -06:00
Zach Pomerantz
9719af66e5 fix: update permit2 to match widget implementation again (#5826)
* fix: update permit2 to match widget implementation (#5821)

* refactor: usePermit2->usePermit2Allowance

* fix: update permit2 logic to match widgets

* fix: lint issues

* fix: memoize the interval callback
2023-01-13 11:28:54 -08:00
Jack Short
14b02eda0f feat: initial pay with any token setup (#5823)
* feat: initial pay with any token ui setup

* reordering

* refactoring

* refactoring
2023-01-13 14:09:27 -05:00
Zach Pomerantz
60bc2a1660 revert: "fix: update permit2 to match widget implementation (#5821)" (#5825)
Revert "fix: update permit2 to match widget implementation (#5821)"

This reverts commit ef3407f299.
2023-01-12 16:40:00 -08:00
Zach Pomerantz
ef3407f299 fix: update permit2 to match widget implementation (#5821)
* refactor: usePermit2->usePermit2Allowance

* fix: update permit2 logic to match widgets

* fix: lint issues
2023-01-12 15:16:07 -08:00
Jordan Frankfurt
f312a148d0 fix: persist volume ranking in token table rows instead of using index (#5819)
* fix: persist volume ranking in token table rows instead of using index

* pr feedback
2023-01-12 14:54:40 -06:00
Charles Bachmeier
cf5bb5740d feat: [List V2] Initial Floating Bar (#5817)
* initial floating bar and VE removal

* dynamic price info

* align bottom bar to bottom

* fixed to bottom

* darkmode gradient

* dynamic overlay

* style tweaks

* toggle bag if flag enabled

* handle really small numbers

* address comments

* add border

Co-authored-by: Charles Bachmeier <charlie@genie.xyz>
2023-01-12 12:11:59 -08:00
Jack Short
5f280ffd0e fix: nft add to bag test (#5824)
fix: nft add to bag teest
2023-01-12 14:33:04 -05:00
Jack Short
97075acb91 feat: pay with any token feature flag (#5814) 2023-01-12 12:16:11 -05:00
219 changed files with 26625 additions and 19136 deletions

1
.env
View File

@@ -10,3 +10,4 @@ REACT_APP_INFURA_KEY="4bf032f2d38a4ed6bb975b80d6340847"
REACT_APP_MOONPAY_API="https://api.moonpay.com"
REACT_APP_MOONPAY_LINK="https://us-central1-uniswap-mobile.cloudfunctions.net/signMoonpayLinkStaging?platform=web"
REACT_APP_MOONPAY_PUBLISHABLE_KEY="pk_test_DycfESRid31UaSxhI5yWKe1r5E5kKSz"
REACT_APP_STATSIG_API_KEY="client-1rY92WZGidd2hgW4x1lsZ7afqm1Qfr3sJfH3A5b8eJa"

View File

@@ -9,3 +9,4 @@ REACT_APP_MOONPAY_PUBLISHABLE_KEY="pk_live_uQG4BJC4w3cxnqpcSqAfohdBFDTsY6E"
REACT_APP_FIREBASE_KEY="AIzaSyBcZWwTcTJHj_R6ipZcrJkXdq05PuX0Rs0"
THE_GRAPH_SCHEMA_ENDPOINT="https://api.thegraph.com/subgraphs/name/uniswap/uniswap-v3"
REACT_APP_SENTRY_ENABLED=false
REACT_APP_STATSIG_API_KEY="client-1rY92WZGidd2hgW4x1lsZ7afqm1Qfr3sJfH3A5b8eJa"

View File

@@ -1,27 +0,0 @@
name: Revert
on:
# manual trigger
workflow_dispatch:
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: ./.github/actions/setup
- run: yarn prepare
- run: yarn build
- name: Setup node@16 (required by Cloudflare Pages)
uses: actions/setup-node@v3
with:
node-version: 16
- name: Update Cloudflare Pages deployment
uses: cloudflare/pages-action@364c7ca09a4b57837c5967871d64a2c31adb8c0d
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
projectName: ${{ secrets.CLOUDFLARE_PROJECT_NAME }}
directory: build
githubToken: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -30,6 +30,11 @@ jobs:
- uses: ./.github/actions/setup
- run: yarn prepare
- run: yarn test
- uses: codecov/codecov-action@v3
with:
token: ${{ secrets.CODECOV_TOKEN }}
fail_ci_if_error: false
verbose: true
cypress-build:
runs-on: ubuntu-latest

View File

@@ -1,5 +1,7 @@
# Uniswap Labs Interface
[![codecov](https://codecov.io/gh/Uniswap/interface/branch/main/graph/badge.svg?token=YVT2Y86O82)](https://codecov.io/gh/Uniswap/interface)
[![Unit Tests](https://github.com/Uniswap/interface/actions/workflows/unit-tests.yaml/badge.svg)](https://github.com/Uniswap/interface/actions/workflows/unit-tests.yaml)
[![Integration Tests](https://github.com/Uniswap/interface/actions/workflows/integration-tests.yaml/badge.svg)](https://github.com/Uniswap/interface/actions/workflows/integration-tests.yaml)
[![Lint](https://github.com/Uniswap/interface/actions/workflows/lint.yml/badge.svg)](https://github.com/Uniswap/interface/actions/workflows/lint.yml)
@@ -40,10 +42,10 @@ For steps on local deployment, development, and code contribution, please see [C
The Uniswap Interface supports swapping, adding liquidity, removing liquidity and migrating liquidity for Uniswap protocol V2.
- Swap on Uniswap V2: https://app.uniswap.org/#/swap?use=v2
- View V2 liquidity: https://app.uniswap.org/#/pool/v2
- Add V2 liquidity: https://app.uniswap.org/#/add/v2
- Migrate V2 liquidity to V3: https://app.uniswap.org/#/migrate/v2
- Swap on Uniswap V2: <https://app.uniswap.org/#/swap?use=v2>
- View V2 liquidity: <https://app.uniswap.org/#/pool/v2>
- Add V2 liquidity: <https://app.uniswap.org/#/add/v2>
- Migrate V2 liquidity to V3: <https://app.uniswap.org/#/migrate/v2>
## Accessing Uniswap V1

View File

@@ -12,9 +12,10 @@ module.exports = {
jest: {
configure(jestConfig) {
return Object.assign({}, jestConfig, {
transformIgnorePatterns: ['@uniswap/conedison/format'],
transformIgnorePatterns: ['@uniswap/conedison/format', '@uniswap/conedison/provider'],
moduleNameMapper: {
'@uniswap/conedison/format': '@uniswap/conedison/dist/format',
'@uniswap/conedison/provider': '@uniswap/conedison/dist/provider',
},
})
},

View File

@@ -1,11 +1,13 @@
import { getTestSelector } from '../utils'
const COLLECTION_ADDRESS = '0xbd3531da5cf5857e7cfaa92426877b022e612cf8'
const PUDGY_COLLECTION_ADDRESS = '0xbd3531da5cf5857e7cfaa92426877b022e612cf8'
const BONSAI_COLLECTION_ADDRESS = '0xec9c519d49856fd2f8133a0741b4dbe002ce211b'
describe('Testing nfts', () => {
beforeEach(() => {
cy.visit('/')
cy.get(getTestSelector('FiatOnrampAnnouncement-close')).first().click()
cy.visit('/').then(() => {
cy.get(getTestSelector('FiatOnrampAnnouncement-close')).first().click()
})
})
it('should load nft leaderboard', () => {
@@ -16,7 +18,7 @@ describe('Testing nfts', () => {
})
it('should load pudgy penguin collection page', () => {
cy.visit(`/#/nfts/collection/${COLLECTION_ADDRESS}`)
cy.visit(`/#/nfts/collection/${PUDGY_COLLECTION_ADDRESS}`)
cy.get(getTestSelector('nft-collection-asset')).should('exist')
cy.get(getTestSelector('nft-collection-filter-buy-now')).should('not.exist')
cy.get(getTestSelector('nft-filter')).first().click()
@@ -24,13 +26,13 @@ describe('Testing nfts', () => {
})
it('should be able to navigate to activity', () => {
cy.visit(`/#/nfts/collection/${COLLECTION_ADDRESS}`)
cy.visit(`/#/nfts/collection/${PUDGY_COLLECTION_ADDRESS}`)
cy.get(getTestSelector('nft-activity')).first().click()
cy.get(getTestSelector('nft-activity-row')).should('exist')
})
it('should go to the details page', () => {
cy.visit(`/#/nfts/collection/${COLLECTION_ADDRESS}`)
cy.visit(`/#/nfts/collection/${PUDGY_COLLECTION_ADDRESS}`)
cy.get(getTestSelector('nft-filter')).first().click()
cy.get(getTestSelector('nft-collection-filter-buy-now')).click()
cy.get(getTestSelector('nft-details-link')).first().click()
@@ -41,7 +43,7 @@ describe('Testing nfts', () => {
})
it('should toggle buy now on details page', () => {
cy.visit(`#/nfts/asset/${COLLECTION_ADDRESS}/8565`)
cy.visit(`#/nfts/asset/${BONSAI_COLLECTION_ADDRESS}/7580`)
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')

View File

@@ -1,11 +1,25 @@
import { getTestSelector } from '../utils'
describe('Pool', () => {
beforeEach(() => cy.visit('/pool'))
beforeEach(() => {
cy.visit('/pool').then(() => {
cy.wait('@eth_blockNumber')
})
})
it('add liquidity links to /add/ETH', () => {
cy.get(getTestSelector('FiatOnrampAnnouncement-close')).first().click()
cy.get('#join-pool-button').click()
cy.url().should('contain', '/add/ETH')
cy.get('body')
.then((body) => {
if (body.find(getTestSelector('FiatOnrampAnnouncement-close')).length > 0) {
cy.get(getTestSelector('FiatOnrampAnnouncement-close')).click()
}
})
.then(() => {
cy.get('#join-pool-button')
.click()
.then(() => {
cy.url().should('contain', '/add/ETH')
})
})
})
})

View File

@@ -0,0 +1,92 @@
import { getTestSelector } from '../utils'
describe('Token details', () => {
before(() => {
cy.visit('/')
})
it('Uniswap token should have all information populated', () => {
// Uniswap token
cy.visit('/tokens/ethereum/0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984')
// Price chart should be filled in
cy.get('[data-cy="chart-header"]').should('include.text', '$')
cy.get('[data-cy="price-chart"]').should('exist')
// Stats should have: TVL, 24H Volume, 52W low, 52W high
cy.get(getTestSelector('token-details-stats')).should('exist')
cy.get(getTestSelector('token-details-stats')).within(() => {
cy.get('[data-cy="tvl"]').should('include.text', '$')
cy.get('[data-cy="volume-24h"]').should('include.text', '$')
cy.get('[data-cy="52w-low"]').should('include.text', '$')
cy.get('[data-cy="52w-high"]').should('include.text', '$')
})
// About section should have description of token
cy.get(getTestSelector('token-details-about-section')).should('exist')
cy.contains('UNI is the governance token for Uniswap').should('exist')
// Links section should link out to Etherscan, More analytics, Website, Twitter
cy.get('[data-cy="resources-container"]').within(() => {
cy.contains('Etherscan')
.should('have.attr', 'href')
.and('include', 'etherscan.io/address/0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984')
cy.contains('More analytics')
.should('have.attr', 'href')
.and('include', 'info.uniswap.org/#/tokens/0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984')
cy.contains('Website').should('have.attr', 'href').and('include', 'uniswap.org')
cy.contains('Twitter').should('have.attr', 'href').and('include', 'twitter.com/Uniswap')
})
// Contract address should be displayed
cy.contains('0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984').should('exist')
// Swap widget should have this token pre-selected as the “destination” token
cy.get(getTestSelector('token-select')).should('include.text', 'UNI')
})
it('token with warning and low trading volume should have all information populated', () => {
// Shiba predator token, low trading volume and also has warning modal
cy.visit('/tokens/ethereum/0xa71d0588EAf47f12B13cF8eC750430d21DF04974')
// Should have missing price chart when price unavailable (expected for this token)
if (cy.get('[data-cy="chart-header"]').contains('Price Unavailable')) {
cy.get('[data-cy="missing-chart"]').should('exist')
}
// Stats should have: TVL, 24H Volume, 52W low, 52W high
cy.get(getTestSelector('token-details-stats')).should('exist')
cy.get(getTestSelector('token-details-stats')).within(() => {
cy.get('[data-cy="tvl"]').should('exist')
cy.get('[data-cy="volume-24h"]').should('exist')
cy.get('[data-cy="52w-low"]').should('exist')
cy.get('[data-cy="52w-high"]').should('exist')
})
// About section should have description of token
cy.get(getTestSelector('token-details-about-section')).should('exist')
cy.contains('QOM is the Shiba Predator').should('exist')
// Links section should link out to Etherscan, More analytics, Website, Twitter
cy.get('[data-cy="resources-container"]').within(() => {
cy.contains('Etherscan')
.should('have.attr', 'href')
.and('include', 'etherscan.io/address/0xa71d0588EAf47f12B13cF8eC750430d21DF04974')
cy.contains('More analytics')
.should('have.attr', 'href')
.and('include', 'info.uniswap.org/#/tokens/0xa71d0588EAf47f12B13cF8eC750430d21DF04974')
cy.contains('Website').should('have.attr', 'href').and('include', 'qom')
cy.contains('Twitter').should('have.attr', 'href').and('include', 'twitter.com/ShibaPredator1')
})
// Contract address should be displayed
cy.contains('0xa71d0588EAf47f12B13cF8eC750430d21DF04974').should('exist')
// Swap widget should have this token pre-selected as the “destination” token
cy.get(getTestSelector('token-select')).should('include.text', 'QOM')
// Warning label should show if relevant ([spec](https://www.notion.so/3f7fce6f93694be08a94a6984d50298e))
cy.get('[data-cy="token-safety-message"]')
.should('include.text', 'Warning')
.and('include.text', "This token isn't traded on leading U.S. centralized exchanges")
})
})

View File

@@ -0,0 +1,83 @@
describe.skip('Token explore filter', () => {
before(() => {
cy.visit('/')
})
it('should filter correctly by uni search term', () => {
cy.visit('/tokens')
cy.get('[data-cy="token-name"]').then(($els) => {
const tokenNames = Array.from($els, (el) => el.innerText)
const filteredByUni = tokenNames.filter((tokenName) => tokenName.toLowerCase().includes('uni'))
cy.wrap(filteredByUni).as('filteredByUni')
})
cy.get('[data-cy="explore-tokens-search-input"]')
.clear()
.type('uni')
.type('{enter}')
.then(() => {
cy.get('[data-cy="token-name"]').its('length').should('be.lt', 100)
cy.get('@filteredByUni').then((filteredByUni) => {
cy.get('[data-cy="token-name"]').then(($els) => {
const tokenNames = Array.from($els, (el) => el.innerText)
expect(tokenNames.length).to.equal(filteredByUni.length)
tokenNames.forEach((tokenName) => {
expect(filteredByUni).to.include(tokenName)
})
})
})
})
})
it('should filter correctly by dao search term', () => {
cy.visit('/tokens')
cy.get('[data-cy="token-name"]').then(($els) => {
const tokenNames = Array.from($els, (el) => el.innerText)
const filteredByDao = tokenNames.filter((tokenName) => tokenName.toLowerCase().includes('dao'))
cy.wrap(filteredByDao).as('filteredByDao')
})
cy.get('[data-cy="explore-tokens-search-input"]')
.clear()
.type('dao')
.type('{enter}')
.then(() => {
cy.get('[data-cy="token-name"]').its('length').should('be.lt', 100)
cy.get('@filteredByDao').then((filteredByDao) => {
cy.get('[data-cy="token-name"]').then(($els) => {
const tokenNames = Array.from($els, (el) => el.innerText)
expect(tokenNames.length).to.equal(filteredByDao.length)
tokenNames.forEach((tokenName) => {
expect(filteredByDao).to.include(tokenName)
})
})
})
})
})
it('should filter correctly by ax search term', () => {
cy.visit('/tokens')
cy.get('[data-cy="token-name"]').then(($els) => {
const tokenNames = Array.from($els, (el) => el.innerText)
const filteredByAx = tokenNames.filter((tokenName) => tokenName.toLowerCase().includes('ax'))
cy.wrap(filteredByAx).as('filteredByAx')
})
cy.get('[data-cy="explore-tokens-search-input"]')
.clear()
.type('ax')
.type('{enter}')
.then(() => {
cy.get('[data-cy="token-name"]').its('length').should('be.lt', 100)
cy.get('@filteredByAx').then((filteredByAx) => {
cy.get('[data-cy="token-name"]').then(($els) => {
const tokenNames = Array.from($els, (el) => el.innerText)
expect(tokenNames.length).to.equal(filteredByAx.length)
tokenNames.forEach((tokenName) => {
expect(filteredByAx).to.include(tokenName)
})
})
})
})
})
})

View File

@@ -0,0 +1,74 @@
import { getTestSelector, getTestSelectorStartsWith } from '../utils'
describe('Token explore', () => {
before(() => {
cy.visit('/')
})
it('should load token leaderboard', () => {
cy.visit('/tokens/ethereum')
cy.get(getTestSelectorStartsWith('token-table')).its('length').should('be.eq', 100)
// 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'))
.find(getTestSelector('percent-change-cell'))
.should('include.text', '%')
cy.get(getTestSelector('header-row')).find(getTestSelector('price-cell')).click()
cy.get(getTestSelector('header-row')).find(getTestSelector('price-cell')).find('svg').should('exist')
})
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'))
.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'))
.find(getTestSelector('volume-cell'))
.then(function ($elem) {
cy.wrap($elem.text()).as('yearlyEthVol')
})
expect(cy.get('@dailyEthVol')).to.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-details-about-section')).should('exist')
cy.get(getTestSelector('token-details-stats')).should('exist')
cy.get(getTestSelector('token-info-container')).should('exist')
cy.get(getTestSelector('chart-container')).should('exist')
cy.contains('Ethereum is a smart contract platform that enables developers to build tokens').should('exist')
cy.contains('Etherscan').should('exist')
})
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')
// 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')
})
it('should update when token explore table network changed', () => {
cy.visit('/tokens/ethereum')
cy.get(getTestSelector('tokens-network-filter-selected')).click()
cy.get(getTestSelector('tokens-network-filter-option-optimism')).click()
cy.get(getTestSelector('tokens-network-filter-selected')).should('contain', 'Optimism')
cy.reload()
cy.get(getTestSelector('tokens-network-filter-selected')).should('contain', 'Optimism')
cy.get(getTestSelector('chain-selector')).last().should('contain', 'Ethereum')
})
})

View File

@@ -1,55 +0,0 @@
import { getTestSelector, getTestSelectorStartsWith } from '../utils'
describe('Testing tokens on uniswap page', () => {
before(() => {
cy.visit('/')
})
it('should load token leaderboard', () => {
cy.visit('/tokens/ethereum')
cy.get(getTestSelectorStartsWith('token-table')).its('length').should('be.gte', 25)
})
it('should keep the same configuration when reloaded: ETH global, OP local', () => {
cy.visit('/tokens/ethereum')
cy.get(getTestSelector('tokens-network-filter-selected')).click()
cy.get(getTestSelector('tokens-network-filter-option-optimism')).click()
cy.reload()
cy.get(getTestSelector('tokens-network-filter-selected')).should('contain', 'Optimism')
})
it('should have the correct network configuration when reloaded: OP global, Polygon local', () => {
cy.get(getTestSelector('chain-selector')).last().click()
cy.get(getTestSelector('chain-selector-option-optimism')).click()
cy.visit('/tokens/ethereum')
cy.get(getTestSelector('tokens-network-filter-selected')).click()
cy.get(getTestSelector('tokens-network-filter-option-polygon')).click()
cy.reload()
cy.get(getTestSelector('tokens-network-filter-selected')).should('contain', 'Polygon')
// With no wallet connected, reloading the page resets the global network.
cy.get(getTestSelector('chain-selector')).last().should('contain', 'Ethereum')
})
it('should load go to ethereum token and return to token list page', () => {
cy.visit('/tokens/ethereum')
cy.get(getTestSelector('token-table-row-Ether')).click()
cy.get(getTestSelector('token-details-stats')).should('exist')
cy.get(getTestSelector('token-details-return-button')).click()
cy.get(getTestSelectorStartsWith('token-table')).its('length').should('be.gte', 25)
})
it('should go to native token on ethereum and render description', () => {
cy.visit('/tokens/ethereum/NATIVE')
cy.get(getTestSelector('token-details-about-section')).should('exist')
cy.contains('Ethereum is a smart contract platform that enables developers').should('exist')
cy.contains('Etherscan').should('exist')
})
it('should go to native token on polygon and render description and links', () => {
cy.visit('/tokens/polygon/NATIVE')
cy.get(getTestSelector('token-details-about-section')).should('exist')
cy.contains('Wrapped Matic on Polygon').should('exist')
cy.contains('Block Explorer').should('exist')
})
})

View File

@@ -0,0 +1,64 @@
import { getTestSelector } from '../utils'
describe('Universal search bar', () => {
before(() => {
cy.visit('/')
cy.get('[data-cy="magnifying-icon"]')
.parent()
.then(($navIcon) => {
$navIcon.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"]')
.should('contain.text', 'Uniswap')
.and('contain.text', 'UNI')
.and('contain.text', '$')
.and('contain.text', '%')
cy.get('[data-cy="searchbar-token-row-UNI"]').click()
cy.get('div').contains('Uniswap').should('exist')
// Stats should have: TVL, 24H Volume, 52W low, 52W high.
cy.get(getTestSelector('token-details-stats')).should('exist')
cy.get(getTestSelector('token-details-stats')).within(() => {
cy.get('[data-cy="tvl"]').should('include.text', '$')
cy.get('[data-cy="volume-24h"]').should('include.text', '$')
cy.get('[data-cy="52w-low"]').should('include.text', '$')
cy.get('[data-cy="52w-high"]').should('include.text', '$')
})
// About section should have description of token.
cy.get(getTestSelector('token-details-about-section')).should('exist')
cy.contains('UNI is the governance token for Uniswap').should('exist')
})
it('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"]')
.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')
})
})

View File

@@ -13,8 +13,8 @@ describe('Wallet Dropdown', () => {
it('should select a language', () => {
cy.get(getTestSelector('wallet-select-language')).click()
cy.get(getTestSelector('wallet-language-item')).contains('Afrikaans').click({ force: true })
cy.get(getTestSelector('wallet-header')).should('contain', 'Taal')
cy.get(getTestSelector('wallet-language-item')).contains('Deutsch').click({ force: true })
cy.get(getTestSelector('wallet-header')).should('contain', 'Sprache')
cy.get(getTestSelector('wallet-language-item')).contains('English').click({ force: true })
cy.get(getTestSelector('wallet-header')).should('contain', 'Language')
cy.get(getTestSelector('wallet-back')).click()
@@ -36,8 +36,8 @@ describe('Wallet Dropdown', () => {
it('should select a language when not connected', () => {
cy.get(getTestSelector('wallet-select-language')).click()
cy.get(getTestSelector('wallet-language-item')).contains('Afrikaans').click({ force: true })
cy.get(getTestSelector('wallet-header')).should('contain', 'Taal')
cy.get(getTestSelector('wallet-language-item')).contains('Deutsch').click({ force: true })
cy.get(getTestSelector('wallet-header')).should('contain', 'Sprache')
cy.get(getTestSelector('wallet-language-item')).contains('English').click({ force: true })
cy.get(getTestSelector('wallet-header')).should('contain', 'Language')
cy.get(getTestSelector('wallet-back')).click()

View File

@@ -70,6 +70,7 @@ beforeEach(() => {
// These are stripped by cypress because chromeWebSecurity === false; this adds them back in.
cy.intercept(/infura.io/, (res) => {
res.headers['origin'] = 'http://localhost:3000'
res.alias = res.body.method
res.continue()
})

View File

@@ -96,6 +96,7 @@
"cypress": "^10.3.1",
"env-cmd": "^10.1.0",
"eslint": "^7.11.0",
"jest-fetch-mock": "^3.0.3",
"jest-styled-components": "^7.0.8",
"ms.macro": "^2.0.0",
"patch-package": "^6.4.7",
@@ -132,9 +133,9 @@
"@reduxjs/toolkit": "^1.6.1",
"@sentry/react": "^7.29.0",
"@types/react-window-infinite-loader": "^1.0.6",
"@uniswap/analytics": "1.2.0",
"@uniswap/analytics-events": "^2.1.0",
"@uniswap/conedison": "^1.1.1",
"@uniswap/analytics": "^1.3.0",
"@uniswap/analytics-events": "^2.3.0",
"@uniswap/conedison": "^1.3.0",
"@uniswap/governance": "^1.0.2",
"@uniswap/liquidity-staker": "^1.0.2",
"@uniswap/merkle-distributor": "1.0.1",
@@ -144,14 +145,14 @@
"@uniswap/sdk-core": "^3.0.1",
"@uniswap/smart-order-router": "^2.10.0",
"@uniswap/token-lists": "^1.0.0-beta.30",
"@uniswap/universal-router-sdk": "1.3.0",
"@uniswap/universal-router-sdk": "^1.3.6",
"@uniswap/v2-core": "1.0.0",
"@uniswap/v2-periphery": "^1.1.0-beta.0",
"@uniswap/v2-sdk": "^3.0.1",
"@uniswap/v3-core": "1.0.0",
"@uniswap/v3-periphery": "^1.1.1",
"@uniswap/v3-sdk": "^3.9.0",
"@uniswap/widgets": "2.22.11",
"@uniswap/widgets": "^2.29.3",
"@vanilla-extract/css": "^1.7.2",
"@vanilla-extract/css-utils": "^0.1.2",
"@vanilla-extract/dynamic": "^2.0.2",
@@ -164,16 +165,16 @@
"@visx/responsive": "^2.10.0",
"@visx/shape": "^2.11.1",
"@walletconnect/ethereum-provider": "^1.8.0",
"@web3-react/coinbase-wallet": "8.0.34-beta.0",
"@web3-react/coinbase-wallet": "8.0.35-beta.0",
"@web3-react/core": "8.0.35-beta.0",
"@web3-react/eip1193": "8.0.26-beta.0",
"@web3-react/eip1193": "8.0.27-beta.0",
"@web3-react/empty": "8.0.20-beta.0",
"@web3-react/gnosis-safe": "8.0.7-beta.0",
"@web3-react/metamask": "8.0.28-beta.0",
"@web3-react/metamask": "8.0.30-beta.0",
"@web3-react/network": "8.0.27-beta.0",
"@web3-react/types": "8.0.20-beta.0",
"@web3-react/url": "8.0.25-beta.0",
"@web3-react/walletconnect": "8.0.36-beta.0",
"@web3-react/walletconnect": "8.0.37-beta.0",
"array.prototype.flat": "^1.2.4",
"array.prototype.flatmap": "^1.2.4",
"cids": "^1.0.0",
@@ -221,6 +222,7 @@
"redux": "^4.1.2",
"redux-localstorage-simple": "^2.3.1",
"setimmediate": "^1.0.5",
"statsig-react": "^1.22.0",
"styled-components": "^5.3.5",
"tiny-invariant": "^1.2.0",
"ua-parser-js": "^0.7.28",

41
src/abis/permit2.json Normal file
View File

@@ -0,0 +1,41 @@
[
{
"inputs": [
{
"internalType": "address",
"name": "",
"type": "address"
},
{
"internalType": "address",
"name": "",
"type": "address"
},
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"name": "allowance",
"outputs": [
{
"internalType": "uint160",
"name": "amount",
"type": "uint160"
},
{
"internalType": "uint48",
"name": "expiration",
"type": "uint48"
},
{
"internalType": "uint48",
"name": "nonce",
"type": "uint48"
}
],
"stateMutability": "view",
"type": "function"
}
]

View File

@@ -1,9 +1,8 @@
import { TraceEvent } from '@uniswap/analytics'
import { BrowserEvent, InterfaceElementName, SharedEventName } from '@uniswap/analytics-events'
import { Link } from 'react-router-dom'
import { useIsDarkMode } from 'state/user/hooks'
import styled from 'styled-components/macro'
import { BREAKPOINTS, ExternalLink } from 'theme'
import { BREAKPOINTS, ExternalLink, StyledRouterLink } from 'theme'
import { DiscordIcon, GithubIcon, TwitterIcon } from './Icons'
import darkUnicornImgSrc from './images/unicornEmbossDark.png'
@@ -97,14 +96,10 @@ const ExternalTextLink = styled(ExternalLink)`
color: ${({ theme }) => theme.textSecondary};
`
const TextLink = styled(Link)`
const TextLink = styled(StyledRouterLink)`
font-size: 16px;
line-height: 20px;
color: ${({ theme }) => theme.textSecondary};
text-decoration: none;
&:hover {
text-decoration: underline;
}
`
const Copyright = styled.span`
@@ -120,7 +115,7 @@ const LogoSectionContent = () => {
<>
<StyledLogo src={isDarkMode ? darkUnicornImgSrc : lightUnicornImgSrc} alt="Uniswap Logo" />
<SocialLinks>
<SocialLink href="https://github.com/Uniswap" target="_blank" rel="noopener noreferrer">
<SocialLink href="https://discord.gg/FCfyBSbCU5" target="_blank" rel="noopener noreferrer">
<DiscordIcon size={32} />
</SocialLink>
<TraceEvent
@@ -132,7 +127,7 @@ const LogoSectionContent = () => {
<TwitterIcon size={32} />
</SocialLink>
</TraceEvent>
<SocialLink href="https://discord.gg/FCfyBSbCU5" target="_blank" rel="noopener noreferrer">
<SocialLink href="https://github.com/Uniswap" target="_blank" rel="noopener noreferrer">
<GithubIcon size={32} />
</SocialLink>
</SocialLinks>
@@ -158,15 +153,9 @@ export const AboutFooter = () => {
</LinkGroup>
<LinkGroup>
<LinkGroupTitle>Protocol</LinkGroupTitle>
<ExternalTextLink href="https://uniswap.org/community" target="_blank" rel="noopener noreferrer">
Community
</ExternalTextLink>
<ExternalTextLink href="https://uniswap.org/governance" target="_blank" rel="noopener noreferrer">
Governance
</ExternalTextLink>
<ExternalTextLink href="https://uniswap.org/developers" target="_blank" rel="noopener noreferrer">
Developers
</ExternalTextLink>
<ExternalTextLink href="https://uniswap.org/community">Community</ExternalTextLink>
<ExternalTextLink href="https://uniswap.org/governance">Governance</ExternalTextLink>
<ExternalTextLink href="https://uniswap.org/developers">Developers</ExternalTextLink>
</LinkGroup>
<LinkGroup>
<LinkGroupTitle>Company</LinkGroupTitle>
@@ -175,18 +164,14 @@ export const AboutFooter = () => {
name={SharedEventName.ELEMENT_CLICKED}
element={InterfaceElementName.CAREERS_LINK}
>
<ExternalTextLink href="https://boards.greenhouse.io/uniswaplabs" target="_blank" rel="noopener noreferrer">
Careers
</ExternalTextLink>
<ExternalTextLink href="https://boards.greenhouse.io/uniswaplabs">Careers</ExternalTextLink>
</TraceEvent>
<TraceEvent
events={[BrowserEvent.onClick]}
name={SharedEventName.ELEMENT_CLICKED}
element={InterfaceElementName.BLOG_LINK}
>
<ExternalTextLink href="https://uniswap.org/blog" target="_blank" rel="noopener noreferrer">
Blog
</ExternalTextLink>
<ExternalTextLink href="https://uniswap.org/blog">Blog</ExternalTextLink>
</TraceEvent>
</LinkGroup>
<LinkGroup>
@@ -209,9 +194,7 @@ export const AboutFooter = () => {
name={SharedEventName.ELEMENT_CLICKED}
element={InterfaceElementName.SUPPORT_LINK}
>
<ExternalTextLink href="https://support.uniswap.org/hc/en-us" target="_blank" rel="noopener noreferrer">
Help Center
</ExternalTextLink>
<ExternalTextLink href="https://support.uniswap.org/hc/en-us">Help Center</ExternalTextLink>
</TraceEvent>
</LinkGroup>
</FooterLinks>

View File

@@ -1,11 +1,6 @@
import { Trans } from '@lingui/macro'
import { useWeb3React } from '@web3-react/core'
import {
getConnection,
getConnectionName,
getHasCoinbaseExtensionInstalled,
getHasMetaMaskExtensionInstalled,
} from 'connection/utils'
import { getConnection, getConnectionName, getIsCoinbaseWallet, getIsMetaMaskWallet } from 'connection/utils'
import { useCallback } from 'react'
import { ExternalLink as LinkIcon } from 'react-feather'
import { useAppDispatch } from 'state/hooks'
@@ -215,8 +210,8 @@ export default function AccountDetails({
const theme = useTheme()
const dispatch = useAppDispatch()
const hasMetaMaskExtension = getHasMetaMaskExtensionInstalled()
const hasCoinbaseExtension = getHasCoinbaseExtensionInstalled()
const hasMetaMaskExtension = getIsMetaMaskWallet()
const hasCoinbaseExtension = getIsCoinbaseWallet()
const isInjectedMobileBrowser = (hasMetaMaskExtension || hasCoinbaseExtension) && isMobile
function formatConnectorName() {

View File

@@ -97,7 +97,8 @@ export const ButtonPrimary = styled(BaseButton)`
export const SmallButtonPrimary = styled(ButtonPrimary)`
width: auto;
font-size: 16px;
padding: 10px 16px;
padding: ${({ padding }) => padding ?? '8px 12px'};
border-radius: 12px;
`

View File

@@ -17,10 +17,10 @@ interface DoubleCurrencyLogoProps {
currency1?: Currency
}
const HigherLogo = styled(CurrencyLogo)`
const HigherLogoWrapper = styled.div`
z-index: 1;
`
const CoveredLogo = styled(CurrencyLogo)<{ sizeraw: number }>`
const CoveredLogoWapper = styled.div<{ sizeraw: number }>`
position: absolute;
left: ${({ sizeraw }) => '-' + (sizeraw / 2).toString() + 'px'} !important;
`
@@ -33,8 +33,16 @@ export default function DoubleCurrencyLogo({
}: DoubleCurrencyLogoProps) {
return (
<Wrapper sizeraw={size} margin={margin}>
{currency0 && <HigherLogo currency={currency0} size={size.toString() + 'px'} />}
{currency1 && <CoveredLogo currency={currency1} size={size.toString() + 'px'} sizeraw={size} />}
{currency0 && (
<HigherLogoWrapper>
<CurrencyLogo hideL2Icon currency={currency0} size={size.toString() + 'px'} />
</HigherLogoWrapper>
)}
{currency1 && (
<CoveredLogoWapper sizeraw={size}>
<CurrencyLogo hideL2Icon currency={currency1} size={size.toString() + 'px'} />
</CoveredLogoWapper>
)}
</Wrapper>
)
}

View File

@@ -1,7 +1,9 @@
import { BaseVariant, FeatureFlag, featureFlagSettings, useUpdateFlag } from 'featureFlags'
import { useFiatOnrampFlag } from 'featureFlags/flags/fiatOnramp'
import { GqlRoutingVariant, useGqlRoutingFlag } from 'featureFlags/flags/gqlRouting'
import { NftListV2Variant, useNftListV2Flag } from 'featureFlags/flags/nftListV2'
import { PayWithAnyTokenVariant, usePayWithAnyTokenFlag } from 'featureFlags/flags/payWithAnyToken'
import { Permit2Variant, usePermit2Flag } from 'featureFlags/flags/permit2'
import { SwapWidgetVariant, useSwapWidgetFlag } from 'featureFlags/flags/swapWidget'
import { TraceJsonRpcVariant, useTraceJsonRpcFlag } from 'featureFlags/flags/traceJsonRpc'
import { useAtomValue, useUpdateAtom } from 'jotai/utils'
import { Children, PropsWithChildren, ReactElement, ReactNode, useCallback, useState } from 'react'
@@ -210,18 +212,30 @@ export default function FeatureFlagModal() {
featureFlag={FeatureFlag.permit2}
label="Permit 2 / Universal Router"
/>
<FeatureFlagOption
variant={BaseVariant}
value={useFiatOnrampFlag()}
featureFlag={FeatureFlag.fiatOnramp}
label="Fiat on-ramp"
/>
<FeatureFlagOption
variant={NftListV2Variant}
value={useNftListV2Flag()}
featureFlag={FeatureFlag.nftListV2}
label="NFT Listing Page v2"
/>
<FeatureFlagOption
variant={PayWithAnyTokenVariant}
value={usePayWithAnyTokenFlag()}
featureFlag={FeatureFlag.payWithAnyToken}
label="Pay With Any Token"
/>
<FeatureFlagOption
variant={SwapWidgetVariant}
value={useSwapWidgetFlag()}
featureFlag={FeatureFlag.swapWidget}
label="Swap Widget"
/>
<FeatureFlagOption
variant={GqlRoutingVariant}
value={useGqlRoutingFlag()}
featureFlag={FeatureFlag.gqlRouting}
label="GraphQL NFT Routing"
/>
<FeatureFlagGroup name="Debug">
<FeatureFlagOption
variant={TraceJsonRpcVariant}

View File

@@ -3,9 +3,7 @@ import { sendAnalyticsEvent } from '@uniswap/analytics'
import { InterfaceEventName } from '@uniswap/analytics-events'
import { useWeb3React } from '@web3-react/core'
import fiatMaskUrl from 'assets/svg/fiat_mask.svg'
import { BaseVariant } from 'featureFlags'
import { useFiatOnrampFlag } from 'featureFlags/flags/fiatOnramp'
import { useCallback, useEffect } from 'react'
import { useCallback, useEffect, useState } from 'react'
import { X } from 'react-feather'
import { useToggleWalletDropdown } from 'state/application/hooks'
import { useAppSelector } from 'state/hooks'
@@ -100,6 +98,7 @@ const MAX_RENDER_COUNT = 3
export function FiatOnrampAnnouncement() {
const { account } = useWeb3React()
const [acks, acknowledge] = useFiatOnrampAck()
const [localClose, setLocalClose] = useState(false)
useEffect(() => {
if (!sessionStorage.getItem(ANNOUNCEMENT_RENDERED)) {
acknowledge({ renderCount: acks?.renderCount + 1 })
@@ -108,6 +107,7 @@ export function FiatOnrampAnnouncement() {
}, [acknowledge, acks])
const handleClose = useCallback(() => {
setLocalClose(true)
localStorage.setItem(ANNOUNCEMENT_DISMISSED, 'true')
}, [])
@@ -118,17 +118,16 @@ export function FiatOnrampAnnouncement() {
acknowledge({ user: true })
}, [acknowledge, toggleWalletDropdown])
const fiatOnrampFlag = useFiatOnrampFlag()
const openModal = useAppSelector((state) => state.application.openModal)
if (
!account ||
acks?.user ||
fiatOnrampFlag === BaseVariant.Control ||
localStorage.getItem(ANNOUNCEMENT_DISMISSED) ||
acks?.renderCount >= MAX_RENDER_COUNT ||
isMobile ||
openModal !== null
openModal !== null ||
localClose
) {
return null
}

View File

@@ -3,6 +3,7 @@ import { useWeb3React } from '@web3-react/core'
import { useCallback, useEffect, useState } from 'react'
import { useCloseModal, useModalIsOpen } from 'state/application/hooks'
import { ApplicationModal } from 'state/application/reducer'
import { useIsDarkMode } from 'state/user/hooks'
import styled, { useTheme } from 'styled-components/macro'
import { CustomLightSpinner, ThemedText } from 'theme'
@@ -67,6 +68,7 @@ const MOONPAY_SUPPORTED_CURRENCY_CODES = [
export default function FiatOnrampModal() {
const { account } = useWeb3React()
const theme = useTheme()
const isDarkMode = useIsDarkMode()
const closeModal = useCloseModal()
const fiatOnrampModalOpen = useModalIsOpen(ApplicationModal.FIAT_ONRAMP)
@@ -90,6 +92,7 @@ export default function FiatOnrampModal() {
},
method: 'POST',
body: JSON.stringify({
theme: isDarkMode ? 'dark' : 'light',
colorCode: theme.accentAction,
defaultCurrencyCode: 'eth',
redirectUrl: 'https://app.uniswap.org/#/swap',
@@ -112,7 +115,7 @@ export default function FiatOnrampModal() {
} finally {
setLoading(false)
}
}, [account, theme.accentAction])
}, [account, isDarkMode, theme.accentAction])
useEffect(() => {
fetchSignedIframeUrl()

View File

@@ -1,3 +1,4 @@
import { getChainInfo } from 'constants/chainInfo'
import { SupportedChainId } from 'constants/chains'
import useTokenLogoSource from 'hooks/useAssetLogoSource'
import React from 'react'
@@ -29,10 +30,28 @@ export type AssetLogoBaseProps = {
backupImg?: string | null
size?: string
style?: React.CSSProperties
hideL2Icon?: boolean
}
type AssetLogoProps = AssetLogoBaseProps & { isNative?: boolean; address?: string | null; chainId?: number }
// TODO(cartcrom): add prop to optionally render an L2Icon w/ the logo
const LogoContainer = styled.div`
position: relative;
display: flex;
`
const L2NetworkLogo = styled.div<{ networkUrl?: string; parentSize: string }>`
--size: ${({ parentSize }) => `calc(${parentSize} / 2)`};
width: var(--size);
height: var(--size);
position: absolute;
left: 50%;
bottom: 0;
background: url(${({ networkUrl }) => networkUrl});
background-repeat: no-repeat;
background-size: ${({ parentSize }) => `calc(${parentSize} / 2) calc(${parentSize} / 2)`};
display: ${({ networkUrl }) => !networkUrl && 'none'};
`
/**
* Renders an image by prioritizing a list of sources, and then eventually a fallback triangle alert
*/
@@ -44,25 +63,27 @@ export default function AssetLogo({
backupImg,
size = '24px',
style,
...rest
hideL2Icon = false,
}: AssetLogoProps) {
const imageProps = {
alt: `${symbol ?? 'token'} logo`,
size,
style,
...rest,
}
const [src, nextSrc] = useTokenLogoSource(address, chainId, isNative, backupImg)
const L2Icon = getChainInfo(chainId)?.circleLogoUrl
if (src) {
return <LogoImage {...imageProps} src={src} onError={nextSrc} />
} else {
return (
<MissingImageLogo size={size}>
{/* use only first 3 characters of Symbol for design reasons */}
{symbol?.toUpperCase().replace('$', '').replace(/\s+/g, '').slice(0, 3)}
</MissingImageLogo>
)
}
return (
<LogoContainer style={style}>
{src ? (
<LogoImage {...imageProps} src={src} onError={nextSrc} />
) : (
<MissingImageLogo size={size}>
{/* use only first 3 characters of Symbol for design reasons */}
{symbol?.toUpperCase().replace('$', '').replace(/\s+/g, '').slice(0, 3)}
</MissingImageLogo>
)}
{!hideL2Icon && <L2NetworkLogo networkUrl={L2Icon} parentSize={size} />}
</LogoContainer>
)
}

View File

@@ -15,6 +15,7 @@ export default function CurrencyLogo(
address={props.currency?.wrapped.address}
symbol={props.symbol ?? props.currency?.symbol}
backupImg={(props.currency as TokenInfo)?.logoURI}
hideL2Icon={props.hideL2Icon ?? true}
{...props}
/>
)

View File

@@ -1,4 +1,6 @@
import { NATIVE_CHAIN_ID } from 'constants/tokens'
import { TokenStandard } from 'graphql/data/__generated__/types-and-hooks'
import { SearchToken } from 'graphql/data/SearchTokens'
import { TokenQueryData } from 'graphql/data/Token'
import { TopToken } from 'graphql/data/TopTokens'
import { CHAIN_NAME_TO_CHAIN_ID } from 'graphql/data/util'
@@ -7,14 +9,14 @@ import AssetLogo, { AssetLogoBaseProps } from './AssetLogo'
export default function QueryTokenLogo(
props: AssetLogoBaseProps & {
token?: TopToken | TokenQueryData
token?: TopToken | TokenQueryData | SearchToken
}
) {
const chainId = props.token?.chain ? CHAIN_NAME_TO_CHAIN_ID[props.token?.chain] : undefined
return (
<AssetLogo
isNative={props.token?.address === NATIVE_CHAIN_ID}
isNative={props.token?.standard === TokenStandard.Native || props.token?.address === NATIVE_CHAIN_ID}
chainId={chainId}
address={props.token?.address}
symbol={props.token?.symbol}

View File

@@ -17,6 +17,9 @@ const StyledDialogOverlay = styled(AnimatedDialogOverlay)<{ $scrollOverlay?: boo
display: flex;
align-items: center;
@media screen and (max-width: ${({ theme }) => theme.breakpoint.sm}px) {
align-items: flex-end;
}
overflow-y: ${({ $scrollOverlay }) => $scrollOverlay && 'scroll'};
justify-content: center;
@@ -27,7 +30,6 @@ const StyledDialogOverlay = styled(AnimatedDialogOverlay)<{ $scrollOverlay?: boo
type StyledDialogProps = {
$minHeight?: number | false
$maxHeight?: number
$isBottomSheet?: boolean
$scrollOverlay?: boolean
$hideBorder?: boolean
$maxWidth: number
@@ -40,14 +42,12 @@ const StyledDialogContent = styled(AnimatedDialogContent)<StyledDialogProps>`
&[data-reach-dialog-content] {
margin: auto;
background-color: ${({ theme }) => theme.backgroundSurface};
border: ${({ theme, $hideBorder }) => !$hideBorder && `1px solid ${theme.deprecated_bg1}`};
border: ${({ theme, $hideBorder }) => !$hideBorder && `1px solid ${theme.backgroundOutline}`};
box-shadow: ${({ theme }) => theme.deepShadow};
padding: 0px;
width: 50vw;
overflow-y: auto;
overflow-x: hidden;
align-self: ${({ $isBottomSheet }) => $isBottomSheet && 'flex-end'};
max-width: ${({ $maxWidth }) => $maxWidth}px;
${({ $maxHeight }) =>
$maxHeight &&
@@ -61,22 +61,17 @@ const StyledDialogContent = styled(AnimatedDialogContent)<StyledDialogProps>`
`}
display: ${({ $scrollOverlay }) => ($scrollOverlay ? 'inline-table' : 'flex')};
border-radius: 20px;
${({ theme }) => theme.deprecated_mediaWidth.deprecated_upToMedium`
@media screen and (max-width: ${({ theme }) => theme.breakpoint.md}px) {
width: 65vw;
margin: auto;
`}
${({ theme, $isBottomSheet }) => theme.deprecated_mediaWidth.deprecated_upToSmall`
width: 85vw;
${
$isBottomSheet &&
css`
width: 100vw;
border-radius: 20px;
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
`
}
`}
}
@media screen and (max-width: ${({ theme }) => theme.breakpoint.sm}px) {
margin: 0;
width: 100vw;
border-radius: 20px;
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
}
}
`
@@ -91,7 +86,6 @@ interface ModalProps {
children?: React.ReactNode
$scrollOverlay?: boolean
hideBorder?: boolean
isBottomSheet?: boolean
}
export default function Modal({
@@ -104,7 +98,6 @@ export default function Modal({
children,
onSwipe = onDismiss,
$scrollOverlay,
isBottomSheet = isMobile,
hideBorder = false,
}: ModalProps) {
const fadeTransition = useTransition(isOpen, {
@@ -148,7 +141,6 @@ export default function Modal({
aria-label="dialog"
$minHeight={minHeight}
$maxHeight={maxHeight}
$isBottomSheet={isBottomSheet}
$scrollOverlay={$scrollOverlay}
$hideBorder={hideBorder}
$maxWidth={maxWidth}

View File

@@ -166,6 +166,9 @@ export const MenuDropdown = () => {
<SecondaryLinkedText href="https://docs.uniswap.org/">
<Trans>Documentation</Trans>
</SecondaryLinkedText>
<SecondaryLinkedText href="https://uniswap.canny.io/feature-requests">
<Trans>Feedback</Trans>
</SecondaryLinkedText>
<SecondaryLinkedText
onClick={() => {
toggleOpen()

View File

@@ -0,0 +1,102 @@
import { SupportedChainId } from 'constants/chains'
import { NATIVE_CHAIN_ID, nativeOnChain } from 'constants/tokens'
import { Chain, NftCollection, useRecentlySearchedAssetsQuery } from 'graphql/data/__generated__/types-and-hooks'
import { SearchToken } from 'graphql/data/SearchTokens'
import { CHAIN_NAME_TO_CHAIN_ID } from 'graphql/data/util'
import { useAtom } from 'jotai'
import { atomWithStorage, useAtomValue } from 'jotai/utils'
import { GenieCollection } from 'nft/types'
import { useCallback, useMemo } from 'react'
import { getNativeTokenDBAddress } from 'utils/nativeTokens'
type RecentlySearchedAsset = {
isNft?: boolean
address: string
chain: Chain
}
// Temporary measure used until backend supports addressing by "NATIVE"
const NATIVE_QUERY_ADDRESS_INPUT = null as unknown as string
function getQueryAddress(chain: Chain) {
return getNativeTokenDBAddress(chain) ?? NATIVE_QUERY_ADDRESS_INPUT
}
const recentlySearchedAssetsAtom = atomWithStorage<RecentlySearchedAsset[]>('recentlySearchedAssets', [])
export function useAddRecentlySearchedAsset() {
const [searchHistory, updateSearchHistory] = useAtom(recentlySearchedAssetsAtom)
return useCallback(
(asset: RecentlySearchedAsset) => {
// Removes the new asset if it was already in the array
const newHistory = searchHistory.filter(
(oldAsset) => !(oldAsset.address === asset.address && oldAsset.chain === asset.chain)
)
newHistory.unshift(asset)
updateSearchHistory(newHistory)
},
[searchHistory, updateSearchHistory]
)
}
export function useRecentlySearchedAssets() {
const history = useAtomValue(recentlySearchedAssetsAtom)
const shortenedHistory = useMemo(() => history.slice(0, 4), [history])
const { data: queryData, loading } = useRecentlySearchedAssetsQuery({
variables: {
collectionAddresses: shortenedHistory.filter((asset) => asset.isNft).map((asset) => asset.address),
contracts: shortenedHistory
.filter((asset) => !asset.isNft)
.map((token) => ({
address: token.address === NATIVE_CHAIN_ID ? getQueryAddress(token.chain) : token.address,
chain: token.chain,
})),
},
})
const data = useMemo(() => {
if (shortenedHistory.length === 0) return []
else if (!queryData) return undefined
// Collects both tokens and collections in a map, so they can later be returned in original order
const resultsMap: { [key: string]: GenieCollection | SearchToken } = {}
const queryCollections = queryData?.nftCollections?.edges.map((edge) => edge.node as NonNullable<NftCollection>)
const collections = queryCollections?.map(
(queryCollection): GenieCollection => {
return {
address: queryCollection.nftContracts?.[0]?.address ?? '',
isVerified: queryCollection?.isVerified,
name: queryCollection?.name,
stats: {
floor_price: queryCollection?.markets?.[0]?.floorPrice?.value,
total_supply: queryCollection?.numAssets,
},
imageUrl: queryCollection?.image?.url ?? '',
}
},
[queryCollections]
)
collections?.forEach((collection) => (resultsMap[collection.address] = collection))
queryData.tokens?.filter(Boolean).forEach((token) => {
resultsMap[token.address ?? `NATIVE-${token.chain}`] = token
})
const data: (SearchToken | GenieCollection)[] = []
shortenedHistory.forEach((asset) => {
if (asset.address === 'NATIVE') {
// Handles special case where wMATIC data needs to be used for MATIC
const native = nativeOnChain(CHAIN_NAME_TO_CHAIN_ID[asset.chain] ?? SupportedChainId.MAINNET)
const queryAddress = getQueryAddress(asset.chain)?.toLowerCase() ?? `NATIVE-${asset.chain}`
const result = resultsMap[queryAddress]
if (result) data.push({ ...result, address: 'NATIVE', ...native })
} else {
const result = resultsMap[asset.address]
if (result) data.push(result)
}
})
return data
}, [queryData, shortenedHistory])
return { data, loading }
}

View File

@@ -2,7 +2,9 @@
import { t, Trans } from '@lingui/macro'
import { sendAnalyticsEvent, Trace, TraceEvent, useTrace } from '@uniswap/analytics'
import { BrowserEvent, InterfaceElementName, InterfaceEventName, InterfaceSectionName } from '@uniswap/analytics-events'
import { useWeb3React } from '@web3-react/core'
import clsx from 'clsx'
import { useSearchTokens } from 'graphql/data/SearchTokens'
import useDebounce from 'hooks/useDebounce'
import { useIsNftPage } from 'hooks/useIsNftPage'
import { useOnClickOutside } from 'hooks/useOnClickOutside'
@@ -12,7 +14,6 @@ import { Row } from 'nft/components/Flex'
import { magicalGradientOnHover } from 'nft/css/common.css'
import { useIsMobile, useIsTablet } from 'nft/hooks'
import { fetchSearchCollections } from 'nft/queries'
import { fetchSearchTokens } from 'nft/queries/genie/SearchTokensFetcher'
import { ChangeEvent, useCallback, useEffect, useMemo, useReducer, useRef, useState } from 'react'
import { useQuery } from 'react-query'
import { useLocation } from 'react-router-dom'
@@ -64,16 +65,8 @@ export const SearchBar = () => {
}
)
const { data: tokens, isLoading: tokensAreLoading } = useQuery(
['searchTokens', debouncedSearchValue],
() => fetchSearchTokens(debouncedSearchValue),
{
refetchOnWindowFocus: false,
refetchOnMount: false,
refetchOnReconnect: false,
enabled: !!debouncedSearchValue.length,
}
)
const { chainId } = useWeb3React()
const { data: tokens, loading: tokensAreLoading } = useSearchTokens(debouncedSearchValue, chainId ?? 1)
const isNFTPage = useIsNftPage()
@@ -148,6 +141,7 @@ export const SearchBar = () => {
return (
<Trace section={InterfaceSectionName.NAVBAR_SEARCH}>
<Box
data-cy="search-bar"
position={{ sm: 'fixed', md: 'absolute', xl: 'relative' }}
width={{ sm: isOpen ? 'viewWidth' : 'auto', md: 'auto' }}
ref={searchRef}
@@ -187,6 +181,7 @@ export const SearchBar = () => {
render={({ translation }) => (
<Box
as="input"
data-cy="search-bar-input"
placeholder={translation as string}
onChange={(event: ChangeEvent<HTMLInputElement>) => {
!isOpen && toggleOpen()

View File

@@ -1,30 +1,33 @@
import { Trans } from '@lingui/macro'
import { useTrace } from '@uniswap/analytics'
import { InterfaceSectionName, NavBarSearchTypes } from '@uniswap/analytics-events'
import { useWeb3React } from '@web3-react/core'
import { SafetyLevel } from 'graphql/data/__generated__/types-and-hooks'
import { SearchToken } from 'graphql/data/SearchTokens'
import useTrendingTokens from 'graphql/data/TrendingTokens'
import { useIsNftPage } from 'hooks/useIsNftPage'
import { Box } from 'nft/components/Box'
import { Column, Row } from 'nft/components/Flex'
import { subheadSmall } from 'nft/css/common.css'
import { useSearchHistory } from 'nft/hooks'
import { fetchTrendingCollections } from 'nft/queries'
import { fetchTrendingTokens } from 'nft/queries/genie/TrendingTokensFetcher'
import { FungibleToken, GenieCollection, TimePeriod, TrendingCollection } from 'nft/types'
import { GenieCollection, TimePeriod, TrendingCollection } from 'nft/types'
import { formatEthPrice } from 'nft/utils/currency'
import { ReactNode, useEffect, useMemo, useState } from 'react'
import { useQuery } from 'react-query'
import { useLocation } from 'react-router-dom'
import { ClockIcon, TrendingArrow } from '../../nft/components/icons'
import { useRecentlySearchedAssets } from './RecentlySearchedAssets'
import * as styles from './SearchBar.css'
import { CollectionRow, SkeletonRow, TokenRow } from './SuggestionRow'
function isCollection(suggestion: GenieCollection | FungibleToken | TrendingCollection) {
return (suggestion as FungibleToken).decimals === undefined
function isCollection(suggestion: GenieCollection | SearchToken | TrendingCollection) {
return (suggestion as SearchToken).decimals === undefined
}
interface SearchBarDropdownSectionProps {
toggleOpen: () => void
suggestions: (GenieCollection | FungibleToken)[]
suggestions: (GenieCollection | SearchToken)[]
header: JSX.Element
headerIcon?: JSX.Element
hoveredIndex: number | undefined
@@ -46,7 +49,7 @@ const SearchBarDropdownSection = ({
eventProperties,
}: SearchBarDropdownSectionProps) => {
return (
<Column gap="12">
<Column gap="12" data-cy="searchbar-dropdown">
<Row paddingX="16" paddingY="4" gap="8" color="gray300" className={subheadSmall} style={{ lineHeight: '20px' }}>
{headerIcon ? headerIcon : null}
<Box>{header}</Box>
@@ -73,7 +76,7 @@ const SearchBarDropdownSection = ({
) : (
<TokenRow
key={suggestion.address}
token={suggestion as FungibleToken}
token={suggestion as SearchToken}
isHovered={hoveredIndex === index + startingIndex}
setHoveredIndex={setHoveredIndex}
toggleOpen={toggleOpen}
@@ -92,9 +95,13 @@ const SearchBarDropdownSection = ({
)
}
function isKnownToken(token: SearchToken) {
return token.project?.safetyLevel == SafetyLevel.Verified || token.project?.safetyLevel == SafetyLevel.MediumWarning
}
interface SearchBarDropdownProps {
toggleOpen: () => void
tokens: FungibleToken[]
tokens: SearchToken[]
collections: GenieCollection[]
queryText: string
hasInput: boolean
@@ -110,8 +117,10 @@ export const SearchBarDropdown = ({
isLoading,
}: SearchBarDropdownProps) => {
const [hoveredIndex, setHoveredIndex] = useState<number | undefined>(0)
const { history: searchHistory, updateItem: updateSearchHistory } = useSearchHistory()
const shortenedHistory = useMemo(() => searchHistory.slice(0, 2), [searchHistory])
const { data: searchHistory } = useRecentlySearchedAssets()
const shortenedHistory = useMemo(() => searchHistory?.slice(0, 2) ?? [...Array<SearchToken>(2)], [searchHistory])
const { pathname } = useLocation()
const isNFTPage = useIsNftPage()
const isTokenPage = pathname.includes('/tokens')
@@ -141,26 +150,12 @@ export const SearchBarDropdown = ({
[isNFTPage, trendingCollectionResults]
)
const { data: trendingTokenResults, isLoading: trendingTokensAreLoading } = useQuery(
['trendingTokens'],
() => fetchTrendingTokens(4),
{
refetchOnWindowFocus: false,
refetchOnMount: false,
refetchOnReconnect: false,
}
)
useEffect(() => {
trendingTokenResults?.forEach(updateSearchHistory)
}, [trendingTokenResults, updateSearchHistory])
const { data: trendingTokenData } = useTrendingTokens(useWeb3React().chainId)
const trendingTokensLength = isTokenPage ? 3 : 2
const trendingTokens = useMemo(
() =>
trendingTokenResults
? trendingTokenResults.slice(0, trendingTokensLength)
: [...Array<FungibleToken>(trendingTokensLength)],
[trendingTokenResults, trendingTokensLength]
() => trendingTokenData?.slice(0, trendingTokensLength) ?? [...Array<SearchToken>(trendingTokensLength)],
[trendingTokenData, trendingTokensLength]
)
const totalSuggestions = hasInput
@@ -197,10 +192,9 @@ export const SearchBarDropdown = ({
}, [toggleOpen, hoveredIndex, totalSuggestions])
const hasVerifiedCollection = collections.some((collection) => collection.isVerified)
const hasVerifiedToken = tokens.some((token) => token.onDefaultList)
const hasKnownToken = tokens.some(isKnownToken)
const showCollectionsFirst =
(isNFTPage && (hasVerifiedCollection || !hasVerifiedToken)) ||
(!isNFTPage && !hasVerifiedToken && hasVerifiedCollection)
(isNFTPage && (hasVerifiedCollection || !hasKnownToken)) || (!isNFTPage && !hasKnownToken && hasVerifiedCollection)
const trace = JSON.stringify(useTrace({ section: InterfaceSectionName.NAVBAR_SEARCH }))
@@ -277,6 +271,7 @@ export const SearchBarDropdown = ({
}}
header={<Trans>Recent searches</Trans>}
headerIcon={<ClockIcon />}
isLoading={!searchHistory}
/>
)}
{!isNFTPage && (
@@ -292,7 +287,7 @@ export const SearchBarDropdown = ({
}}
header={<Trans>Popular tokens</Trans>}
headerIcon={<TrendingArrow />}
isLoading={trendingTokensAreLoading}
isLoading={!trendingTokenData}
/>
)}
{!isTokenPage && (
@@ -323,7 +318,7 @@ export const SearchBarDropdown = ({
trendingCollections,
trendingCollectionsAreLoading,
trendingTokens,
trendingTokensAreLoading,
trendingTokenData,
hoveredIndex,
toggleOpen,
shortenedHistory,
@@ -334,6 +329,7 @@ export const SearchBarDropdown = ({
queryText,
totalSuggestions,
trace,
searchHistory,
])
return (

View File

@@ -1,21 +1,18 @@
import { sendAnalyticsEvent } from '@uniswap/analytics'
import { InterfaceEventName } from '@uniswap/analytics-events'
import { formatUSDPrice } from '@uniswap/conedison/format'
import { useWeb3React } from '@web3-react/core'
import clsx from 'clsx'
import AssetLogo from 'components/Logo/AssetLogo'
import { L2NetworkLogo, LogoContainer } from 'components/Tokens/TokenTable/TokenRow'
import QueryTokenLogo from 'components/Logo/QueryTokenLogo'
import TokenSafetyIcon from 'components/TokenSafety/TokenSafetyIcon'
import { getChainInfo } from 'constants/chainInfo'
import { NATIVE_CHAIN_ID } from 'constants/tokens'
import { checkWarning } from 'constants/tokenSafety'
import { checkSearchTokenWarning } from 'constants/tokenSafety'
import { Chain, TokenStandard } from 'graphql/data/__generated__/types-and-hooks'
import { SearchToken } from 'graphql/data/SearchTokens'
import { getTokenDetailsURL } from 'graphql/data/util'
import { Box } from 'nft/components/Box'
import { Column, Row } from 'nft/components/Flex'
import { VerifiedIcon } from 'nft/components/icons'
import { vars } from 'nft/css/sprinkles.css'
import { useSearchHistory } from 'nft/hooks'
import { FungibleToken, GenieCollection } from 'nft/types'
import { GenieCollection } from 'nft/types'
import { ethNumberStandardFormatter } from 'nft/utils/currency'
import { putCommas } from 'nft/utils/putCommas'
import { useCallback, useEffect, useState } from 'react'
@@ -23,11 +20,9 @@ import { Link, useNavigate } from 'react-router-dom'
import styled from 'styled-components/macro'
import { getDeltaArrow } from '../Tokens/TokenDetails/PriceChart'
import { useAddRecentlySearchedAsset } from './RecentlySearchedAssets'
import * as styles from './SearchBar.css'
const StyledLogoContainer = styled(LogoContainer)`
margin-right: 8px;
`
const PriceChangeContainer = styled.div`
display: flex;
align-items: center;
@@ -63,16 +58,15 @@ export const CollectionRow = ({
}: CollectionRowProps) => {
const [brokenImage, setBrokenImage] = useState(false)
const [loaded, setLoaded] = useState(false)
const addToSearchHistory = useSearchHistory(
(state: { addItem: (item: FungibleToken | GenieCollection) => void }) => state.addItem
)
const addRecentlySearchedAsset = useAddRecentlySearchedAsset()
const navigate = useNavigate()
const handleClick = useCallback(() => {
addToSearchHistory(collection)
addRecentlySearchedAsset({ ...collection, isNft: true, chain: Chain.Ethereum })
toggleOpen()
sendAnalyticsEvent(InterfaceEventName.NAVBAR_RESULT_SELECTED, { ...eventProperties })
}, [addToSearchHistory, collection, toggleOpen, eventProperties])
}, [addRecentlySearchedAsset, collection, toggleOpen, eventProperties])
useEffect(() => {
const keyDownHandler = (event: KeyboardEvent) => {
@@ -130,17 +124,8 @@ export const CollectionRow = ({
)
}
function useBridgedAddress(token: FungibleToken): [string | undefined, number | undefined, string | undefined] {
const { chainId: connectedChainId } = useWeb3React()
const bridgedAddress = connectedChainId ? token.extensions?.bridgeInfo?.[connectedChainId]?.tokenAddress : undefined
if (bridgedAddress && connectedChainId) {
return [bridgedAddress, connectedChainId, getChainInfo(connectedChainId)?.circleLogoUrl]
}
return [undefined, undefined, undefined]
}
interface TokenRowProps {
token: FungibleToken
token: SearchToken
isHovered: boolean
setHoveredIndex: (index: number | undefined) => void
toggleOpen: () => void
@@ -149,19 +134,18 @@ interface TokenRowProps {
}
export const TokenRow = ({ token, isHovered, setHoveredIndex, toggleOpen, index, eventProperties }: TokenRowProps) => {
const addToSearchHistory = useSearchHistory(
(state: { addItem: (item: FungibleToken | GenieCollection) => void }) => state.addItem
)
const addRecentlySearchedAsset = useAddRecentlySearchedAsset()
const navigate = useNavigate()
const handleClick = useCallback(() => {
addToSearchHistory(token)
const address = !token.address && token.standard === TokenStandard.Native ? 'NATIVE' : token.address
address && addRecentlySearchedAsset({ address, chain: token.chain })
toggleOpen()
sendAnalyticsEvent(InterfaceEventName.NAVBAR_RESULT_SELECTED, { ...eventProperties })
}, [addToSearchHistory, toggleOpen, token, eventProperties])
}, [addRecentlySearchedAsset, token, toggleOpen, eventProperties])
const [bridgedAddress, bridgedChain, L2Icon] = useBridgedAddress(token)
const tokenDetailsPath = getTokenDetailsURL(bridgedAddress ?? token.address, undefined, bridgedChain ?? token.chainId)
const tokenDetailsPath = getTokenDetailsURL(token)
// Close the modal on escape
useEffect(() => {
const keyDownHandler = (event: KeyboardEvent) => {
@@ -177,10 +161,11 @@ export const TokenRow = ({ token, isHovered, setHoveredIndex, toggleOpen, index,
}
}, [toggleOpen, isHovered, token, navigate, handleClick, tokenDetailsPath])
const arrow = getDeltaArrow(token.price24hChange, 18)
const arrow = getDeltaArrow(token.market?.pricePercentChange?.value, 18)
return (
<Link
data-cy={`searchbar-token-row-${token.symbol}`}
to={tokenDetailsPath}
onClick={handleClick}
onMouseEnter={() => !isHovered && setHoveredIndex(index)}
@@ -189,37 +174,33 @@ export const TokenRow = ({ token, isHovered, setHoveredIndex, toggleOpen, index,
style={{ background: isHovered ? vars.color.lightGrayOverlay : 'none' }}
>
<Row style={{ width: '65%' }}>
<StyledLogoContainer>
<AssetLogo
isNative={token.address === NATIVE_CHAIN_ID}
address={token.address}
chainId={token.chainId}
symbol={token.symbol}
size="36px"
backupImg={token.logoURI}
/>
<L2NetworkLogo networkUrl={L2Icon} size="16px" />
</StyledLogoContainer>
<QueryTokenLogo
token={token}
symbol={token.symbol}
size="36px"
backupImg={token.project?.logoUrl}
style={{ paddingRight: '8px' }}
/>
<Column className={styles.suggestionPrimaryContainer}>
<Row gap="4" width="full">
<Box className={styles.primaryText}>{token.name}</Box>
<TokenSafetyIcon warning={checkWarning(token.address)} />
<TokenSafetyIcon warning={checkSearchTokenWarning(token)} />
</Row>
<Box className={styles.secondaryText}>{token.symbol}</Box>
</Column>
</Row>
<Column className={styles.suggestionSecondaryContainer}>
{token.priceUsd && (
{token.market?.price?.value && (
<Row gap="4">
<Box className={styles.primaryText}>{formatUSDPrice(token.priceUsd)}</Box>
<Box className={styles.primaryText}>{formatUSDPrice(token.market.price.value)}</Box>
</Row>
)}
{token.price24hChange && (
{token.market?.pricePercentChange?.value && (
<PriceChangeContainer>
<ArrowCell>{arrow}</ArrowCell>
<PriceChangeText isNegative={token.price24hChange < 0}>
{Math.abs(token.price24hChange).toFixed(2)}%
<PriceChangeText isNegative={token.market.pricePercentChange.value < 0}>
{Math.abs(token.market.pricePercentChange.value).toFixed(2)}%
</PriceChangeText>
</PriceChangeContainer>
)}

View File

@@ -1,11 +1,14 @@
import { Trans } from '@lingui/macro'
import { useWeb3React } from '@web3-react/core'
import Web3Status from 'components/Web3Status'
import { NftListV2Variant, useNftListV2Flag } from 'featureFlags/flags/nftListV2'
import { chainIdToBackendName } from 'graphql/data/util'
import { useIsNftPage } from 'hooks/useIsNftPage'
import { Box } from 'nft/components/Box'
import { Row } from 'nft/components/Flex'
import { UniIcon } from 'nft/components/icons'
import { useProfilePageState } from 'nft/hooks'
import { ProfilePageStateType } from 'nft/types'
import { ReactNode } from 'react'
import { NavLink, NavLinkProps, useLocation, useNavigate } from 'react-router-dom'
import styled from 'styled-components/macro'
@@ -54,8 +57,7 @@ export const PageTabs = () => {
pathname.startsWith('/pool') ||
pathname.startsWith('/add') ||
pathname.startsWith('/remove') ||
pathname.startsWith('/increase') ||
pathname.startsWith('/find')
pathname.startsWith('/increase')
const isNftPage = useIsNftPage()
@@ -79,6 +81,8 @@ export const PageTabs = () => {
const Navbar = () => {
const isNftPage = useIsNftPage()
const sellPageState = useProfilePageState((state) => state.state)
const isNftListV2 = useNftListV2Flag() === NftListV2Variant.Enabled
const navigate = useNavigate()
return (
@@ -120,7 +124,7 @@ const Navbar = () => {
<Box display={{ sm: 'none', lg: 'flex' }}>
<MenuDropdown />
</Box>
{isNftPage && <Bag />}
{isNftPage && (!isNftListV2 || sellPageState !== ProfilePageStateType.LISTING) && <Bag />}
{!isNftPage && (
<Box display={{ sm: 'none', lg: 'flex' }}>
<ChainSelector />

View File

@@ -3,11 +3,9 @@ import { useWeb3React } from '@web3-react/core'
import { RowFixed } from 'components/Row'
import { getChainInfo } from 'constants/chainInfo'
import useCurrentBlockTimestamp from 'hooks/useCurrentBlockTimestamp'
import useGasPrice from 'hooks/useGasPrice'
import { useIsLandingPage } from 'hooks/useIsLandingPage'
import { useIsNftPage } from 'hooks/useIsNftPage'
import useMachineTimeMs from 'hooks/useMachineTime'
import JSBI from 'jsbi'
import useBlockNumber from 'lib/hooks/useBlockNumber'
import ms from 'ms.macro'
import { useEffect, useMemo, useState } from 'react'
@@ -71,17 +69,6 @@ const StyledPollingDot = styled.div<{ warning: boolean }>`
transition: 250ms ease background-color;
`
const StyledGasDot = styled.div`
background-color: ${({ theme }) => theme.textTertiary};
border-radius: 50%;
height: 4px;
min-height: 4px;
min-width: 4px;
position: relative;
transition: 250ms ease background-color;
width: 4px;
`
const rotate360 = keyframes`
from {
transform: rotate(0deg);
@@ -123,9 +110,6 @@ export default function Polling() {
const isNftPage = useIsNftPage()
const isLandingPage = useIsLandingPage()
const ethGasPrice = useGasPrice()
const priceGwei = ethGasPrice ? JSBI.divide(ethGasPrice, JSBI.BigInt(1000000000)) : undefined
const waitMsBeforeWarning =
(chainId ? getChainInfo(chainId)?.blockWaitMsBeforeWarning : DEFAULT_MS_BEFORE_WARNING) ?? DEFAULT_MS_BEFORE_WARNING
@@ -163,25 +147,6 @@ export default function Polling() {
return (
<RowFixed>
<StyledPolling onMouseEnter={() => setIsHover(true)} onMouseLeave={() => setIsHover(false)}>
<ExternalLink href="https://etherscan.io/gastracker">
{!!priceGwei && (
<RowFixed style={{ marginRight: '8px' }}>
<ThemedText.DeprecatedMain fontSize="11px" mr="8px">
<MouseoverTooltip
text={
<Trans>
The current fast gas amount for sending a transaction on L1. Gas fees are paid in Ethereum&apos;s
native currency Ether (ETH) and denominated in GWEI.
</Trans>
}
>
{priceGwei.toString()} <Trans>gwei</Trans>
</MouseoverTooltip>
</ThemedText.DeprecatedMain>
<StyledGasDot />
</RowFixed>
)}
</ExternalLink>
<StyledPollingBlockNumber breathe={isMounting} hovering={isHover} warning={warning}>
<ExternalLink href={blockExternalLinkHref}>
<MouseoverTooltip

View File

@@ -99,9 +99,9 @@ export default function PositionList({
</ToggleLabel>
</ToggleWrap>
</MobileHeader>
{positions.map((p) => {
return <PositionListItem key={p.tokenId.toString()} positionDetails={p} />
})}
{positions.map((p) => (
<PositionListItem key={p.tokenId.toString()} {...p} />
))}
</>
)
}

View File

@@ -0,0 +1,38 @@
import { BigNumber } from '@ethersproject/bignumber'
import { render, screen } from 'test-utils'
import PositionListItem from '.'
jest.mock('hooks/Tokens', () => {
const originalModule = jest.requireActual('hooks/Tokens')
const uniSDK = jest.requireActual('@uniswap/sdk-core')
return {
__esModule: true,
...originalModule,
useToken: jest.fn(
() =>
new uniSDK.Token(
1,
'0x39AA39c021dfbaE8faC545936693aC917d5E7563',
8,
'https://www.example.com',
'example.com coin'
)
),
}
})
test('PositionListItem should not render when the name contains a url', () => {
const positionDetails = {
token0: '0x39AA39c021dfbaE8faC545936693aC917d5E7563',
token1: '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2',
tokenId: BigNumber.from(436148),
fee: 100,
liquidity: BigNumber.from('0x5c985aff8059be04'),
tickLower: -800,
tickUpper: 1600,
}
render(<PositionListItem {...positionDetails} />)
screen.debug()
expect(screen.queryByText('.com', { exact: false })).toBe(null)
})

View File

@@ -1,3 +1,4 @@
import { BigNumber } from '@ethersproject/bignumber'
import { Trans } from '@lingui/macro'
import { Percent, Price, Token } from '@uniswap/sdk-core'
import { Position } from '@uniswap/v3-sdk'
@@ -15,9 +16,9 @@ import { Link } from 'react-router-dom'
import { Bound } from 'state/mint/v3/actions'
import styled from 'styled-components/macro'
import { HideSmall, MEDIA_WIDTHS, SmallOnly } from 'theme'
import { PositionDetails } from 'types/position'
import { formatTickPrice } from 'utils/formatTickPrice'
import { unwrappedToken } from 'utils/unwrappedToken'
import { hasURL } from 'utils/urlChecks'
import { DAI, USDC_MAINNET, USDT, WBTC, WRAPPED_NATIVE_CURRENCY } from '../../constants/tokens'
@@ -109,7 +110,13 @@ const DataText = styled.div`
`
interface PositionListItemProps {
positionDetails: PositionDetails
token0: string
token1: string
tokenId: BigNumber
fee: number
liquidity: BigNumber
tickLower: number
tickUpper: number
}
export function getPriceOrderingFromPositionForUI(position?: Position): {
@@ -166,16 +173,15 @@ export function getPriceOrderingFromPositionForUI(position?: Position): {
}
}
export default function PositionListItem({ positionDetails }: PositionListItemProps) {
const {
token0: token0Address,
token1: token1Address,
fee: feeAmount,
liquidity,
tickLower,
tickUpper,
} = positionDetails
export default function PositionListItem({
token0: token0Address,
token1: token1Address,
tokenId,
fee: feeAmount,
liquidity,
tickLower,
tickUpper,
}: PositionListItemProps) {
const token0 = useToken(token0Address)
const token1 = useToken(token1Address)
@@ -203,10 +209,23 @@ export default function PositionListItem({ positionDetails }: PositionListItemPr
// check if price is within range
const outOfRange: boolean = pool ? pool.tickCurrent < tickLower || pool.tickCurrent >= tickUpper : false
const positionSummaryLink = '/pool/' + positionDetails.tokenId
const positionSummaryLink = '/pool/' + tokenId
const removed = liquidity?.eq(0)
const containsURL = useMemo(
() =>
[token0?.name, token0?.symbol, token1?.name, token1?.symbol].reduce(
(acc, testString) => acc || Boolean(testString && hasURL(testString)),
false
),
[token0?.name, token0?.symbol, token1?.name, token1?.symbol]
)
if (containsURL) {
return null
}
return (
<LinkRow to={positionSummaryLink}>
<RowBetween>

View File

@@ -105,235 +105,240 @@ exports[`renders currency rows correctly when currencies list is non-empty 1`] =
}
<div
style="position: relative; height: 10px; width: 100%; overflow: auto; will-change: transform; direction: ltr;"
style="padding-right: 4px;"
>
<div
style="height: 168px; width: 100%;"
class="CurrencyList_scrollbarStyle__1pi21y70"
style="position: relative; height: 10px; width: 100%; overflow: auto; will-change: transform; direction: ltr;"
>
<div
class="c0 c1 c2 c3 token-item-0x6B175474E89094C44Da98b954EedeAC495271d0F"
style="position: absolute; left: 0px; top: 0px; height: 56px; width: 100%;"
tabindex="0"
style="height: 168px; width: 100%;"
>
<div
class="c4"
>
CurrencyLogo currency=DAI
</div>
<div
class="c5"
style="opacity: 1;"
class="c0 c1 c2 c3 token-item-0x6B175474E89094C44Da98b954EedeAC495271d0F"
style="position: absolute; left: 0px; top: 0px; height: 56px; width: 100%;"
tabindex="0"
>
<div
class="c0 c1"
class="c4"
>
CurrencyLogo currency=DAI
</div>
<div
class="c5"
style="opacity: 1;"
>
<div
class="c6 css-vurnku"
title="Dai Stablecoin"
>
Dai Stablecoin
</div>
<div
class="c7"
class="c0 c1"
>
<div
class="c8"
class="c6 css-vurnku"
title="Dai Stablecoin"
>
<svg
class="c9"
fill="none"
height="24"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
Dai Stablecoin
</div>
<div
class="c7"
>
<div
class="c8"
>
<path
d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"
/>
<line
x1="12"
x2="12"
y1="9"
y2="13"
/>
<line
x1="12"
x2="12.01"
y1="17"
y2="17"
/>
</svg>
<svg
class="c9"
fill="none"
height="24"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"
/>
<line
x1="12"
x2="12"
y1="9"
y2="13"
/>
<line
x1="12"
x2="12.01"
y1="17"
y2="17"
/>
</svg>
</div>
</div>
</div>
</div>
<div
class="c10 css-yfjwjl"
>
DAI
</div>
</div>
<div
class="c4"
>
<div
class="c0 c1 c11"
style="justify-self: flex-end;"
/>
</div>
</div>
<div
class="c0 c1 c2 c3 token-item-0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"
style="position: absolute; left: 0px; top: 56px; height: 56px; width: 100%;"
tabindex="0"
>
<div
class="c4"
>
CurrencyLogo currency=USDC
</div>
<div
class="c5"
style="opacity: 1;"
>
<div
class="c0 c1"
>
<div
class="c6 css-vurnku"
title="USD//C"
class="c10 css-yfjwjl"
>
USD//C
DAI
</div>
</div>
<div
class="c4"
>
<div
class="c7"
class="c0 c1 c11"
style="justify-self: flex-end;"
/>
</div>
</div>
<div
class="c0 c1 c2 c3 token-item-0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"
style="position: absolute; left: 0px; top: 56px; height: 56px; width: 100%;"
tabindex="0"
>
<div
class="c4"
>
CurrencyLogo currency=USDC
</div>
<div
class="c5"
style="opacity: 1;"
>
<div
class="c0 c1"
>
<div
class="c8"
class="c6 css-vurnku"
title="USD//C"
>
<svg
class="c9"
fill="none"
height="24"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
USD//C
</div>
<div
class="c7"
>
<div
class="c8"
>
<path
d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"
/>
<line
x1="12"
x2="12"
y1="9"
y2="13"
/>
<line
x1="12"
x2="12.01"
y1="17"
y2="17"
/>
</svg>
<svg
class="c9"
fill="none"
height="24"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"
/>
<line
x1="12"
x2="12"
y1="9"
y2="13"
/>
<line
x1="12"
x2="12.01"
y1="17"
y2="17"
/>
</svg>
</div>
</div>
</div>
</div>
<div
class="c10 css-yfjwjl"
>
USDC
</div>
</div>
<div
class="c4"
>
<div
class="c0 c1 c11"
style="justify-self: flex-end;"
/>
</div>
</div>
<div
class="c0 c1 c2 c3 token-item-0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599"
style="position: absolute; left: 0px; top: 112px; height: 56px; width: 100%;"
tabindex="0"
>
<div
class="c4"
>
CurrencyLogo currency=WBTC
</div>
<div
class="c5"
style="opacity: 1;"
>
<div
class="c0 c1"
>
<div
class="c6 css-vurnku"
title="Wrapped BTC"
class="c10 css-yfjwjl"
>
Wrapped BTC
USDC
</div>
</div>
<div
class="c4"
>
<div
class="c7"
class="c0 c1 c11"
style="justify-self: flex-end;"
/>
</div>
</div>
<div
class="c0 c1 c2 c3 token-item-0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599"
style="position: absolute; left: 0px; top: 112px; height: 56px; width: 100%;"
tabindex="0"
>
<div
class="c4"
>
CurrencyLogo currency=WBTC
</div>
<div
class="c5"
style="opacity: 1;"
>
<div
class="c0 c1"
>
<div
class="c8"
class="c6 css-vurnku"
title="Wrapped BTC"
>
<svg
class="c9"
fill="none"
height="24"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"
/>
<line
x1="12"
x2="12"
y1="9"
y2="13"
/>
<line
x1="12"
x2="12.01"
y1="17"
y2="17"
/>
</svg>
Wrapped BTC
</div>
<div
class="c7"
>
<div
class="c8"
>
<svg
class="c9"
fill="none"
height="24"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"
/>
<line
x1="12"
x2="12"
y1="9"
y2="13"
/>
<line
x1="12"
x2="12.01"
y1="17"
y2="17"
/>
</svg>
</div>
</div>
</div>
<div
class="c10 css-yfjwjl"
>
WBTC
</div>
</div>
<div
class="c10 css-yfjwjl"
class="c4"
>
WBTC
<div
class="c0 c1 c11"
style="justify-self: flex-end;"
/>
</div>
</div>
<div
class="c4"
>
<div
class="c0 c1 c11"
style="justify-self: flex-end;"
/>
</div>
</div>
</div>
</div>
@@ -383,31 +388,36 @@ exports[`renders loading rows when isLoading is true 1`] = `
}
<div
style="position: relative; height: 10px; width: 100%; overflow: auto; will-change: transform; direction: ltr;"
style="padding-right: 4px;"
>
<div
style="height: 560px; width: 100%;"
class="CurrencyList_scrollbarStyle__1pi21y70"
style="position: relative; height: 10px; width: 100%; overflow: auto; will-change: transform; direction: ltr;"
>
<div
class="c0 c1"
style="height: 560px; width: 100%;"
>
<div />
<div />
<div />
</div>
<div
class="c0 c1"
>
<div />
<div />
<div />
</div>
<div
class="c0 c1"
>
<div />
<div />
<div />
<div
class="c0 c1"
>
<div />
<div />
<div />
</div>
<div
class="c0 c1"
>
<div />
<div />
<div />
</div>
<div
class="c0 c1"
>
<div />
<div />
<div />
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,20 @@
import { style } from '@vanilla-extract/css'
import { themeVars } from 'nft/css/sprinkles.css'
export const scrollbarStyle = style([
{
scrollbarWidth: 'thin',
scrollbarColor: `${themeVars.colors.backgroundOutline} transparent`,
height: '100%',
selectors: {
'&::-webkit-scrollbar': {
background: 'transparent',
width: '4px',
},
'&::-webkit-scrollbar-thumb': {
background: `${themeVars.colors.backgroundOutline}`,
borderRadius: '8px',
},
},
},
])

View File

@@ -20,6 +20,7 @@ import CurrencyLogo from '../../Logo/CurrencyLogo'
import Row, { RowFixed } from '../../Row'
import { MouseoverTooltip } from '../../Tooltip'
import { LoadingRows, MenuItem } from '../styleds'
import * as styles from './index.css'
function currencyKey(currency: Currency): string {
return currency.isToken ? currency.address : 'ETHER'
@@ -288,21 +289,34 @@ export default function CurrencyList({
return currencyKey(currency)
}, [])
return isLoading ? (
<FixedSizeList height={height} ref={fixedListRef as any} width="100%" itemData={[]} itemCount={10} itemSize={56}>
{LoadingRow}
</FixedSizeList>
) : (
<FixedSizeList
height={height}
ref={fixedListRef as any}
width="100%"
itemData={itemData}
itemCount={itemData.length}
itemSize={56}
itemKey={itemKey}
>
{Row}
</FixedSizeList>
return (
<div style={{ paddingRight: '4px' }}>
{isLoading ? (
<FixedSizeList
className={styles.scrollbarStyle}
height={height}
ref={fixedListRef as any}
width="100%"
itemData={[]}
itemCount={10}
itemSize={56}
>
{LoadingRow}
</FixedSizeList>
) : (
<FixedSizeList
className={styles.scrollbarStyle}
height={height}
ref={fixedListRef as any}
width="100%"
itemData={itemData}
itemCount={itemData.length}
itemSize={56}
itemKey={itemKey}
>
{Row}
</FixedSizeList>
)}
</div>
)
}

View File

@@ -32,8 +32,10 @@ import { PaddedColumn, SearchInput, Separator } from './styleds'
const ContentWrapper = styled(Column)`
background-color: ${({ theme }) => theme.backgroundSurface};
width: 100%;
overflow: hidden;
flex: 1 1;
position: relative;
border-radius: 20px;
`
interface CurrencySearchProps {
@@ -45,6 +47,7 @@ interface CurrencySearchProps {
showCommonBases?: boolean
showCurrencyAmount?: boolean
disableNonToken?: boolean
onlyShowCurrenciesWithBalance?: boolean
}
export function CurrencySearch({
@@ -56,6 +59,7 @@ export function CurrencySearch({
disableNonToken,
onDismiss,
isOpen,
onlyShowCurrenciesWithBalance,
}: CurrencySearchProps) {
const { chainId } = useWeb3React()
const theme = useTheme()
@@ -92,6 +96,10 @@ export function CurrencySearch({
!balancesAreLoading
? filteredTokens
.filter((token) => {
if (onlyShowCurrenciesWithBalance) {
return balances[token.address]?.greaterThan(0)
}
// If there is no query, filter out unselected user-added tokens with no balance.
if (!debouncedQuery && token instanceof UserAddedToken) {
if (selectedCurrency?.equals(token) || otherSelectedCurrency?.equals(token)) return true
@@ -101,7 +109,15 @@ export function CurrencySearch({
})
.sort(tokenComparator.bind(null, balances))
: [],
[balances, balancesAreLoading, debouncedQuery, filteredTokens, otherSelectedCurrency, selectedCurrency]
[
balances,
balancesAreLoading,
debouncedQuery,
filteredTokens,
otherSelectedCurrency,
selectedCurrency,
onlyShowCurrenciesWithBalance,
]
)
const isLoading = Boolean(balancesAreLoading && !tokenLoaderTimerElapsed)
@@ -114,11 +130,23 @@ export function CurrencySearch({
const s = debouncedQuery.toLowerCase().trim()
const tokens = filteredSortedTokens.filter((t) => !(t.equals(wrapped) || (disableNonToken && t.isNative)))
const natives = (disableNonToken || native.equals(wrapped) ? [wrapped] : [native, wrapped]).filter(
(n) => n.symbol?.toLowerCase()?.indexOf(s) !== -1 || n.name?.toLowerCase()?.indexOf(s) !== -1
)
const shouldShowWrapped =
!onlyShowCurrenciesWithBalance || (!balancesAreLoading && balances[wrapped.address]?.greaterThan(0))
const natives = (
disableNonToken || native.equals(wrapped) ? [wrapped] : shouldShowWrapped ? [native, wrapped] : [native]
).filter((n) => n.symbol?.toLowerCase()?.indexOf(s) !== -1 || n.name?.toLowerCase()?.indexOf(s) !== -1)
return [...natives, ...tokens]
}, [debouncedQuery, filteredSortedTokens, wrapped, disableNonToken, native])
}, [
debouncedQuery,
filteredSortedTokens,
onlyShowCurrenciesWithBalance,
balancesAreLoading,
balances,
wrapped,
disableNonToken,
native,
])
const handleCurrencySelect = useCallback(
(currency: Currency, hasWarning?: boolean) => {
@@ -168,7 +196,9 @@ export function CurrencySearch({
// if no results on main list, show option to expand into inactive
const filteredInactiveTokens = useSearchInactiveTokenLists(
filteredTokens.length === 0 || (debouncedQuery.length > 2 && !isAddressSearch) ? debouncedQuery : undefined
!onlyShowCurrenciesWithBalance && (filteredTokens.length === 0 || (debouncedQuery.length > 2 && !isAddressSearch))
? debouncedQuery
: undefined
)
// Timeout token loader after 3 seconds to avoid hanging in a loading state.

View File

@@ -17,6 +17,7 @@ interface CurrencySearchModalProps {
showCommonBases?: boolean
showCurrencyAmount?: boolean
disableNonToken?: boolean
onlyShowCurrenciesWithBalance?: boolean
}
enum CurrencyModalView {
@@ -34,6 +35,7 @@ export default memo(function CurrencySearchModal({
showCommonBases = false,
showCurrencyAmount = true,
disableNonToken = false,
onlyShowCurrenciesWithBalance = false,
}: CurrencySearchModalProps) {
const [modalView, setModalView] = useState<CurrencyModalView>(CurrencyModalView.search)
const lastOpen = useLast(isOpen)
@@ -84,6 +86,7 @@ export default memo(function CurrencySearchModal({
showCommonBases={showCommonBases}
showCurrencyAmount={showCurrencyAmount}
disableNonToken={disableNonToken}
onlyShowCurrenciesWithBalance={onlyShowCurrenciesWithBalance}
/>
)
break

View File

@@ -28,7 +28,7 @@ export default function TokenSafetyIcon({ warning }: { warning: Warning | null }
case WARNING_LEVEL.BLOCKED:
return (
<WarningContainer>
<BlockedIcon strokeWidth={2.5} />
<BlockedIcon data-cy="blocked-icon" strokeWidth={2.5} />
</WarningContainer>
)
case WARNING_LEVEL.UNKNOWN:

View File

@@ -50,7 +50,7 @@ export default function TokenSafetyMessage({ warning, tokenAddress }: TokenSafet
const { heading, description } = getWarningCopy(warning)
return (
<Label color={textColor} backgroundColor={backgroundColor}>
<Label data-cy="token-safety-message" color={textColor} backgroundColor={backgroundColor}>
<TitleRow>
{warning.canProceed ? <AlertTriangle size="16px" /> : <Slash size="16px" />}
<Title marginLeft="7px">{warning.message}</Title>

View File

@@ -103,9 +103,8 @@ export function AboutSection({ address, chainId, description, homepageUrl, twitt
<ThemedText.SubHeaderSmall>
<Trans>Links</Trans>
</ThemedText.SubHeaderSmall>
<ResourcesContainer>
<ResourcesContainer data-cy="resources-container">
<Resource
data-testid="token-details-about-section-explorer-link"
name={chainId === SupportedChainId.MAINNET ? 'Etherscan' : 'Block Explorer'}
link={`${baseExplorerUrl}${address === 'NATIVE' ? '' : 'address/' + address}`}
/>

View File

@@ -81,7 +81,7 @@ export default function BalanceSummary({ token }: { token: Currency }) {
<Trans>Your balance on {label}</Trans>
</ThemedText.SubHeaderSmall>
<BalanceRow>
<CurrencyLogo currency={token} size="2rem" />
<CurrencyLogo currency={token} size="2rem" hideL2Icon={false} />
<BalanceContainer>
<BalanceAmountsContainer>
<BalanceItem>

View File

@@ -13,7 +13,7 @@ import TimePeriodSelector from './TimeSelector'
function usePriceHistory(tokenPriceData: TokenPriceQuery): PricePoint[] | undefined {
// Appends the current price to the end of the priceHistory array
const priceHistory = useMemo(() => {
const market = tokenPriceData.tokens?.[0]?.market
const market = tokenPriceData.token?.market
const priceHistory = market?.priceHistory?.filter(isPricePoint)
const currentPrice = market?.price?.value
if (Array.isArray(priceHistory) && currentPrice !== undefined) {
@@ -58,7 +58,7 @@ function Chart({
const timePeriod = useAtomValue(pageTimePeriodAtom)
return (
<ChartContainer>
<ChartContainer data-testid="chart-container">
<ParentSize>
{({ width }) => <PriceChart prices={prices ?? null} width={width} height={436} timePeriod={timePeriod} />}
</ParentSize>

View File

@@ -3,9 +3,11 @@ import { formatCurrencyAmount, NumberType } from '@uniswap/conedison/format'
import { Currency } from '@uniswap/sdk-core'
import { useWeb3React } from '@web3-react/core'
import { NATIVE_CHAIN_ID } from 'constants/tokens'
import { FeatureGate } from 'featureFlags/flags/featureFlags'
import { CHAIN_ID_TO_BACKEND_NAME } from 'graphql/data/util'
import { useStablecoinValue } from 'hooks/useStablecoinPrice'
import useCurrencyBalance from 'lib/hooks/useCurrencyBalance'
import { useGate } from 'statsig-react'
import styled from 'styled-components/macro'
import { StyledInternalLink } from 'theme'
@@ -13,9 +15,10 @@ const Wrapper = styled.div`
align-content: center;
align-items: center;
border: 1px solid ${({ theme }) => theme.backgroundOutline};
border-bottom: none;
background-color: ${({ theme }) => theme.backgroundSurface};
border-radius: 20px 20px 0px 0px;
bottom: 56px;
bottom: 52px;
color: ${({ theme }) => theme.textSecondary};
display: flex;
flex-direction: row;
@@ -86,6 +89,7 @@ export default function MobileBalanceSummaryFooter({ token }: { token: Currency
const formattedBalance = formatCurrencyAmount(balance, NumberType.TokenNonTx)
const formattedUsdValue = formatCurrencyAmount(useStablecoinValue(balance), NumberType.FiatTokenStats)
const chain = CHAIN_ID_TO_BACKEND_NAME[token.chainId].toLowerCase()
const { value: isDummyGateFlagEnabled } = useGate(FeatureGate.DUMMY)
return (
<Wrapper>
@@ -101,7 +105,7 @@ export default function MobileBalanceSummaryFooter({ token }: { token: Currency
</BalanceInfo>
)}
<SwapButton to={`/swap?chainName=${chain}&outputCurrency=${token.isNative ? NATIVE_CHAIN_ID : token.address}`}>
<Trans>Swap</Trans>
<Trans>{isDummyGateFlagEnabled ? 'Go to Swap' : 'Swap'}</Trans>
</SwapButton>
</Wrapper>
)

View File

@@ -275,7 +275,7 @@ export function PriceChart({ width, height, prices: originalPrices, timePeriod }
return (
<>
<ChartHeader>
<ChartHeader data-cy="chart-header">
{displayPrice.value ? (
<>
<TokenPrice>{formatDollar({ num: displayPrice.value, isPrice: true })}</TokenPrice>
@@ -294,7 +294,7 @@ export function PriceChart({ width, height, prices: originalPrices, timePeriod }
{!chartAvailable ? (
<MissingPriceChart width={width} height={graphHeight} message={!!displayPrice.value && missingPricesMessage} />
) : (
<svg width={width} height={graphHeight} style={{ minWidth: '100%' }}>
<svg data-cy="price-chart" width={width} height={graphHeight} style={{ minWidth: '100%' }}>
<AnimatedInLineChart
data={prices}
getX={getX}
@@ -411,7 +411,7 @@ function MissingPriceChart({ width, height, message }: { width: number; height:
const theme = useTheme()
const midPoint = height / 2 + 45
return (
<StyledMissingChart width={width} height={height} style={{ minWidth: '100%' }}>
<StyledMissingChart data-cy="missing-chart" width={width} height={height} style={{ minWidth: '100%' }}>
<path
d={`M 0 ${midPoint} Q 104 ${midPoint - 70}, 208 ${midPoint} T 416 ${midPoint}
M 416 ${midPoint} Q 520 ${midPoint - 70}, 624 ${midPoint} T 832 ${midPoint}`}

View File

@@ -1,12 +1,11 @@
import { WidgetSkeleton } from 'components/Widget'
import { WIDGET_WIDTH } from 'components/Widget'
import { DEFAULT_WIDGET_WIDTH } from 'components/Widget'
import { ArrowLeft } from 'react-feather'
import { useParams } from 'react-router-dom'
import styled, { useTheme } from 'styled-components/macro'
import { textFadeIn } from 'theme/styles'
import { LoadingBubble } from '../loading'
import { LogoContainer } from '../TokenTable/TokenRow'
import { AboutContainer, AboutHeader } from './About'
import { BreadcrumbNavLink } from './BreadcrumbNavLink'
import { TokenPrice } from './PriceChart'
@@ -44,7 +43,7 @@ export const RightPanel = styled.div`
display: none;
flex-direction: column;
gap: 20px;
width: ${WIDGET_WIDTH}px;
width: ${DEFAULT_WIDGET_WIDTH}px;
@media screen and (min-width: ${({ theme }) => theme.breakpoint.lg}px) {
display: flex;
@@ -227,9 +226,7 @@ export default function TokenDetailsSkeleton() {
</BreadcrumbNavLink>
<TokenInfoContainer>
<TokenNameCell>
<LogoContainer>
<TokenLogoBubble />
</LogoContainer>
<TokenLogoBubble />
<TitleBubble />
</TokenNameCell>
</TokenInfoContainer>

View File

@@ -45,9 +45,19 @@ export const StatsWrapper = styled.div`
type NumericStat = number | undefined | null
function Stat({ value, title, description }: { value: NumericStat; title: ReactNode; description?: ReactNode }) {
function Stat({
dataCy,
value,
title,
description,
}: {
dataCy: string
value: NumericStat
title: ReactNode
description?: ReactNode
}) {
return (
<StatWrapper>
<StatWrapper data-cy={`${dataCy}`}>
<MouseoverTooltip text={description}>{title}</MouseoverTooltip>
<StatPrice>{formatNumber(value, NumberType.FiatTokenStats)}</StatPrice>
</StatWrapper>
@@ -71,11 +81,13 @@ export default function StatsSection(props: StatsSectionProps) {
<TokenStatsSection>
<StatPair>
<Stat
dataCy="tvl"
value={TVL}
description={HEADER_DESCRIPTIONS[TokenSortMethod.TOTAL_VALUE_LOCKED]}
title={<Trans>TVL</Trans>}
/>
<Stat
dataCy="volume-24h"
value={volume24H}
description={
<Trans>
@@ -86,8 +98,8 @@ export default function StatsSection(props: StatsSectionProps) {
/>
</StatPair>
<StatPair>
<Stat value={priceLow52W} title={<Trans>52W low</Trans>} />
<Stat value={priceHigh52W} title={<Trans>52W high</Trans>} />
<Stat dataCy="52w-low" value={priceLow52W} title={<Trans>52W low</Trans>} />
<Stat dataCy="52w-high" value={priceHigh52W} title={<Trans>52W high</Trans>} />
</StatPair>
</TokenStatsSection>
</StatsWrapper>

View File

@@ -20,7 +20,6 @@ import TokenDetailsSkeleton, {
TokenNameCell,
} from 'components/Tokens/TokenDetails/Skeleton'
import StatsSection from 'components/Tokens/TokenDetails/StatsSection'
import { L2NetworkLogo, LogoContainer } from 'components/Tokens/TokenTable/TokenRow'
import TokenSafetyMessage from 'components/TokenSafety/TokenSafetyMessage'
import TokenSafetyModal from 'components/TokenSafety/TokenSafetyModal'
import Widget from 'components/Widget'
@@ -111,7 +110,7 @@ export default function TokenDetails({
const pageChainId = CHAIN_NAME_TO_CHAIN_ID[chain]
const tokenQueryData = tokenQuery.tokens?.[0]
const tokenQueryData = tokenQuery.token
const crossChainMap = useMemo(
() =>
tokenQueryData?.project?.tokens.reduce((map, current) => {
@@ -134,25 +133,27 @@ export default function TokenDetails({
if (!address) return
const bridgedAddress = crossChainMap[update]
if (bridgedAddress) {
startTokenTransition(() => navigate(getTokenDetailsURL(bridgedAddress, update)))
startTokenTransition(() => navigate(getTokenDetailsURL({ address: bridgedAddress, chain })))
} else if (didFetchFromChain || token?.isNative) {
startTokenTransition(() => navigate(getTokenDetailsURL(address, update)))
startTokenTransition(() => navigate(getTokenDetailsURL({ address, chain })))
}
},
[address, crossChainMap, didFetchFromChain, navigate, token?.isNative]
[address, chain, crossChainMap, didFetchFromChain, navigate, token?.isNative]
)
useOnGlobalChainSwitch(navigateToTokenForChain)
const navigateToWidgetSelectedToken = useCallback(
(token: Currency) => {
const address = token.isNative ? NATIVE_CHAIN_ID : token.address
startTokenTransition(() => navigate(getTokenDetailsURL(address, chain)))
startTokenTransition(() => navigate(getTokenDetailsURL({ address, chain })))
},
[chain, navigate]
)
const [continueSwap, setContinueSwap] = useState<{ resolve: (value: boolean | PromiseLike<boolean>) => void }>()
const [openTokenSafetyModal, setOpenTokenSafetyModal] = useState(false)
// Show token safety modal if Swap-reviewing a warning token, at all times if the current token is blocked
const shouldShowSpeedbump = !useIsUserAddedTokenOnChain(address, pageChainId) && tokenWarning !== null
const onReviewSwapClick = useCallback(
@@ -168,8 +169,6 @@ export default function TokenDetails({
[continueSwap, setContinueSwap]
)
const L2Icon = getChainInfo(pageChainId)?.circleLogoUrl
// address will never be undefined if token is defined; address is checked here to appease typechecker
if (token === undefined || !address) {
return <InvalidTokenDetails chainName={address && getChainInfo(pageChainId)?.label} />
@@ -186,12 +185,10 @@ export default function TokenDetails({
<BreadcrumbNavLink to={`/tokens/${chain.toLowerCase()}`}>
<ArrowLeft data-testid="token-details-return-button" size={14} /> Tokens
</BreadcrumbNavLink>
<TokenInfoContainer>
<TokenInfoContainer data-testid="token-info-container">
<TokenNameCell>
<LogoContainer>
<CurrencyLogo currency={token} size="32px" />
<L2NetworkLogo networkUrl={L2Icon} size="16px" />
</LogoContainer>
<CurrencyLogo currency={token} size="32px" hideL2Icon={false} />
{token.name ?? <Trans>Name not found</Trans>}
<TokenSymbol>{token.symbol ?? <Trans>Symbol not found</Trans>}</TokenSymbol>
</TokenNameCell>
@@ -220,22 +217,28 @@ export default function TokenDetails({
<TokenDetailsSkeleton />
)}
<RightPanel>
<Widget
token={token ?? undefined}
onTokenChange={navigateToWidgetSelectedToken}
onReviewSwapClick={onReviewSwapClick}
/>
<RightPanel onClick={() => isBlockedToken && setOpenTokenSafetyModal(true)}>
<div style={{ pointerEvents: isBlockedToken ? 'none' : 'auto' }}>
<Widget
defaultTokens={{
default: token ?? undefined,
}}
onDefaultTokenChange={navigateToWidgetSelectedToken}
onReviewSwapClick={onReviewSwapClick}
/>
</div>
{tokenWarning && <TokenSafetyMessage tokenAddress={address} warning={tokenWarning} />}
{token && <BalanceSummary token={token} />}
</RightPanel>
{token && <MobileBalanceSummaryFooter token={token} />}
<TokenSafetyModal
isOpen={isBlockedToken || !!continueSwap}
isOpen={openTokenSafetyModal || !!continueSwap}
tokenAddress={address}
onContinue={() => onResolveSwap(true)}
onBlocked={() => navigate(-1)}
onBlocked={() => {
setOpenTokenSafetyModal(false)
}}
onCancel={() => onResolveSwap(false)}
showCancel={true}
/>

View File

@@ -84,6 +84,7 @@ export default function SearchBar() {
element={InterfaceElementName.EXPLORE_SEARCH_INPUT}
>
<SearchInput
data-cy="explore-tokens-search-input"
type="search"
placeholder={`${translation}`}
id="searchBar"

View File

@@ -111,7 +111,7 @@ export default function TimeSelector() {
return (
<StyledMenu ref={node}>
<FilterOption onClick={toggleMenu} aria-label="timeSelector" active={open}>
<FilterOption onClick={toggleMenu} aria-label="timeSelector" active={open} data-testid="time-selector">
<StyledMenuContent>
{DISPLAYS[activeTime]}
<Chevron open={open}>
@@ -128,6 +128,7 @@ export default function TimeSelector() {
{ORDERED_TIMES.map((time) => (
<InternalLinkMenuItem
key={DISPLAYS[time]}
data-testid={DISPLAYS[time]}
onClick={() => {
setTime(time)
toggleMenu()

View File

@@ -6,7 +6,6 @@ import { ParentSize } from '@visx/responsive'
import SparklineChart from 'components/Charts/SparklineChart'
import QueryTokenLogo from 'components/Logo/QueryTokenLogo'
import { MouseoverTooltip } from 'components/Tooltip'
import { getChainInfo } from 'constants/chainInfo'
import { SparklineMap, TopToken } from 'graphql/data/TopTokens'
import { CHAIN_NAME_TO_CHAIN_ID, getTokenDetailsURL } from 'graphql/data/util'
import { useAtomValue } from 'jotai/utils'
@@ -279,23 +278,6 @@ export const SparkLineLoadingBubble = styled(LongLoadingBubble)`
height: 4px;
`
export const L2NetworkLogo = styled.div<{ networkUrl?: string; size?: string }>`
height: ${({ size }) => size ?? '12px'};
width: ${({ size }) => size ?? '12px'};
position: absolute;
left: 50%;
bottom: 0;
background: url(${({ networkUrl }) => networkUrl});
background-repeat: no-repeat;
background-size: ${({ size }) => (size ? `${size} ${size}` : '12px 12px')};
display: ${({ networkUrl }) => !networkUrl && 'none'};
`
export const LogoContainer = styled.div`
position: relative;
align-items: center;
display: flex;
`
const InfoIconContainer = styled.div`
margin-left: 2px;
display: flex;
@@ -307,7 +289,9 @@ export const HEADER_DESCRIPTIONS: Record<TokenSortMethod, ReactNode | undefined>
[TokenSortMethod.PRICE]: undefined,
[TokenSortMethod.PERCENT_CHANGE]: undefined,
[TokenSortMethod.TOTAL_VALUE_LOCKED]: (
<Trans>Total value locked (TVL) is the amount of the asset thats currently in a Uniswap v3 liquidity pool.</Trans>
<Trans>
Total value locked (TVL) is the aggregate amount of the asset available across all Uniswap v3 liquidity pools.
</Trans>
),
[TokenSortMethod.VOLUME]: (
<Trans>Volume is the amount of the asset that has been traded on Uniswap v3 during the selected time frame.</Trans>
@@ -378,15 +362,23 @@ function TokenRow({
const rowCells = (
<>
<ListNumberCell header={header}>{listNumber}</ListNumberCell>
<NameCell>{tokenInfo}</NameCell>
<PriceCell sortable={header}>{price}</PriceCell>
<PercentChangeCell sortable={header}>{percentChange}</PercentChangeCell>
<TvlCell sortable={header}>{tvl}</TvlCell>
<VolumeCell sortable={header}>{volume}</VolumeCell>
<NameCell data-testid="name-cell">{tokenInfo}</NameCell>
<PriceCell data-testid="price-cell" sortable={header}>
{price}
</PriceCell>
<PercentChangeCell data-testid="percent-change-cell" sortable={header}>
{percentChange}
</PercentChangeCell>
<TvlCell data-testid="tvl-cell" sortable={header}>
{tvl}
</TvlCell>
<VolumeCell data-testid="volume-cell" sortable={header}>
{volume}
</VolumeCell>
<SparkLineCell>{sparkLine}</SparkLineCell>
</>
)
if (header) return <StyledHeaderRow>{rowCells}</StyledHeaderRow>
if (header) return <StyledHeaderRow data-testid="header-row">{rowCells}</StyledHeaderRow>
return <StyledTokenRow {...rest}>{rowCells}</StyledTokenRow>
}
@@ -434,34 +426,29 @@ interface LoadedRowProps {
tokenListLength: number
token: NonNullable<TopToken>
sparklineMap: SparklineMap
sortRank: number
}
/* Loaded State: row component with token information */
export const LoadedRow = forwardRef((props: LoadedRowProps, ref: ForwardedRef<HTMLDivElement>) => {
const { tokenListIndex, tokenListLength, token } = props
const tokenAddress = token.address
const tokenName = token.name
const tokenSymbol = token.symbol
const { tokenListIndex, tokenListLength, token, sortRank } = props
const filterString = useAtomValue(filterStringAtom)
const sortAscending = useAtomValue(sortAscendingAtom)
const lowercaseChainName = useParams<{ chainName?: string }>().chainName?.toUpperCase() ?? 'ethereum'
const filterNetwork = lowercaseChainName.toUpperCase()
const chainId = CHAIN_NAME_TO_CHAIN_ID[filterNetwork]
const L2Icon = getChainInfo(chainId)?.circleLogoUrl
const timePeriod = useAtomValue(filterTimeAtom)
const delta = token.market?.pricePercentChange?.value
const arrow = getDeltaArrow(delta)
const smallArrow = getDeltaArrow(delta, 14)
const formattedDelta = formatDelta(delta)
const rank = sortAscending ? tokenListLength - tokenListIndex : tokenListIndex + 1
const exploreTokenSelectedEventProperties = {
chain_id: chainId,
token_address: tokenAddress,
token_symbol: tokenSymbol,
token_address: token.address,
token_symbol: token.symbol,
token_list_index: tokenListIndex,
token_list_rank: rank,
token_list_rank: sortRank,
token_list_length: tokenListLength,
time_frame: timePeriod,
search_token_address_input: filterString,
@@ -469,25 +456,22 @@ export const LoadedRow = forwardRef((props: LoadedRowProps, ref: ForwardedRef<HT
// TODO: currency logo sizing mobile (32px) vs. desktop (24px)
return (
<div ref={ref} data-testid={`token-table-row-${tokenName}`}>
<div ref={ref} data-testid={`token-table-row-${token.symbol}`}>
<StyledLink
to={getTokenDetailsURL(token.address ?? '', token.chain)}
to={getTokenDetailsURL(token)}
onClick={() =>
sendAnalyticsEvent(InterfaceEventName.EXPLORE_TOKEN_ROW_CLICKED, exploreTokenSelectedEventProperties)
}
>
<TokenRow
header={false}
listNumber={rank}
listNumber={sortRank}
tokenInfo={
<ClickableName>
<LogoContainer>
<QueryTokenLogo token={token} />
<L2NetworkLogo networkUrl={L2Icon} />
</LogoContainer>
<QueryTokenLogo token={token} />
<TokenInfoCell>
<TokenName>{tokenName}</TokenName>
<TokenSymbol>{tokenSymbol}</TokenSymbol>
<TokenName data-cy="token-name">{token.name}</TokenName>
<TokenSymbol>{token.symbol}</TokenSymbol>
</TokenInfoCell>
</ClickableName>
}

View File

@@ -76,12 +76,11 @@ function LoadingTokenTable({ rowCount = PAGE_SIZE }: { rowCount?: number }) {
}
export default function TokenTable() {
// TODO: consider moving prefetched call into app.tsx and passing it here, use a preloaded call & updated on interval every 60s
const chainName = validateUrlChainParam(useParams<{ chainName?: string }>().chainName)
const { tokens, loadingTokens, sparklines } = useTopTokens(chainName)
const { tokens, tokenSortRank, loadingTokens, sparklines } = useTopTokens(chainName)
/* loading and error state */
if (loadingTokens) {
if (loadingTokens && !tokens) {
return <LoadingTokenTable rowCount={PAGE_SIZE} />
} else if (!tokens) {
return (
@@ -103,13 +102,14 @@ export default function TokenTable() {
<TokenDataContainer>
{tokens.map(
(token, index) =>
token && (
token?.address && (
<LoadedRow
key={token?.address}
key={token.address}
tokenListIndex={index}
tokenListLength={tokens.length}
token={token}
sparklineMap={sparklines}
sortRank={tokenSortRank[token.address]}
/>
)
)}

View File

@@ -2,8 +2,6 @@ import { useWeb3React } from '@web3-react/core'
import AddressClaimModal from 'components/claim/AddressClaimModal'
import ConnectedAccountBlocked from 'components/ConnectedAccountBlocked'
import FiatOnrampModal from 'components/FiatOnrampModal'
import { BaseVariant } from 'featureFlags'
import { useFiatOnrampFlag } from 'featureFlags/flags/fiatOnramp'
import useAccountRiskCheck from 'hooks/useAccountRiskCheck'
import { lazy } from 'react'
import { useModalIsOpen, useToggleModal } from 'state/application/hooks'
@@ -20,7 +18,6 @@ export default function TopLevelModals() {
const { account } = useWeb3React()
useAccountRiskCheck(account)
const accountBlocked = Boolean(blockedAccountModalOpen && account)
const fiatOnrampFlagEnabled = useFiatOnrampFlag() === BaseVariant.Enabled
return (
<>
@@ -29,7 +26,7 @@ export default function TopLevelModals() {
<Bag />
<TransactionCompleteModal />
<AirdropModal />
{fiatOnrampFlagEnabled && <FiatOnrampModal />}
<FiatOnrampModal />
</>
)
}

View File

@@ -9,8 +9,6 @@ import Tooltip from 'components/Tooltip'
import { getConnection } from 'connection/utils'
import { getChainInfoOrDefault } from 'constants/chainInfo'
import { SupportedChainId } from 'constants/chains'
import { BaseVariant } from 'featureFlags'
import { useFiatOnrampFlag } from 'featureFlags/flags/fiatOnramp'
import useCopyClipboard from 'hooks/useCopyClipboard'
import useStablecoinPrice from 'hooks/useStablecoinPrice'
import useNativeCurrency from 'lib/hooks/useNativeCurrency'
@@ -21,7 +19,6 @@ import { ProfilePageStateType } from 'nft/types'
import { useCallback, useEffect, useMemo, useState } from 'react'
import { Copy, CreditCard, ExternalLink as ExternalLinkIcon, Info, Power } from 'react-feather'
import { useNavigate } from 'react-router-dom'
import { Text } from 'rebass'
import { useCurrencyBalanceString } from 'state/connection/hooks'
import { useAppDispatch } from 'state/hooks'
import { useFiatOnrampAck } from 'state/user/hooks'
@@ -57,7 +54,7 @@ const BuyCryptoButton = styled(ThemeButton)<{ $animateBorder: boolean }>`
border-style: solid;
border-width: 1px;
height: 40px;
margin-top: 12px;
margin-top: 8px;
animation-direction: alternate;
animation-duration: ${({ theme }) => theme.transition.duration.slow};
animation-fill-mode: none;
@@ -70,7 +67,7 @@ const WalletButton = styled(ThemeButton)`
border-radius: 12px;
padding-top: 10px;
padding-bottom: 10px;
margin-top: 12px;
margin-top: 4px;
color: white;
border: none;
`
@@ -127,15 +124,6 @@ const FiatOnrampAvailabilityExternalLink = styled(ExternalLink)`
margin-left: 6px;
width: 14px;
`
const FlexContainer = styled.div`
display: flex;
`
const StatusWrapper = styled.div`
display: inline-block;
margin-top: 4px;
width: 70%;
`
const TruncatedTextStyle = css`
text-overflow: ellipsis;
@@ -143,8 +131,14 @@ const TruncatedTextStyle = css`
white-space: nowrap;
`
const AccountNamesWrapper = styled.div`
const FlexContainer = styled.div`
${TruncatedTextStyle}
padding-right: 4px;
display: inline-flex;
`
const AccountNamesWrapper = styled.div`
min-width: 0;
margin-right: 8px;
`
@@ -168,6 +162,9 @@ const StyledLoadingButtonSpinner = styled(LoadingButtonSpinner)`
fill: ${({ theme }) => theme.accentAction};
`
const BalanceWrapper = styled.div`
display: flex;
flex-direction: column;
gap: 8px;
padding: 16px 0;
`
@@ -177,10 +174,6 @@ const HeaderWrapper = styled.div`
justify-content: space-between;
`
const AuthenticatedHeaderWrapper = styled.div`
padding: 0 16px;
`
const AuthenticatedHeader = () => {
const { account, chainId, connector, ENSName } = useWeb3React()
const [isCopied, setCopied] = useCopyClipboard()
@@ -230,7 +223,6 @@ const AuthenticatedHeader = () => {
closeModal()
}, [clearCollectionFilters, closeModal, navigate, resetSellAssets, setSellPageState])
const fiatOnrampFlag = useFiatOnrampFlag()
// animate the border of the buy crypto button when a user navigates here from the feature announcement
// can be removed when components/FiatOnrampAnnouncment.tsx is no longer used
const [acknowledgements, acknowledge] = useFiatOnrampAck()
@@ -280,21 +272,19 @@ const AuthenticatedHeader = () => {
const closeFiatOnrampUnavailableTooltip = useCallback(() => setShow(false), [setShow])
return (
<AuthenticatedHeaderWrapper>
<>
<HeaderWrapper>
<StatusWrapper>
<FlexContainer>
<StatusIcon connectionType={connectionType} size={24} />
{ENSName ? (
<AccountNamesWrapper>
<ENSNameContainer>{ENSName}</ENSNameContainer>
<AccountContainer>{account && shortenAddress(account, 2, 4)}</AccountContainer>
</AccountNamesWrapper>
) : (
<ThemedText.SubHeader marginTop="2.5px">{account && shortenAddress(account, 2, 4)}</ThemedText.SubHeader>
)}
</FlexContainer>
</StatusWrapper>
<FlexContainer>
<StatusIcon connectionType={connectionType} size={24} />
{ENSName ? (
<AccountNamesWrapper>
<ENSNameContainer>{ENSName}</ENSNameContainer>
<AccountContainer>{account && shortenAddress(account, 2, 4)}</AccountContainer>
</AccountNamesWrapper>
) : (
<ThemedText.SubHeader marginTop="2.5px">{account && shortenAddress(account, 2, 4)}</ThemedText.SubHeader>
)}
</FlexContainer>
<IconContainer>
<IconButton onClick={copy} Icon={Copy}>
{isCopied ? <Trans>Copied!</Trans> : <Trans>Copy</Trans>}
@@ -309,9 +299,10 @@ const AuthenticatedHeader = () => {
</HeaderWrapper>
<Column>
<BalanceWrapper>
<Text fontSize={36} fontWeight={400}>
<ThemedText.SubHeaderSmall>ETH Balance</ThemedText.SubHeaderSmall>
<ThemedText.HeadlineLarge fontSize={36} fontWeight={400}>
{balanceString} {nativeCurrencySymbol}
</Text>
</ThemedText.HeadlineLarge>
{amountUSD !== undefined && <USDText>{formatUSDPrice(amountUSD)} USD</USDText>}
</BalanceWrapper>
<ProfileButton
@@ -322,43 +313,43 @@ const AuthenticatedHeader = () => {
>
<Trans>View and sell NFTs</Trans>
</ProfileButton>
{fiatOnrampFlag === BaseVariant.Enabled && (
<>
<BuyCryptoButton
$animateBorder={animateBuyCryptoButtonBorder}
size={ButtonSize.medium}
emphasis={ButtonEmphasis.medium}
onClick={handleBuyCryptoClick}
disabled={disableBuyCryptoButton}
>
{error ? (
<ThemedText.BodyPrimary>{error}</ThemedText.BodyPrimary>
<BuyCryptoButton
$animateBorder={animateBuyCryptoButtonBorder}
size={ButtonSize.medium}
emphasis={ButtonEmphasis.medium}
onClick={handleBuyCryptoClick}
disabled={disableBuyCryptoButton}
>
{error ? (
<ThemedText.BodyPrimary>{error}</ThemedText.BodyPrimary>
) : (
<>
{fiatOnrampAvailabilityLoading ? (
<StyledLoadingButtonSpinner />
) : (
<>
{fiatOnrampAvailabilityLoading ? <StyledLoadingButtonSpinner /> : <CreditCard />}{' '}
<Trans>Buy crypto</Trans>
</>
)}
</BuyCryptoButton>
{Boolean(!fiatOnrampAvailable && fiatOnrampAvailabilityChecked) && (
<FiatOnrampNotAvailableText marginTop="8px">
<Trans>Not available in your region</Trans>
<Tooltip
show={showFiatOnrampUnavailableTooltip}
text={<Trans>Moonpay is not available in some regions. Click to learn more.</Trans>}
>
<FiatOnrampAvailabilityExternalLink
onMouseEnter={openFiatOnrampUnavailableTooltip}
onMouseLeave={closeFiatOnrampUnavailableTooltip}
style={{ color: 'inherit' }}
href="https://support.uniswap.org/hc/en-us/articles/11306664890381-Why-isn-t-MoonPay-available-in-my-region-"
>
<StyledInfoIcon />
</FiatOnrampAvailabilityExternalLink>
</Tooltip>
</FiatOnrampNotAvailableText>
)}
</>
<CreditCard height="20px" width="20px" />
)}{' '}
<Trans>Buy crypto</Trans>
</>
)}
</BuyCryptoButton>
{Boolean(!fiatOnrampAvailable && fiatOnrampAvailabilityChecked) && (
<FiatOnrampNotAvailableText marginTop="8px">
<Trans>Not available in your region</Trans>
<Tooltip
show={showFiatOnrampUnavailableTooltip}
text={<Trans>Moonpay is not available in some regions. Click to learn more.</Trans>}
>
<FiatOnrampAvailabilityExternalLink
onMouseEnter={openFiatOnrampUnavailableTooltip}
onMouseLeave={closeFiatOnrampUnavailableTooltip}
style={{ color: 'inherit' }}
href="https://support.uniswap.org/hc/en-us/articles/11306664890381-Why-isn-t-MoonPay-available-in-my-region-"
>
<StyledInfoIcon />
</FiatOnrampAvailabilityExternalLink>
</Tooltip>
</FiatOnrampNotAvailableText>
)}
{isUnclaimed && (
<UNIButton onClick={openClaimModal} size={ButtonSize.medium} emphasis={ButtonEmphasis.medium}>
@@ -371,7 +362,7 @@ const AuthenticatedHeader = () => {
</UNIButton>
)}
</Column>
</AuthenticatedHeaderWrapper>
</>
)
}

View File

@@ -91,7 +91,6 @@ const IconWrap = styled.span`
const DefaultMenuWrap = styled.div`
width: 100%;
height: 100%;
padding: 0 8px;
`
const DefaultText = styled.span`

View File

@@ -4,7 +4,6 @@ import styled from 'styled-components/macro'
const Menu = styled.div`
width: 100%;
height: 100%;
font-size: 16px;
overflow: auto;
max-height: 450px;
@@ -58,8 +57,9 @@ const StyledChevron = styled(ChevronLeft)`
const BackSection = styled.div`
position: absolute;
background-color: ${({ theme }) => theme.backgroundSurface};
width: 99%;
padding: 0 16px 16px 16px;
width: fill-available;
margin: 0px 2vw 0px 0px;
padding: 0px 0px 2vh 0px;
color: ${({ theme }) => theme.textSecondary};
cursor: default;
display: flex;

View File

@@ -19,7 +19,7 @@ const WalletWrapper = styled.div`
background-color: ${({ theme }) => theme.backgroundSurface};
border: ${({ theme }) => `1px solid ${theme.backgroundOutline}`};
box-shadow: ${({ theme }) => theme.deepShadow};
padding: 16px 0;
padding: 16px;
@media only screen and (max-width: ${({ theme }) => `${theme.breakpoint.sm}px`}) {
width: 100%;

View File

@@ -70,8 +70,8 @@ it('loads Wallet Modal on desktop', async () => {
it('loads Wallet Modal on desktop with generic Injected', async () => {
jest.spyOn(connectionUtils, 'getIsInjected').mockReturnValue(true)
jest.spyOn(connectionUtils, 'getHasMetaMaskExtensionInstalled').mockReturnValue(false)
jest.spyOn(connectionUtils, 'getHasCoinbaseExtensionInstalled').mockReturnValue(false)
jest.spyOn(connectionUtils, 'getIsMetaMaskWallet').mockReturnValue(false)
jest.spyOn(connectionUtils, 'getIsCoinbaseWallet').mockReturnValue(false)
render(<WalletModal pendingTransactions={[]} confirmedTransactions={[]} />)
expect(screen.getByText('Browser Wallet')).toBeInTheDocument()
@@ -82,8 +82,8 @@ it('loads Wallet Modal on desktop with generic Injected', async () => {
it('loads Wallet Modal on desktop with MetaMask installed', async () => {
jest.spyOn(connectionUtils, 'getIsInjected').mockReturnValue(true)
jest.spyOn(connectionUtils, 'getHasMetaMaskExtensionInstalled').mockReturnValue(true)
jest.spyOn(connectionUtils, 'getHasCoinbaseExtensionInstalled').mockReturnValue(false)
jest.spyOn(connectionUtils, 'getIsMetaMaskWallet').mockReturnValue(true)
jest.spyOn(connectionUtils, 'getIsCoinbaseWallet').mockReturnValue(false)
render(<WalletModal pendingTransactions={[]} confirmedTransactions={[]} />)
expect(screen.getByText('MetaMask')).toBeInTheDocument()
@@ -96,8 +96,8 @@ it('loads Wallet Modal on mobile', async () => {
UserAgentMock.isMobile = true
jest.spyOn(connectionUtils, 'getIsInjected').mockReturnValue(false)
jest.spyOn(connectionUtils, 'getHasMetaMaskExtensionInstalled').mockReturnValue(false)
jest.spyOn(connectionUtils, 'getHasCoinbaseExtensionInstalled').mockReturnValue(false)
jest.spyOn(connectionUtils, 'getIsMetaMaskWallet').mockReturnValue(false)
jest.spyOn(connectionUtils, 'getIsCoinbaseWallet').mockReturnValue(false)
render(<WalletModal pendingTransactions={[]} confirmedTransactions={[]} />)
expect(screen.getByText('Open in Coinbase Wallet')).toBeInTheDocument()
@@ -109,8 +109,8 @@ it('loads Wallet Modal on MetaMask browser', async () => {
UserAgentMock.isMobile = true
jest.spyOn(connectionUtils, 'getIsInjected').mockReturnValue(true)
jest.spyOn(connectionUtils, 'getHasMetaMaskExtensionInstalled').mockReturnValue(true)
jest.spyOn(connectionUtils, 'getHasCoinbaseExtensionInstalled').mockReturnValue(false)
jest.spyOn(connectionUtils, 'getIsMetaMaskWallet').mockReturnValue(true)
jest.spyOn(connectionUtils, 'getIsCoinbaseWallet').mockReturnValue(false)
render(<WalletModal pendingTransactions={[]} confirmedTransactions={[]} />)
expect(screen.getByText('MetaMask')).toBeInTheDocument()
@@ -121,8 +121,8 @@ it('loads Wallet Modal on Coinbase Wallet browser', async () => {
UserAgentMock.isMobile = true
jest.spyOn(connectionUtils, 'getIsInjected').mockReturnValue(true)
jest.spyOn(connectionUtils, 'getHasMetaMaskExtensionInstalled').mockReturnValue(false)
jest.spyOn(connectionUtils, 'getHasCoinbaseExtensionInstalled').mockReturnValue(true)
jest.spyOn(connectionUtils, 'getIsMetaMaskWallet').mockReturnValue(false)
jest.spyOn(connectionUtils, 'getIsCoinbaseWallet').mockReturnValue(true)
render(<WalletModal pendingTransactions={[]} confirmedTransactions={[]} />)
expect(screen.getByText('Coinbase Wallet')).toBeInTheDocument()

View File

@@ -10,9 +10,9 @@ import { networkConnection } from 'connection'
import {
getConnection,
getConnectionName,
getHasCoinbaseExtensionInstalled,
getHasMetaMaskExtensionInstalled,
getIsCoinbaseWallet,
getIsInjected,
getIsMetaMaskWallet,
} from 'connection/utils'
import usePrevious from 'hooks/usePrevious'
import { useCallback, useEffect, useState } from 'react'
@@ -253,8 +253,8 @@ export default function WalletModal({
function getOptions() {
const isInjected = getIsInjected()
const hasMetaMaskExtension = getHasMetaMaskExtensionInstalled()
const hasCoinbaseExtension = getHasCoinbaseExtensionInstalled()
const hasMetaMaskExtension = getIsMetaMaskWallet()
const hasCoinbaseExtension = getIsCoinbaseWallet()
const isCoinbaseWalletBrowser = isMobile && hasCoinbaseExtension
const isMetaMaskBrowser = isMobile && hasMetaMaskExtension

View File

@@ -1,7 +1,6 @@
import { useWeb3React, Web3ReactHooks, Web3ReactProvider } from '@web3-react/core'
import { Connector } from '@web3-react/types'
import { Connection } from 'connection'
import { ConnectionType, setMetMaskErrorHandler } from 'connection'
import { getConnectionName } from 'connection/utils'
import { isSupportedChain } from 'constants/chains'
import { RPC_PROVIDERS } from 'constants/providers'
@@ -9,19 +8,8 @@ import { TraceJsonRpcVariant, useTraceJsonRpcFlag } from 'featureFlags/flags/tra
import useEagerlyConnect from 'hooks/useEagerlyConnect'
import useOrderedConnections from 'hooks/useOrderedConnections'
import { ReactNode, useEffect, useMemo } from 'react'
import { updateConnectionError } from 'state/connection/reducer'
import { useAppDispatch } from 'state/hooks'
export default function Web3Provider({ children }: { children: ReactNode }) {
const dispatch = useAppDispatch()
// Set metamask error handler for metamask disconnection warning modal.
useEffect(() => {
setMetMaskErrorHandler((error: Error) =>
dispatch(updateConnectionError({ connectionType: ConnectionType.INJECTED, error: error.message }))
)
}, [dispatch])
useEagerlyConnect()
const connections = useOrderedConnections()
const connectors: [Connector, Web3ReactHooks][] = connections.map(({ hooks, connector }) => [connector, hooks])

View File

@@ -1,97 +0,0 @@
import { Trans } from '@lingui/macro'
import { ButtonPrimary } from 'components/Button'
import { AutoColumn } from 'components/Column'
import Modal from 'components/Modal'
import { RowBetween } from 'components/Row'
import { AlertTriangle } from 'react-feather'
import { Text } from 'rebass'
import styled from 'styled-components/macro'
import { CloseIcon, ThemedText } from 'theme'
import { useModalIsOpen, useToggleMetamaskConnectionErrorModal } from '../../state/application/hooks'
import { ApplicationModal } from '../../state/application/reducer'
const Wrapper = styled.div`
width: 100%;
position: relative;
display: flex;
flex-flow: column;
align-items: center;
`
const Container = styled.div`
width: 100%;
padding: 32px 32px;
display: flex;
flex-flow: column;
align-items: center;
`
const LogoContainer = styled.div`
display: flex;
gap: 16px;
`
const ShortColumn = styled(AutoColumn)`
margin-top: 10px;
`
const InfoText = styled(Text)`
padding: 0 12px 0 12px;
font-size: 14px;
line-height: 20px;
text-align: center;
`
const StyledButton = styled(ButtonPrimary)`
margin-top: 24px;
width: 100%;
font-weight: 600;
`
const WarningIcon = styled(AlertTriangle)`
width: 76px;
height: 76px;
margin-top: 4px;
margin-bottom: 28px;
stroke-width: 1px;
margin-right: 4px;
color: ${({ theme }) => theme.accentCritical};
`
const onReconnect = () => window.location.reload()
const header = 'Wallet disconnected'
const description = 'A Metamask error caused your wallet to disconnect. Reload the page to reconnect.'
export default function MetamaskConnectionError() {
const modalOpen = useModalIsOpen(ApplicationModal.METAMASK_CONNECTION_ERROR)
const toggleModal = useToggleMetamaskConnectionErrorModal()
return (
<Modal isOpen={modalOpen} onDismiss={toggleModal} minHeight={false} maxHeight={90}>
<Wrapper>
<RowBetween style={{ padding: '1rem' }}>
<div />
<CloseIcon onClick={toggleModal} />
</RowBetween>
<Container>
<AutoColumn>
<LogoContainer>
<WarningIcon />
</LogoContainer>
</AutoColumn>
<ShortColumn>
<InfoText>
<ThemedText.HeadlineSmall marginBottom="8px">{header}</ThemedText.HeadlineSmall>
<ThemedText.BodySmall>{description}</ThemedText.BodySmall>
</InfoText>
</ShortColumn>
<StyledButton onClick={onReconnect}>
<Trans>Reload</Trans>
</StyledButton>
</Container>
</Wrapper>
</Modal>
)
}

View File

@@ -5,12 +5,12 @@ import { useWeb3React } from '@web3-react/core'
import { FiatOnrampAnnouncement } from 'components/FiatOnrampAnnouncement'
import { IconWrapper } from 'components/Identicon/StatusIcon'
import WalletDropdown from 'components/WalletDropdown'
import { getConnection, getIsMetaMask } from 'connection/utils'
import { getConnection } from 'connection/utils'
import { Portal } from 'nft/components/common/Portal'
import { useIsNftClaimAvailable } from 'nft/hooks/useIsNftClaimAvailable'
import { getIsValidSwapQuote } from 'pages/Swap'
import { darken } from 'polished'
import { useCallback, useEffect, useMemo, useRef } from 'react'
import { useCallback, useMemo, useRef } from 'react'
import { AlertTriangle, ChevronDown, ChevronUp } from 'react-feather'
import { useAppSelector } from 'state/hooks'
import { useDerivedSwapInfo } from 'state/swap/hooks'
@@ -22,7 +22,6 @@ import { useOnClickOutside } from '../../hooks/useOnClickOutside'
import {
useCloseModal,
useModalIsOpen,
useOpenMetamaskConnectionErrorModal,
useToggleWalletDropdown,
useToggleWalletModal,
} from '../../state/application/hooks'
@@ -35,7 +34,6 @@ import StatusIcon from '../Identicon/StatusIcon'
import Loader from '../Loader'
import { RowBetween } from '../Row'
import WalletModal from '../WalletModal'
import MetamaskConnectionError from './MetamaskConnectionError'
// https://stackoverflow.com/a/31617326
const FULL_BORDER_RADIUS = 9999
@@ -213,16 +211,10 @@ function Web3StatusInner() {
toggleWalletDropdown()
}, [toggleWalletDropdown])
const toggleWalletModal = useToggleWalletModal()
const openMetamaskConnectionErrorModal = useOpenMetamaskConnectionErrorModal()
const walletIsOpen = useModalIsOpen(ApplicationModal.WALLET_DROPDOWN)
const isClaimAvailable = useIsNftClaimAvailable((state) => state.isClaimAvailable)
const error = useAppSelector((state) => state.connection.errorByConnectionType[connectionType])
useEffect(() => {
if (getIsMetaMask(connectionType) && error) {
openMetamaskConnectionErrorModal()
}
}, [error, connectionType, openMetamaskConnectionErrorModal])
const allTransactions = useAllTransactions()
@@ -326,7 +318,6 @@ export default function Web3Status() {
<Web3StatusInner />
<FiatOnrampAnnouncement />
<WalletModal ENSName={ENSName ?? undefined} pendingTransactions={pending} confirmedTransactions={confirmed} />
<MetamaskConnectionError />
<Portal>
<span ref={walletRef}>
<WalletDropdown />

View File

@@ -26,16 +26,17 @@ import {
getTokenAddress,
} from 'lib/utils/analytics'
import { useCallback, useState } from 'react'
import { useToggleWalletModal } from 'state/application/hooks'
import { useIsDarkMode } from 'state/user/hooks'
import { computeRealizedPriceImpact } from 'utils/prices'
import { switchChain } from 'utils/switchChain'
import { useSyncWidgetInputs } from './inputs'
import { DefaultTokens, useSyncWidgetInputs } from './inputs'
import { useSyncWidgetSettings } from './settings'
import { DARK_THEME, LIGHT_THEME } from './theme'
import { useSyncWidgetTransactions } from './transactions'
export const WIDGET_WIDTH = 360
export const DEFAULT_WIDGET_WIDTH = 360
const WIDGET_ROUTER_URL = 'https://api.uniswap.org/v1/'
@@ -44,19 +45,34 @@ function useWidgetTheme() {
}
interface WidgetProps {
token?: Currency
onTokenChange?: (token: Currency) => void
defaultTokens: DefaultTokens
width?: number | string
onDefaultTokenChange?: (token: Currency) => void
onReviewSwapClick?: OnReviewSwapClick
}
export default function Widget({ token, onTokenChange, onReviewSwapClick }: WidgetProps) {
export default function Widget({
defaultTokens,
width = DEFAULT_WIDGET_WIDTH,
onDefaultTokenChange,
onReviewSwapClick,
}: WidgetProps) {
const { connector, provider } = useWeb3React()
const locale = useActiveLocale()
const theme = useWidgetTheme()
const { inputs, tokenSelector } = useSyncWidgetInputs({ token, onTokenChange })
const { inputs, tokenSelector } = useSyncWidgetInputs({
defaultTokens,
onDefaultTokenChange,
})
const { settings } = useSyncWidgetSettings()
const { transactions } = useSyncWidgetTransactions()
const toggleWalletModal = useToggleWalletModal()
const onConnectWalletClick = useCallback(() => {
toggleWalletModal()
return false // prevents the in-widget wallet modal from opening
}, [toggleWalletModal])
const onSwitchChain = useCallback(
// TODO(WEB-1757): Widget should not break if this rejects - upstream the catch to ignore it.
({ chainId }: AddEthereumChainParameter) => switchChain(connector, Number(chainId)).catch(() => undefined),
@@ -152,8 +168,9 @@ export default function Widget({ token, onTokenChange, onReviewSwapClick }: Widg
routerUrl={WIDGET_ROUTER_URL}
locale={locale}
theme={theme}
width={WIDGET_WIDTH}
width={width}
// defaultChainId is excluded - it is always inferred from the passed provider
onConnectWalletClick={onConnectWalletClick}
provider={provider}
onSwitchChain={onSwitchChain}
tokenList={EMPTY_TOKEN_LIST} // prevents loading the default token list, as we use our own token selector UI
@@ -172,7 +189,7 @@ export default function Widget({ token, onTokenChange, onReviewSwapClick }: Widg
)
}
export function WidgetSkeleton() {
export function WidgetSkeleton({ width = DEFAULT_WIDGET_WIDTH }: { width?: number | string }) {
const theme = useWidgetTheme()
return <SwapWidgetSkeleton theme={theme} width={WIDGET_WIDTH} />
return <SwapWidgetSkeleton theme={theme} width={width} />
}

View File

@@ -8,10 +8,11 @@ const EMPTY_AMOUNT = ''
type SwapValue = Required<SwapController>['value']
type SwapTokens = Pick<SwapValue, Field.INPUT | Field.OUTPUT> & { default?: Currency }
export type DefaultTokens = Partial<SwapTokens>
function includesDefaultToken(tokens: SwapTokens) {
if (!tokens.default) return true
return tokens[Field.INPUT]?.equals(tokens.default) || tokens[Field.OUTPUT]?.equals(tokens.default)
function missingDefaultToken(tokens: SwapTokens) {
if (!tokens.default) return false
return !tokens[Field.INPUT]?.equals(tokens.default) && !tokens[Field.OUTPUT]?.equals(tokens.default)
}
/**
@@ -20,27 +21,31 @@ function includesDefaultToken(tokens: SwapTokens) {
* Enforces that token is a part of the returned value.
*/
export function useSyncWidgetInputs({
token,
onTokenChange,
defaultTokens,
onDefaultTokenChange,
}: {
token?: Currency
onTokenChange?: (token: Currency) => void
defaultTokens: DefaultTokens
onDefaultTokenChange?: (token: Currency) => void
}) {
const trace = useTrace({ section: InterfaceSectionName.WIDGET })
const [type, setType] = useState<SwapValue['type']>(TradeType.EXACT_INPUT)
const [amount, setAmount] = useState<SwapValue['amount']>(EMPTY_AMOUNT)
const [tokens, setTokens] = useState<SwapTokens>({ [Field.OUTPUT]: token, default: token })
const [tokens, setTokens] = useState<SwapTokens>(defaultTokens)
useEffect(() => {
setTokens((tokens) => {
const update = { ...tokens, default: token }
if (!includesDefaultToken(update)) {
return { [Field.OUTPUT]: update.default, default: update.default }
}
return update
})
}, [token])
if (!tokens[Field.INPUT] && !tokens[Field.OUTPUT]) {
setTokens((tokens) => {
const update = {
...tokens,
[Field.INPUT]: defaultTokens[Field.INPUT] ?? tokens[Field.INPUT],
[Field.OUTPUT]: defaultTokens[Field.OUTPUT] ?? tokens[Field.OUTPUT] ?? defaultTokens.default,
default: defaultTokens.default,
}
return update
})
}
}, [defaultTokens, tokens])
const onAmountChange = useCallback(
(field: Field, amount: string, origin?: 'max') => {
@@ -96,13 +101,14 @@ export function useSyncWidgetInputs({
return type
})
if (!includesDefaultToken(update)) {
onTokenChange?.(update[Field.OUTPUT] || selectingToken)
if (missingDefaultToken(update)) {
onDefaultTokenChange?.(update[Field.OUTPUT] ?? selectingToken)
}
setTokens(update)
},
[onTokenChange, selectingField, tokens]
[onDefaultTokenChange, selectingField, tokens]
)
const tokenSelector = (
<CurrencySearchModal
isOpen={selectingField !== undefined}
@@ -110,6 +116,7 @@ export function useSyncWidgetInputs({
selectedCurrency={selectingField && tokens[selectingField]}
otherSelectedCurrency={selectingField && tokens[invertField(selectingField)]}
onCurrencySelect={onTokenSelect}
showCommonBases
/>
)
@@ -117,11 +124,11 @@ export function useSyncWidgetInputs({
() => ({
type,
amount,
// If the default has not yet been handled, preemptively disable the widget by passing no tokens. Effectively,
// If the initial state has not yet been set, preemptively disable the widget by passing no tokens. Effectively,
// this resets the widget - avoiding rendering stale state - because with no tokens the skeleton will be rendered.
...(token && tokens.default?.equals(token) ? tokens : undefined),
...(tokens[Field.INPUT] || tokens[Field.OUTPUT] ? tokens : undefined),
}),
[amount, token, tokens, type]
[amount, tokens, type]
)
const valueHandlers: SwapEventHandlers = useMemo(
() => ({ onAmountChange, onSwitchTokens, onTokenSelectorClick }),

View File

@@ -1,5 +1,5 @@
import { Percent } from '@uniswap/sdk-core'
import { Slippage, SwapController, SwapEventHandlers } from '@uniswap/widgets'
import { RouterPreference, Slippage, SwapController, SwapEventHandlers } from '@uniswap/widgets'
import { DEFAULT_DEADLINE_FROM_NOW } from 'constants/misc'
import { useCallback, useMemo, useState } from 'react'
import { useUserSlippageTolerance, useUserTransactionTTL } from 'state/user/hooks'
@@ -37,6 +37,8 @@ export function useSyncWidgetSettings() {
[setAppSlippage]
)
const [routerPreference, onRouterPreferenceChange] = useState(RouterPreference.API)
const onSettingsReset = useCallback(() => {
setWidgetTtl(undefined)
setAppTtl(DEFAULT_DEADLINE_FROM_NOW)
@@ -46,11 +48,15 @@ export function useSyncWidgetSettings() {
const settings: SwapController['settings'] = useMemo(() => {
const auto = appSlippage === 'auto'
return { slippage: { auto, max: widgetSlippage }, transactionTtl: widgetTtl }
}, [widgetSlippage, widgetTtl, appSlippage])
return {
slippage: { auto, max: widgetSlippage },
transactionTtl: widgetTtl,
routerPreference,
}
}, [appSlippage, widgetSlippage, widgetTtl, routerPreference])
const settingsHandlers: SwapEventHandlers = useMemo(
() => ({ onSettingsReset, onSlippageChange, onTransactionDeadlineChange }),
[onSettingsReset, onSlippageChange, onTransactionDeadlineChange]
() => ({ onSettingsReset, onSlippageChange, onTransactionDeadlineChange, onRouterPreferenceChange }),
[onSettingsReset, onSlippageChange, onTransactionDeadlineChange, onRouterPreferenceChange]
)
return { settings: { settings, ...settingsHandlers } }

View File

@@ -1,45 +1,68 @@
import { Theme } from '@uniswap/widgets'
import { darkTheme, lightTheme } from 'theme/colors'
import { Z_INDEX } from 'theme/zIndex'
const zIndex = {
modal: Z_INDEX.modal,
}
const fonts = {
fontFamily: 'Inter custom',
}
export const LIGHT_THEME = {
export const LIGHT_THEME: Theme = {
// surface
container: lightTheme.backgroundSurface,
interactive: lightTheme.backgroundInteractive,
module: lightTheme.backgroundModule,
accent: lightTheme.accentAction,
dialog: lightTheme.backgroundBackdrop,
accentSoft: lightTheme.accentActionSoft,
container: lightTheme.backgroundSurface,
module: lightTheme.backgroundModule,
interactive: lightTheme.backgroundInteractive,
outline: lightTheme.backgroundOutline,
dialog: lightTheme.backgroundBackdrop,
scrim: lightTheme.backgroundScrim,
// text
onAccent: lightTheme.white,
primary: lightTheme.textPrimary,
secondary: lightTheme.textSecondary,
hint: lightTheme.textTertiary,
onInteractive: lightTheme.accentTextDarkPrimary,
// shadow
deepShadow: lightTheme.deepShadow,
networkDefaultShadow: lightTheme.networkDefaultShadow,
// state
success: lightTheme.accentSuccess,
warning: lightTheme.accentWarning,
error: lightTheme.accentCritical,
...fonts,
zIndex,
}
export const DARK_THEME = {
export const DARK_THEME: Theme = {
// surface
container: darkTheme.backgroundSurface,
interactive: darkTheme.backgroundInteractive,
module: darkTheme.backgroundModule,
accent: darkTheme.accentAction,
dialog: darkTheme.backgroundBackdrop,
accentSoft: darkTheme.accentActionSoft,
container: darkTheme.backgroundSurface,
module: darkTheme.backgroundModule,
interactive: darkTheme.backgroundInteractive,
outline: darkTheme.backgroundOutline,
dialog: darkTheme.backgroundBackdrop,
scrim: darkTheme.backgroundScrim,
// text
onAccent: darkTheme.white,
primary: darkTheme.textPrimary,
secondary: darkTheme.textSecondary,
hint: darkTheme.textTertiary,
onInteractive: darkTheme.accentTextLightPrimary,
// shadow
deepShadow: darkTheme.deepShadow,
networkDefaultShadow: darkTheme.networkDefaultShadow,
// state
success: darkTheme.accentSuccess,
warning: darkTheme.accentWarning,
error: darkTheme.accentCritical,
...fonts,
zIndex,
}

View File

@@ -1,15 +1,23 @@
import { sendAnalyticsEvent, useTrace } from '@uniswap/analytics'
import { InterfaceEventName, InterfaceSectionName, SwapEventName } from '@uniswap/analytics-events'
import { Trade } from '@uniswap/router-sdk'
import { Currency, Percent } from '@uniswap/sdk-core'
import {
OnTxSuccess,
TradeType,
Transaction,
TransactionEventHandlers,
TransactionInfo,
TransactionType,
TransactionType as WidgetTransactionType,
} from '@uniswap/widgets'
import { useWeb3React } from '@web3-react/core'
import { WrapType } from 'hooks/useWrapCallback'
import { formatSwapSignedAnalyticsEventProperties, formatToDecimal, getTokenAddress } from 'lib/utils/analytics'
import {
formatPercentInBasisPointsNumber,
formatSwapSignedAnalyticsEventProperties,
formatToDecimal,
getTokenAddress,
} from 'lib/utils/analytics'
import { useCallback, useMemo } from 'react'
import { useTransactionAdder } from 'state/transactions/hooks'
import {
@@ -19,6 +27,42 @@ import {
WrapTransactionInfo,
} from 'state/transactions/types'
import { currencyId } from 'utils/currencyId'
import { computeRealizedPriceImpact } from 'utils/prices'
interface AnalyticsEventProps {
trade: Trade<Currency, Currency, TradeType>
gasUsed: string | undefined
blockNumber: number | undefined
hash: string | undefined
allowedSlippage: Percent
succeeded: boolean
}
const formatAnalyticsEventProperties = ({
trade,
hash,
allowedSlippage,
succeeded,
gasUsed,
blockNumber,
}: AnalyticsEventProps) => ({
estimated_network_fee_usd: gasUsed,
transaction_hash: hash,
token_in_address: getTokenAddress(trade.inputAmount.currency),
token_out_address: getTokenAddress(trade.outputAmount.currency),
token_in_symbol: trade.inputAmount.currency.symbol,
token_out_symbol: trade.outputAmount.currency.symbol,
token_in_amount: formatToDecimal(trade.inputAmount, trade.inputAmount.currency.decimals),
token_out_amount: formatToDecimal(trade.outputAmount, trade.outputAmount.currency.decimals),
price_impact_basis_points: formatPercentInBasisPointsNumber(computeRealizedPriceImpact(trade)),
allowed_slippage_basis_points: formatPercentInBasisPointsNumber(allowedSlippage),
chain_id:
trade.inputAmount.currency.chainId === trade.outputAmount.currency.chainId
? trade.inputAmount.currency.chainId
: undefined,
swap_quote_block_number: blockNumber,
succeeded,
})
/** Integrates the Widget's transactions, showing the widget's transactions in the app. */
export function useSyncWidgetTransactions() {
@@ -46,7 +90,7 @@ export function useSyncWidgetTransactions() {
amount: transactionAmount
? formatToDecimal(transactionAmount, transactionAmount?.currency.decimals)
: undefined,
type: type === WidgetTransactionType.WRAP ? WrapType.WRAP : WrapType.UNWRAP,
type: type === WidgetTransactionType.WRAP ? TransactionType.WRAP : TransactionType.UNWRAP,
...trace,
}
sendAnalyticsEvent(InterfaceEventName.WRAP_TOKEN_TXN_SUBMITTED, eventProperties)
@@ -94,7 +138,24 @@ export function useSyncWidgetTransactions() {
[addTransaction, chainId, trace]
)
const txHandlers: TransactionEventHandlers = useMemo(() => ({ onTxSubmit }), [onTxSubmit])
const onTxSuccess: OnTxSuccess = useCallback((hash: string, tx) => {
if (tx.info.type === TransactionType.SWAP) {
const { trade, slippageTolerance } = tx.info
sendAnalyticsEvent(
SwapEventName.SWAP_TRANSACTION_COMPLETED,
formatAnalyticsEventProperties({
trade,
hash,
gasUsed: tx.receipt?.gasUsed?.toString(),
blockNumber: tx.receipt?.blockNumber,
allowedSlippage: slippageTolerance,
succeeded: tx.receipt?.status === 1,
})
)
}
}, [])
const txHandlers: TransactionEventHandlers = useMemo(() => ({ onTxSubmit, onTxSuccess }), [onTxSubmit, onTxSuccess])
return { transactions: { ...txHandlers } }
}

View File

@@ -25,21 +25,10 @@ export interface Connection {
type: ConnectionType
}
let metaMaskErrorHandler: (error: Error) => void | undefined
export function setMetMaskErrorHandler(errorHandler: (error: Error) => void) {
metaMaskErrorHandler = errorHandler
}
function onError(error: Error) {
console.debug(`web3-react error: ${error}`)
}
function onMetamaskError(error: Error) {
onError(error)
metaMaskErrorHandler?.(error)
}
const [web3Network, web3NetworkHooks] = initializeConnector<Network>(
(actions) => new Network({ actions, urlMap: RPC_PROVIDERS, defaultChainId: 1 })
)
@@ -49,9 +38,7 @@ export const networkConnection: Connection = {
type: ConnectionType.NETWORK,
}
const [web3Injected, web3InjectedHooks] = initializeConnector<MetaMask>(
(actions) => new MetaMask({ actions, onError: onMetamaskError })
)
const [web3Injected, web3InjectedHooks] = initializeConnector<MetaMask>((actions) => new MetaMask({ actions, onError }))
export const injectedConnection: Connection = {
connector: web3Injected,
hooks: web3InjectedHooks,

View File

@@ -12,18 +12,21 @@ export function getIsInjected(): boolean {
return Boolean(window.ethereum)
}
export function getHasMetaMaskExtensionInstalled(): boolean {
return window.ethereum?.isMetaMask ?? false
export function getIsBraveWallet(): boolean {
return window.ethereum?.isBraveWallet ?? false
}
export function getHasCoinbaseExtensionInstalled(): boolean {
export function getIsMetaMaskWallet(): boolean {
// When using Brave browser, `isMetaMask` is set to true when using the built-in wallet
// This function should return true only when using the MetaMask extension
// https://wallet-docs.brave.com/ethereum/wallet-detection#compatability-with-metamask
return (window.ethereum?.isMetaMask ?? false) && !getIsBraveWallet()
}
export function getIsCoinbaseWallet(): boolean {
return window.ethereum?.isCoinbaseWallet ?? false
}
export function getIsMetaMask(connectionType: ConnectionType): boolean {
return connectionType === ConnectionType.INJECTED && getHasMetaMaskExtensionInstalled()
}
const CONNECTIONS = [
gnosisSafeConnection,
injectedConnection,
@@ -56,7 +59,7 @@ export function getConnection(c: Connector | ConnectionType) {
export function getConnectionName(
connectionType: ConnectionType,
hasMetaMaskExtension: boolean = getHasMetaMaskExtensionInstalled()
hasMetaMaskExtension: boolean = getIsMetaMaskWallet()
) {
switch (connectionType) {
case ConnectionType.INJECTED:

View File

@@ -20,7 +20,8 @@ export const COMMON_CONTRACT_NAMES: Record<number, { [address: string]: string }
},
}
export const DEFAULT_AVERAGE_BLOCK_TIME_IN_SECS = 13
// in PoS, ethereum block time is 12s, see https://ethereum.org/en/developers/docs/blocks/#block-time
export const DEFAULT_AVERAGE_BLOCK_TIME_IN_SECS = 12
// Block time here is slightly higher (~1s) than average in order to avoid ongoing proposals past the displayed time
export const AVERAGE_BLOCK_TIME_IN_SECS: { [chainId: number]: number } = {

View File

@@ -1,6 +1,6 @@
export const UNI_LIST = 'https://tokens.uniswap.org'
export const UNI_EXTENDED_LIST = 'https://extendedtokens.uniswap.org/'
const UNI_UNSUPPORTED_LIST = 'https://unsupportedtokens.uniswap.org/'
export const UNI_LIST = 'https://gateway.ipfs.io/ipns/tokens.uniswap.org'
export const UNI_EXTENDED_LIST = 'https://gateway.ipfs.io/ipns/extendedtokens.uniswap.org'
const UNI_UNSUPPORTED_LIST = 'https://gateway.ipfs.io/ipns/unsupportedtokens.uniswap.org'
const AAVE_LIST = 'tokenlist.aave.eth'
const BA_LIST = 'https://raw.githubusercontent.com/The-Blockchain-Association/sec-notice-list/master/ba-sec-list.json'
const CMC_ALL_LIST = 'https://api.coinmarketcap.com/data-api/v3/uniswap/all.json'
@@ -8,7 +8,6 @@ const COINGECKO_LIST = 'https://tokens.coingecko.com/uniswap/all.json'
const COMPOUND_LIST = 'https://raw.githubusercontent.com/compound-finance/token-list/master/compound.tokenlist.json'
const GEMINI_LIST = 'https://www.gemini.com/uniswap/manifest.json'
const KLEROS_LIST = 't2crtokens.eth'
const ROLL_LIST = 'https://app.tryroll.com/tokens.json'
const SET_LIST = 'https://raw.githubusercontent.com/SetProtocol/uniswap-tokenlist/main/set.tokenlist.json'
const WRAPPED_LIST = 'wrapped.tokensoft.eth'
@@ -30,7 +29,6 @@ export const DEFAULT_INACTIVE_LIST_URLS: string[] = [
GEMINI_LIST,
WRAPPED_LIST,
SET_LIST,
ROLL_LIST,
ARBITRUM_LIST,
OPTIMISM_LIST,
CELO_LIST,

View File

@@ -1,4 +1,6 @@
import { Plural, Trans } from '@lingui/macro'
import { TokenStandard } from 'graphql/data/__generated__/types-and-hooks'
import { SearchToken } from 'graphql/data/SearchTokens'
import { ZERO_ADDRESS } from './misc'
import { NATIVE_CHAIN_ID } from './tokens'
@@ -94,3 +96,11 @@ export function checkWarning(tokenAddress: string) {
return BlockedWarning
}
}
// TODO(cartcrom): Replace all usage of WARNING_LEVEL with SafetyLevel
export function checkSearchTokenWarning(token: SearchToken) {
if (!token.address) {
return token.standard === TokenStandard.Native ? null : StrongWarning
}
return checkWarning(token.address)
}

View File

@@ -1,6 +1,12 @@
export enum FeatureFlag {
fiatOnramp = 'fiatOnramp',
traceJsonRpc = 'traceJsonRpc',
permit2 = 'permit2',
nftListV2 = 'nftListV2',
payWithAnyToken = 'payWithAnyToken',
swapWidget = 'swapWidget',
gqlRouting = 'gqlRouting',
}
export enum FeatureGate {
DUMMY = 'web_dummy_gate_amplitude_id',
}

View File

@@ -1,6 +0,0 @@
import { BaseVariant } from '../index'
export function useFiatOnrampFlag(): BaseVariant {
return BaseVariant.Enabled
// return useBaseFlag(FeatureFlag.fiatOnramp)
}

View File

@@ -0,0 +1,7 @@
import { BaseVariant, FeatureFlag, useBaseFlag } from '../index'
export function useGqlRoutingFlag(): BaseVariant {
return useBaseFlag(FeatureFlag.gqlRouting)
}
export { BaseVariant as GqlRoutingVariant }

View File

@@ -1,7 +1,7 @@
import { BaseVariant, FeatureFlag, useBaseFlag } from '../index'
import { BaseVariant } from '../index'
export function useNftListV2Flag(): BaseVariant {
return useBaseFlag(FeatureFlag.nftListV2)
return BaseVariant.Enabled
}
export { BaseVariant as NftListV2Variant }

View File

@@ -0,0 +1,24 @@
import { UNIVERSAL_ROUTER_ADDRESS } from '@uniswap/universal-router-sdk'
import { useWeb3React } from '@web3-react/core'
import { BaseVariant, FeatureFlag, useBaseFlag } from '../index'
export function usePayWithAnyTokenFlag(): BaseVariant {
return useBaseFlag(FeatureFlag.payWithAnyToken)
}
export function usePayWithAnyTokenEnabled(): boolean {
const flagEnabled = usePayWithAnyTokenFlag() === BaseVariant.Enabled
const { chainId } = useWeb3React()
try {
// Detect if the Universal Router is not yet deployed to chainId.
// This is necessary so that we can fallback correctly on chains without a Universal Router deployment.
// It will be removed once Universal Router is deployed on all supported chains.
chainId && UNIVERSAL_ROUTER_ADDRESS(chainId)
return flagEnabled
} catch {
return false
}
}
export { BaseVariant as PayWithAnyTokenVariant }

View File

@@ -4,7 +4,7 @@ import { useWeb3React } from '@web3-react/core'
import { BaseVariant, FeatureFlag, useBaseFlag } from '../index'
export function usePermit2Flag(): BaseVariant {
return useBaseFlag(FeatureFlag.permit2)
return useBaseFlag(FeatureFlag.permit2, BaseVariant.Enabled)
}
export function usePermit2Enabled(): boolean {

View File

@@ -0,0 +1,11 @@
import { BaseVariant, FeatureFlag, useBaseFlag } from '../index'
export function useSwapWidgetFlag(): BaseVariant {
return useBaseFlag(FeatureFlag.swapWidget, BaseVariant.Control)
}
export function useSwapWidgetEnabled(): boolean {
return useSwapWidgetFlag() === BaseVariant.Enabled
}
export { BaseVariant as SwapWidgetVariant }

View File

@@ -0,0 +1,59 @@
import gql from 'graphql-tag'
gql`
query RecentlySearchedAssets($collectionAddresses: [String!]!, $contracts: [ContractInput!]!) {
nftCollections(filter: { addresses: $collectionAddresses }) {
edges {
node {
collectionId
image {
url
}
isVerified
name
numAssets
nftContracts {
address
}
markets(currencies: ETH) {
floorPrice {
currency
value
}
}
}
}
}
tokens(contracts: $contracts) {
id
decimals
name
chain
standard
address
symbol
market(currency: USD) {
id
price {
id
value
currency
}
pricePercentChange(duration: DAY) {
id
value
}
volume24H: volume(duration: DAY) {
id
value
currency
}
}
project {
id
logoUrl
safetyLevel
}
}
}
`

View File

@@ -0,0 +1,102 @@
import { WRAPPED_NATIVE_CURRENCY } from 'constants/tokens'
import gql from 'graphql-tag'
import { useMemo } from 'react'
import { Chain, SearchTokensQuery, useSearchTokensQuery } from './__generated__/types-and-hooks'
import { chainIdToBackendName } from './util'
gql`
query SearchTokens($searchQuery: String!) {
searchTokens(searchQuery: $searchQuery) {
id
decimals
name
chain
standard
address
symbol
market(currency: USD) {
id
price {
id
value
currency
}
pricePercentChange(duration: DAY) {
id
value
}
volume24H: volume(duration: DAY) {
id
value
currency
}
}
project {
id
logoUrl
safetyLevel
}
}
}
`
export type SearchToken = NonNullable<NonNullable<SearchTokensQuery['searchTokens']>[number]>
function isMoreRevelantToken(current: SearchToken, existing: SearchToken | undefined, searchChain: Chain) {
if (!existing) return true
// Always priotize natives, and if both tokens are native, prefer native on current chain (i.e. Matic on Polygon over Matic on Mainnet )
if (current.standard === 'NATIVE' && (existing.standard !== 'NATIVE' || current.chain === searchChain)) return true
// Prefer tokens on the searched chain, otherwise prefer mainnet tokens
return current.chain === searchChain || (existing.chain !== searchChain && current.chain === Chain.Ethereum)
}
// Places natives first, wrapped native on current chain next, then sorts by volume
function searchTokenSortFunction(
searchChain: Chain,
wrappedNativeAddress: string | undefined,
a: SearchToken,
b: SearchToken
) {
if (a.standard === 'NATIVE') {
if (b.standard === 'NATIVE') {
if (a.chain === searchChain) return -1
else if (b.chain === searchChain) return 1
else return 0
} else return -1
} else if (b.standard === 'NATIVE') return 1
else if (wrappedNativeAddress && a.address === wrappedNativeAddress) return -1
else if (wrappedNativeAddress && b.address === wrappedNativeAddress) return 1
else return (b.market?.volume24H?.value ?? 0) - (a.market?.volume24H?.value ?? 0)
}
export function useSearchTokens(searchQuery: string, chainId: number) {
const { data, loading, error } = useSearchTokensQuery({
variables: {
searchQuery,
},
})
const sortedTokens = useMemo(() => {
const searchChain = chainIdToBackendName(chainId)
// Stores results, allowing overwriting cross-chain tokens w/ more 'relevant token'
const selectionMap: { [projectId: string]: SearchToken } = {}
data?.searchTokens?.forEach((token) => {
if (token.project?.id) {
const existing = selectionMap[token.project.id]
if (isMoreRevelantToken(token, existing, searchChain)) selectionMap[token.project.id] = token
}
})
return Object.values(selectionMap).sort(
searchTokenSortFunction.bind(null, searchChain, WRAPPED_NATIVE_CURRENCY[chainId]?.address)
)
}, [data, chainId])
return {
data: sortedTokens,
loading,
error,
}
}

View File

@@ -14,40 +14,49 @@ The difference between Token and TokenProject:
TokenProjectMarket is aggregated market data (aggregated over multiple dexes and centralized exchanges) that we get from coingecko.
*/
gql`
query Token($contract: ContractInput!) {
tokens(contracts: [$contract]) {
query Token($chain: Chain!, $address: String = null) {
token(chain: $chain, address: $address) {
id
decimals
name
chain
address
symbol
standard
market(currency: USD) {
id
totalValueLocked {
id
value
currency
}
price {
id
value
currency
}
volume24H: volume(duration: DAY) {
id
value
currency
}
priceHigh52W: priceHighLow(duration: YEAR, highLow: HIGH) {
id
value
}
priceLow52W: priceHighLow(duration: YEAR, highLow: LOW) {
id
value
}
}
project {
id
description
homepageUrl
twitterName
logoUrl
tokens {
id
chain
address
}
@@ -58,7 +67,7 @@ gql`
export type { Chain, TokenQuery } from './__generated__/types-and-hooks'
export type TokenQueryData = NonNullable<TokenQuery['tokens']>[number]
export type TokenQueryData = TokenQuery['token']
// TODO: Return a QueryToken from useTokenQuery instead of TokenQueryData to make it more usable in Currency-centric interfaces.
export class QueryToken extends WrappedTokenInfo {

View File

@@ -1,14 +1,19 @@
import gql from 'graphql-tag'
// TODO: Implemnt this as a refetchable fragment on tokenQuery when backend adds support
gql`
query TokenPrice($contract: ContractInput!, $duration: HistoryDuration!) {
tokens(contracts: [$contract]) {
query TokenPrice($chain: Chain!, $address: String = null, $duration: HistoryDuration!) {
token(chain: $chain, address: $address) {
id
address
chain
market(currency: USD) {
id
price {
id
value
}
priceHistory(duration: $duration) {
id
timestamp
value
}

View File

@@ -15,7 +15,15 @@ import {
useTopTokens100Query,
useTopTokensSparklineQuery,
} from './__generated__/types-and-hooks'
import { CHAIN_NAME_TO_CHAIN_ID, isPricePoint, PricePoint, toHistoryDuration, unwrapToken } from './util'
import {
CHAIN_NAME_TO_CHAIN_ID,
isPricePoint,
PollingInterval,
PricePoint,
toHistoryDuration,
unwrapToken,
usePollQueryWhileMounted,
} from './util'
gql`
query TopTokens100($duration: HistoryDuration!, $chain: Chain!) {
@@ -25,25 +33,32 @@ gql`
chain
address
symbol
standard
market(currency: USD) {
id
totalValueLocked {
id
value
currency
}
price {
id
value
currency
}
pricePercentChange(duration: $duration) {
id
currency
value
}
volume(duration: $duration) {
id
value
currency
}
}
project {
id
logoUrl
}
}
@@ -53,9 +68,13 @@ gql`
gql`
query TopTokensSparkline($duration: HistoryDuration!, $chain: Chain!) {
topTokens(pageSize: 100, page: 1, chain: $chain) {
id
address
chain
market(currency: USD) {
id
priceHistory(duration: $duration) {
id
timestamp
value
}
@@ -64,11 +83,12 @@ gql`
}
`
function useSortedTokens(tokens: NonNullable<TopTokens100Query['topTokens']>) {
function useSortedTokens(tokens: TopTokens100Query['topTokens']) {
const sortMethod = useAtomValue(sortMethodAtom)
const sortAscending = useAtomValue(sortAscendingAtom)
return useMemo(() => {
if (!tokens) return undefined
let tokenArray = Array.from(tokens)
switch (sortMethod) {
case TokenSortMethod.PRICE:
@@ -93,12 +113,13 @@ function useSortedTokens(tokens: NonNullable<TopTokens100Query['topTokens']>) {
}, [tokens, sortMethod, sortAscending])
}
function useFilteredTokens(tokens: NonNullable<TopTokens100Query['topTokens']>) {
function useFilteredTokens(tokens: TopTokens100Query['topTokens']) {
const filterString = useAtomValue(filterStringAtom)
const lowercaseFilterString = useMemo(() => filterString.toLowerCase(), [filterString])
return useMemo(() => {
if (!tokens) return undefined
let returnTokens = tokens
if (lowercaseFilterString) {
returnTokens = returnTokens?.filter((token) => {
@@ -119,6 +140,7 @@ export type TopToken = NonNullable<NonNullable<TopTokens100Query>['topTokens']>[
interface UseTopTokensReturnValue {
tokens: TopToken[] | undefined
tokenSortRank: Record<string, number>
loadingTokens: boolean
sparklines: SparklineMap
}
@@ -127,9 +149,12 @@ export function useTopTokens(chain: Chain): UseTopTokensReturnValue {
const chainId = CHAIN_NAME_TO_CHAIN_ID[chain]
const duration = toHistoryDuration(useAtomValue(filterTimeAtom))
const { data: sparklineQuery } = useTopTokensSparklineQuery({
variables: { duration, chain },
})
const { data: sparklineQuery } = usePollQueryWhileMounted(
useTopTokensSparklineQuery({
variables: { duration, chain },
}),
PollingInterval.Slow
)
const sparklines = useMemo(() => {
const unwrappedTokens = sparklineQuery?.topTokens?.map((topToken) => unwrapToken(chainId, topToken))
@@ -140,14 +165,29 @@ export function useTopTokens(chain: Chain): UseTopTokensReturnValue {
return map
}, [chainId, sparklineQuery?.topTokens])
const { data, loading: loadingTokens } = useTopTokens100Query({
variables: { duration, chain },
})
const mappedTokens = useMemo(
() => data?.topTokens?.map((token) => unwrapToken(chainId, token)) ?? [],
[chainId, data]
const { data, loading: loadingTokens } = usePollQueryWhileMounted(
useTopTokens100Query({
variables: { duration, chain },
}),
PollingInterval.Fast
)
const unwrappedTokens = useMemo(() => data?.topTokens?.map((token) => unwrapToken(chainId, token)), [chainId, data])
const sortedTokens = useSortedTokens(unwrappedTokens)
const tokenSortRank = useMemo(
() =>
sortedTokens?.reduce((acc, cur, i) => {
if (!cur.address) return acc
return {
...acc,
[cur.address]: i + 1,
}
}, {}) ?? {},
[sortedTokens]
)
const filteredTokens = useFilteredTokens(sortedTokens)
return useMemo(
() => ({ tokens: filteredTokens, tokenSortRank, loadingTokens, sparklines }),
[filteredTokens, tokenSortRank, loadingTokens, sparklines]
)
const filteredTokens = useFilteredTokens(mappedTokens)
const sortedTokens = useSortedTokens(filteredTokens)
return useMemo(() => ({ tokens: sortedTokens, loadingTokens, sparklines }), [loadingTokens, sortedTokens, sparklines])
}

View File

@@ -0,0 +1,51 @@
import gql from 'graphql-tag'
import { useMemo } from 'react'
import { useTrendingTokensQuery } from './__generated__/types-and-hooks'
import { chainIdToBackendName, unwrapToken } from './util'
gql`
query TrendingTokens($chain: Chain!) {
topTokens(pageSize: 4, page: 1, chain: $chain, orderBy: VOLUME) {
id
decimals
name
chain
standard
address
symbol
market(currency: USD) {
id
price {
id
value
currency
}
pricePercentChange(duration: DAY) {
id
value
}
volume24H: volume(duration: DAY) {
id
value
currency
}
}
project {
id
logoUrl
safetyLevel
}
}
}
`
export default function useTrendingTokens(chainId?: number) {
const chain = chainIdToBackendName(chainId)
const { data, loading } = useTrendingTokensQuery({ variables: { chain } })
return useMemo(
() => ({ data: data?.topTokens?.map((token) => unwrapToken(chainId ?? 1, token)), loading }),
[chainId, data?.topTokens, loading]
)
}

View File

@@ -82,7 +82,8 @@ export enum Chain {
Ethereum = 'ETHEREUM',
EthereumGoerli = 'ETHEREUM_GOERLI',
Optimism = 'OPTIMISM',
Polygon = 'POLYGON'
Polygon = 'POLYGON',
UnknownChain = 'UNKNOWN_CHAIN'
}
export type ContractInput = {
@@ -139,6 +140,49 @@ export enum MarketSortableField {
Volume = 'VOLUME'
}
export type NftActivity = {
__typename?: 'NftActivity';
address: Scalars['String'];
asset?: Maybe<NftAsset>;
fromAddress: Scalars['String'];
id: Scalars['ID'];
marketplace?: Maybe<NftMarketplace>;
orderStatus?: Maybe<OrderStatus>;
price?: Maybe<Amount>;
quantity?: Maybe<Scalars['Int']>;
timestamp: Scalars['Int'];
toAddress?: Maybe<Scalars['String']>;
tokenId?: Maybe<Scalars['String']>;
transactionHash?: Maybe<Scalars['String']>;
type: NftActivityType;
url?: Maybe<Scalars['String']>;
};
export type NftActivityConnection = {
__typename?: 'NftActivityConnection';
edges: Array<NftActivityEdge>;
pageInfo: PageInfo;
};
export type NftActivityEdge = {
__typename?: 'NftActivityEdge';
cursor: Scalars['String'];
node: NftActivity;
};
export type NftActivityFilterInput = {
activityTypes?: InputMaybe<Array<NftActivityType>>;
address?: InputMaybe<Scalars['String']>;
tokenId?: InputMaybe<Scalars['String']>;
};
export enum NftActivityType {
CancelListing = 'CANCEL_LISTING',
Listing = 'LISTING',
Sale = 'SALE',
Transfer = 'TRANSFER'
}
export type NftApproval = {
__typename?: 'NftApproval';
approvedAddress: Scalars['String'];
@@ -386,6 +430,7 @@ export type NftCollectionTraitStats = {
export type NftCollectionsFilterInput = {
addresses?: InputMaybe<Array<Scalars['String']>>;
nameQuery?: InputMaybe<Scalars['String']>;
};
export type NftContract = IContract & {
@@ -468,12 +513,45 @@ export enum NftRarityProvider {
RaritySniper = 'RARITY_SNIPER'
}
export type NftRouteResponse = {
__typename?: 'NftRouteResponse';
calldata: Scalars['String'];
id: Scalars['ID'];
route?: Maybe<Array<NftTrade>>;
sendAmount: TokenAmount;
toAddress: Scalars['String'];
};
export enum NftStandard {
Erc721 = 'ERC721',
Erc1155 = 'ERC1155',
Noncompliant = 'NONCOMPLIANT'
}
export type NftTrade = {
__typename?: 'NftTrade';
amount: Scalars['Int'];
contractAddress: Scalars['String'];
id: Scalars['ID'];
marketplace: NftMarketplace;
/** price represents the current price of the NFT, which can be different from quotePrice */
price: TokenAmount;
/** quotePrice represents the last quoted price of the NFT */
quotePrice?: Maybe<TokenAmount>;
tokenId: Scalars['String'];
tokenType: NftStandard;
};
export type NftTradeInput = {
amount: Scalars['Int'];
contractAddress: Scalars['String'];
id: Scalars['ID'];
marketplace: NftMarketplace;
quotePrice?: InputMaybe<TokenAmountInput>;
tokenId: Scalars['String'];
tokenType: NftStandard;
};
export type NftTransfer = {
__typename?: 'NftTransfer';
asset: NftAsset;
@@ -504,6 +582,36 @@ export type PageInfo = {
startCursor?: Maybe<Scalars['String']>;
};
/** v2 pool parameters as defined by https://github.com/Uniswap/v2-sdk/blob/main/src/entities/pair.ts */
export type PairInput = {
tokenAmountA: TokenAmountInput;
tokenAmountB: TokenAmountInput;
};
export type PermitDetailsInput = {
amount: Scalars['String'];
expiration: Scalars['String'];
nonce: Scalars['String'];
token: Scalars['String'];
};
export type PermitInput = {
details: PermitDetailsInput;
sigDeadline: Scalars['String'];
signature: Scalars['String'];
spender: Scalars['String'];
};
/** v3 pool parameters as defined by https://github.com/Uniswap/v3-sdk/blob/main/src/entities/pool.ts */
export type PoolInput = {
fee: Scalars['Int'];
liquidity: Scalars['String'];
sqrtRatioX96: Scalars['String'];
tickCurrent: Scalars['String'];
tokenA: TokenInput;
tokenB: TokenInput;
};
export type Portfolio = {
__typename?: 'Portfolio';
assetActivities?: Maybe<Array<Maybe<AssetActivity>>>;
@@ -514,11 +622,11 @@ export type Portfolio = {
tokenBalances?: Maybe<Array<Maybe<TokenBalance>>>;
tokensTotalDenominatedValue?: Maybe<Amount>;
tokensTotalDenominatedValueChange?: Maybe<AmountChange>;
tokensTotalDenominatedValueHistory?: Maybe<Array<Maybe<TimestampedAmount>>>;
};
export type PortfolioAssetActivitiesArgs = {
includeOffChain?: InputMaybe<Scalars['Boolean']>;
page?: InputMaybe<Scalars['Int']>;
pageSize?: InputMaybe<Scalars['Int']>;
};
@@ -528,18 +636,14 @@ export type PortfolioTokensTotalDenominatedValueChangeArgs = {
duration?: InputMaybe<HistoryDuration>;
};
export type PortfolioTokensTotalDenominatedValueHistoryArgs = {
duration?: InputMaybe<HistoryDuration>;
};
export type Query = {
__typename?: 'Query';
assetActivities?: Maybe<Array<Maybe<AssetActivity>>>;
nftActivity?: Maybe<NftActivityConnection>;
nftAssets?: Maybe<NftAssetConnection>;
nftBalances?: Maybe<NftBalanceConnection>;
nftCollections?: Maybe<NftCollectionConnection>;
nftCollectionsById?: Maybe<Array<Maybe<NftCollection>>>;
nftRoute?: Maybe<NftRouteResponse>;
portfolios?: Maybe<Array<Maybe<Portfolio>>>;
searchTokenProjects?: Maybe<Array<Maybe<TokenProject>>>;
searchTokens?: Maybe<Array<Maybe<Token>>>;
@@ -550,10 +654,11 @@ export type Query = {
};
export type QueryAssetActivitiesArgs = {
address: Scalars['String'];
page?: InputMaybe<Scalars['Int']>;
pageSize?: InputMaybe<Scalars['Int']>;
export type QueryNftActivityArgs = {
chain?: InputMaybe<Chain>;
cursor?: InputMaybe<Scalars['String']>;
filter?: InputMaybe<NftActivityFilterInput>;
limit?: InputMaybe<Scalars['Int']>;
};
@@ -574,19 +679,20 @@ export type QueryNftBalancesArgs = {
after?: InputMaybe<Scalars['String']>;
before?: InputMaybe<Scalars['String']>;
chain?: InputMaybe<Chain>;
cursor?: InputMaybe<Scalars['String']>;
filter?: InputMaybe<NftBalancesFilterInput>;
first?: InputMaybe<Scalars['Int']>;
last?: InputMaybe<Scalars['Int']>;
limit?: InputMaybe<Scalars['Int']>;
ownerAddress: Scalars['String'];
};
export type QueryNftCollectionsArgs = {
after?: InputMaybe<Scalars['String']>;
before?: InputMaybe<Scalars['String']>;
chain?: InputMaybe<Chain>;
cursor?: InputMaybe<Scalars['String']>;
filter?: InputMaybe<NftCollectionsFilterInput>;
first?: InputMaybe<Scalars['Int']>;
last?: InputMaybe<Scalars['Int']>;
limit?: InputMaybe<Scalars['Int']>;
};
@@ -595,9 +701,16 @@ export type QueryNftCollectionsByIdArgs = {
};
export type QueryNftRouteArgs = {
chain?: InputMaybe<Chain>;
nftTrades: Array<NftTradeInput>;
senderAddress: Scalars['String'];
tokenTrades?: InputMaybe<Array<TokenTradeInput>>;
};
export type QueryPortfoliosArgs = {
ownerAddresses: Array<Scalars['String']>;
useAltDataSource?: InputMaybe<Scalars['Boolean']>;
};
@@ -667,6 +780,18 @@ export type TokenMarketArgs = {
currency?: InputMaybe<Currency>;
};
export type TokenAmount = {
__typename?: 'TokenAmount';
currency: Currency;
id: Scalars['ID'];
value: Scalars['String'];
};
export type TokenAmountInput = {
amount: Scalars['String'];
token: TokenInput;
};
export type TokenApproval = {
__typename?: 'TokenApproval';
approvedAddress: Scalars['String'];
@@ -689,6 +814,13 @@ export type TokenBalance = {
tokenProjectMarket?: Maybe<TokenProjectMarket>;
};
export type TokenInput = {
address: Scalars['String'];
chainId: Scalars['Int'];
decimals: Scalars['Int'];
isNative: Scalars['Boolean'];
};
export type TokenMarket = {
__typename?: 'TokenMarket';
id: Scalars['ID'];
@@ -747,6 +879,7 @@ export type TokenProjectMarketsArgs = {
export type TokenProjectMarket = {
__typename?: 'TokenProjectMarket';
currency: Currency;
/** @deprecated Use marketCap */
fullyDilutedMarketCap?: Maybe<Amount>;
id: Scalars['ID'];
marketCap?: Maybe<Amount>;
@@ -754,9 +887,12 @@ export type TokenProjectMarket = {
priceHighLow?: Maybe<Amount>;
priceHistory?: Maybe<Array<Maybe<TimestampedAmount>>>;
pricePercentChange?: Maybe<Amount>;
/** @deprecated Use pricePercentChange */
pricePercentChange24h?: Maybe<Amount>;
tokenProject: TokenProject;
/** @deprecated Use TokenMarket.volume for Uniswap volume */
volume?: Maybe<Amount>;
/** @deprecated Use TokenMarket.volume with duration DAY for Uniswap volume */
volume24h?: Maybe<Amount>;
};
@@ -794,6 +930,31 @@ export enum TokenStandard {
Native = 'NATIVE'
}
export type TokenTradeInput = {
permit?: InputMaybe<PermitInput>;
routes?: InputMaybe<TokenTradeRoutesInput>;
slippageToleranceBasisPoints?: InputMaybe<Scalars['Int']>;
tokenAmount: TokenAmountInput;
};
export type TokenTradeRouteInput = {
inputAmount: TokenAmountInput;
outputAmount: TokenAmountInput;
pools: Array<TradePoolInput>;
};
export type TokenTradeRoutesInput = {
mixedRoutes?: InputMaybe<Array<TokenTradeRouteInput>>;
tradeType: TokenTradeType;
v2Routes?: InputMaybe<Array<TokenTradeRouteInput>>;
v3Routes?: InputMaybe<Array<TokenTradeRouteInput>>;
};
export enum TokenTradeType {
ExactInput = 'EXACT_INPUT',
ExactOutput = 'EXACT_OUTPUT'
}
export type TokenTransfer = {
__typename?: 'TokenTransfer';
asset: Token;
@@ -806,6 +967,11 @@ export type TokenTransfer = {
transactedValue?: Maybe<Amount>;
};
export type TradePoolInput = {
pair?: InputMaybe<PairInput>;
pool?: InputMaybe<PoolInput>;
};
export type Transaction = {
__typename?: 'Transaction';
blockNumber: Scalars['Int'];
@@ -831,20 +997,37 @@ export enum TransactionStatus {
Pending = 'PENDING'
}
export type TokenQueryVariables = Exact<{
contract: ContractInput;
export type RecentlySearchedAssetsQueryVariables = Exact<{
collectionAddresses: Array<Scalars['String']> | Scalars['String'];
contracts: Array<ContractInput> | ContractInput;
}>;
export type TokenQuery = { __typename?: 'Query', tokens?: Array<{ __typename?: 'Token', id: string, decimals?: number, name?: string, chain: Chain, address?: string, symbol?: string, market?: { __typename?: 'TokenMarket', totalValueLocked?: { __typename?: 'Amount', value: number, currency?: Currency }, price?: { __typename?: 'Amount', value: number, currency?: Currency }, volume24H?: { __typename?: 'Amount', value: number, currency?: Currency }, priceHigh52W?: { __typename?: 'Amount', value: number }, priceLow52W?: { __typename?: 'Amount', value: number } }, project?: { __typename?: 'TokenProject', description?: string, homepageUrl?: string, twitterName?: string, logoUrl?: string, tokens: Array<{ __typename?: 'Token', chain: Chain, address?: string }> } }> };
export type RecentlySearchedAssetsQuery = { __typename?: 'Query', nftCollections?: { __typename?: 'NftCollectionConnection', edges: Array<{ __typename?: 'NftCollectionEdge', node: { __typename?: 'NftCollection', collectionId: string, isVerified?: boolean, name?: string, numAssets?: number, image?: { __typename?: 'Image', url: string }, nftContracts?: Array<{ __typename?: 'NftContract', address: string }>, markets?: Array<{ __typename?: 'NftCollectionMarket', floorPrice?: { __typename?: 'TimestampedAmount', currency?: Currency, value: number } }> } }> }, tokens?: Array<{ __typename?: 'Token', id: string, decimals?: number, name?: string, chain: Chain, standard?: TokenStandard, address?: string, symbol?: string, market?: { __typename?: 'TokenMarket', id: string, price?: { __typename?: 'Amount', id: string, value: number, currency?: Currency }, pricePercentChange?: { __typename?: 'Amount', id: string, value: number }, volume24H?: { __typename?: 'Amount', id: string, value: number, currency?: Currency } }, project?: { __typename?: 'TokenProject', id: string, logoUrl?: string, safetyLevel?: SafetyLevel } }> };
export type SearchTokensQueryVariables = Exact<{
searchQuery: Scalars['String'];
}>;
export type SearchTokensQuery = { __typename?: 'Query', searchTokens?: Array<{ __typename?: 'Token', id: string, decimals?: number, name?: string, chain: Chain, standard?: TokenStandard, address?: string, symbol?: string, market?: { __typename?: 'TokenMarket', id: string, price?: { __typename?: 'Amount', id: string, value: number, currency?: Currency }, pricePercentChange?: { __typename?: 'Amount', id: string, value: number }, volume24H?: { __typename?: 'Amount', id: string, value: number, currency?: Currency } }, project?: { __typename?: 'TokenProject', id: string, logoUrl?: string, safetyLevel?: SafetyLevel } }> };
export type TokenQueryVariables = Exact<{
chain: Chain;
address?: InputMaybe<Scalars['String']>;
}>;
export type TokenQuery = { __typename?: 'Query', token?: { __typename?: 'Token', id: string, decimals?: number, name?: string, chain: Chain, address?: string, symbol?: string, standard?: TokenStandard, market?: { __typename?: 'TokenMarket', id: string, totalValueLocked?: { __typename?: 'Amount', id: string, value: number, currency?: Currency }, price?: { __typename?: 'Amount', id: string, value: number, currency?: Currency }, volume24H?: { __typename?: 'Amount', id: string, value: number, currency?: Currency }, priceHigh52W?: { __typename?: 'Amount', id: string, value: number }, priceLow52W?: { __typename?: 'Amount', id: string, value: number } }, project?: { __typename?: 'TokenProject', id: string, description?: string, homepageUrl?: string, twitterName?: string, logoUrl?: string, tokens: Array<{ __typename?: 'Token', id: string, chain: Chain, address?: string }> } } };
export type TokenPriceQueryVariables = Exact<{
contract: ContractInput;
chain: Chain;
address?: InputMaybe<Scalars['String']>;
duration: HistoryDuration;
}>;
export type TokenPriceQuery = { __typename?: 'Query', tokens?: Array<{ __typename?: 'Token', market?: { __typename?: 'TokenMarket', price?: { __typename?: 'Amount', value: number }, priceHistory?: Array<{ __typename?: 'TimestampedAmount', timestamp: number, value: number }> } }> };
export type TokenPriceQuery = { __typename?: 'Query', token?: { __typename?: 'Token', id: string, address?: string, chain: Chain, market?: { __typename?: 'TokenMarket', id: string, price?: { __typename?: 'Amount', id: string, value: number }, priceHistory?: Array<{ __typename?: 'TimestampedAmount', id: string, timestamp: number, value: number }> } } };
export type TopTokens100QueryVariables = Exact<{
duration: HistoryDuration;
@@ -852,7 +1035,7 @@ export type TopTokens100QueryVariables = Exact<{
}>;
export type TopTokens100Query = { __typename?: 'Query', topTokens?: Array<{ __typename?: 'Token', id: string, name?: string, chain: Chain, address?: string, symbol?: string, market?: { __typename?: 'TokenMarket', totalValueLocked?: { __typename?: 'Amount', value: number, currency?: Currency }, price?: { __typename?: 'Amount', value: number, currency?: Currency }, pricePercentChange?: { __typename?: 'Amount', currency?: Currency, value: number }, volume?: { __typename?: 'Amount', value: number, currency?: Currency } }, project?: { __typename?: 'TokenProject', logoUrl?: string } }> };
export type TopTokens100Query = { __typename?: 'Query', topTokens?: Array<{ __typename?: 'Token', id: string, name?: string, chain: Chain, address?: string, symbol?: string, standard?: TokenStandard, market?: { __typename?: 'TokenMarket', id: string, totalValueLocked?: { __typename?: 'Amount', id: string, value: number, currency?: Currency }, price?: { __typename?: 'Amount', id: string, value: number, currency?: Currency }, pricePercentChange?: { __typename?: 'Amount', id: string, currency?: Currency, value: number }, volume?: { __typename?: 'Amount', id: string, value: number, currency?: Currency } }, project?: { __typename?: 'TokenProject', id: string, logoUrl?: string } }> };
export type TopTokensSparklineQueryVariables = Exact<{
duration: HistoryDuration;
@@ -860,7 +1043,14 @@ export type TopTokensSparklineQueryVariables = Exact<{
}>;
export type TopTokensSparklineQuery = { __typename?: 'Query', topTokens?: Array<{ __typename?: 'Token', address?: string, market?: { __typename?: 'TokenMarket', priceHistory?: Array<{ __typename?: 'TimestampedAmount', timestamp: number, value: number }> } }> };
export type TopTokensSparklineQuery = { __typename?: 'Query', topTokens?: Array<{ __typename?: 'Token', id: string, address?: string, chain: Chain, market?: { __typename?: 'TokenMarket', id: string, priceHistory?: Array<{ __typename?: 'TimestampedAmount', id: string, timestamp: number, value: number }> } }> };
export type TrendingTokensQueryVariables = Exact<{
chain: Chain;
}>;
export type TrendingTokensQuery = { __typename?: 'Query', topTokens?: Array<{ __typename?: 'Token', id: string, decimals?: number, name?: string, chain: Chain, standard?: TokenStandard, address?: string, symbol?: string, market?: { __typename?: 'TokenMarket', id: string, price?: { __typename?: 'Amount', id: string, value: number, currency?: Currency }, pricePercentChange?: { __typename?: 'Amount', id: string, value: number }, volume24H?: { __typename?: 'Amount', id: string, value: number, currency?: Currency } }, project?: { __typename?: 'TokenProject', id: string, logoUrl?: string, safetyLevel?: SafetyLevel } }> };
export type AssetQueryVariables = Exact<{
address: Scalars['String'];
@@ -901,44 +1091,212 @@ export type NftBalanceQueryVariables = Exact<{
}>;
export type NftBalanceQuery = { __typename?: 'Query', nftBalances?: { __typename?: 'NftBalanceConnection', edges: Array<{ __typename?: 'NftBalanceEdge', node: { __typename?: 'NftBalance', listedMarketplaces?: Array<NftMarketplace>, ownedAsset?: { __typename?: 'NftAsset', id: string, animationUrl?: string, description?: string, flaggedBy?: string, name?: string, ownerAddress?: string, suspiciousFlag?: boolean, tokenId: string, collection?: { __typename?: 'NftCollection', isVerified?: boolean, name?: string, image?: { __typename?: 'Image', url: string }, nftContracts?: Array<{ __typename?: 'NftContract', address: string, chain: Chain, name?: string, standard?: NftStandard, symbol?: string, totalSupply?: number }>, markets?: Array<{ __typename?: 'NftCollectionMarket', floorPrice?: { __typename?: 'TimestampedAmount', value: number } }> }, image?: { __typename?: 'Image', url: string }, originalImage?: { __typename?: 'Image', url: string }, smallImage?: { __typename?: 'Image', url: string }, thumbnail?: { __typename?: 'Image', url: string }, listings?: { __typename?: 'NftOrderConnection', edges: Array<{ __typename?: 'NftOrderEdge', node: { __typename?: 'NftOrder', createdAt: number, marketplace: NftMarketplace, endAt?: number, price: { __typename?: 'Amount', value: number, currency?: Currency } } }> } }, listingFees?: Array<{ __typename?: 'NftFee', payoutAddress: string, basisPoints: number }>, lastPrice?: { __typename?: 'TimestampedAmount', currency?: Currency, timestamp: number, value: number } } }>, pageInfo: { __typename?: 'PageInfo', endCursor?: string, hasNextPage?: boolean, hasPreviousPage?: boolean, startCursor?: string } } };
export type NftBalanceQuery = { __typename?: 'Query', nftBalances?: { __typename?: 'NftBalanceConnection', edges: Array<{ __typename?: 'NftBalanceEdge', node: { __typename?: 'NftBalance', listedMarketplaces?: Array<NftMarketplace>, ownedAsset?: { __typename?: 'NftAsset', id: string, animationUrl?: string, description?: string, flaggedBy?: string, name?: string, ownerAddress?: string, suspiciousFlag?: boolean, tokenId: string, collection?: { __typename?: 'NftCollection', isVerified?: boolean, name?: string, twitterName?: string, image?: { __typename?: 'Image', url: string }, nftContracts?: Array<{ __typename?: 'NftContract', address: string, chain: Chain, name?: string, standard?: NftStandard, symbol?: string, totalSupply?: number }>, markets?: Array<{ __typename?: 'NftCollectionMarket', floorPrice?: { __typename?: 'TimestampedAmount', value: number } }> }, image?: { __typename?: 'Image', url: string }, originalImage?: { __typename?: 'Image', url: string }, smallImage?: { __typename?: 'Image', url: string }, thumbnail?: { __typename?: 'Image', url: string }, listings?: { __typename?: 'NftOrderConnection', edges: Array<{ __typename?: 'NftOrderEdge', node: { __typename?: 'NftOrder', createdAt: number, marketplace: NftMarketplace, endAt?: number, price: { __typename?: 'Amount', value: number, currency?: Currency } } }> } }, listingFees?: Array<{ __typename?: 'NftFee', payoutAddress: string, basisPoints: number }>, lastPrice?: { __typename?: 'TimestampedAmount', currency?: Currency, timestamp: number, value: number } } }>, pageInfo: { __typename?: 'PageInfo', endCursor?: string, hasNextPage?: boolean, hasPreviousPage?: boolean, startCursor?: string } } };
export type NftRouteQueryVariables = Exact<{
chain?: InputMaybe<Chain>;
senderAddress: Scalars['String'];
nftTrades: Array<NftTradeInput> | NftTradeInput;
tokenTrades?: InputMaybe<Array<TokenTradeInput> | TokenTradeInput>;
}>;
export type NftRouteQuery = { __typename?: 'Query', nftRoute?: { __typename?: 'NftRouteResponse', id: string, calldata: string, toAddress: string, route?: Array<{ __typename?: 'NftTrade', amount: number, contractAddress: string, id: string, marketplace: NftMarketplace, tokenId: string, tokenType: NftStandard, price: { __typename?: 'TokenAmount', id: string, currency: Currency, value: string }, quotePrice?: { __typename?: 'TokenAmount', id: string, currency: Currency, value: string } }>, sendAmount: { __typename?: 'TokenAmount', id: string, currency: Currency, value: string } } };
export const RecentlySearchedAssetsDocument = gql`
query RecentlySearchedAssets($collectionAddresses: [String!]!, $contracts: [ContractInput!]!) {
nftCollections(filter: {addresses: $collectionAddresses}) {
edges {
node {
collectionId
image {
url
}
isVerified
name
numAssets
nftContracts {
address
}
markets(currencies: ETH) {
floorPrice {
currency
value
}
}
}
}
}
tokens(contracts: $contracts) {
id
decimals
name
chain
standard
address
symbol
market(currency: USD) {
id
price {
id
value
currency
}
pricePercentChange(duration: DAY) {
id
value
}
volume24H: volume(duration: DAY) {
id
value
currency
}
}
project {
id
logoUrl
safetyLevel
}
}
}
`;
/**
* __useRecentlySearchedAssetsQuery__
*
* To run a query within a React component, call `useRecentlySearchedAssetsQuery` and pass it any options that fit your needs.
* When your component renders, `useRecentlySearchedAssetsQuery` returns an object from Apollo Client that contains loading, error, and data properties
* you can use to render your UI.
*
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
*
* @example
* const { data, loading, error } = useRecentlySearchedAssetsQuery({
* variables: {
* collectionAddresses: // value for 'collectionAddresses'
* contracts: // value for 'contracts'
* },
* });
*/
export function useRecentlySearchedAssetsQuery(baseOptions: Apollo.QueryHookOptions<RecentlySearchedAssetsQuery, RecentlySearchedAssetsQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useQuery<RecentlySearchedAssetsQuery, RecentlySearchedAssetsQueryVariables>(RecentlySearchedAssetsDocument, options);
}
export function useRecentlySearchedAssetsLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<RecentlySearchedAssetsQuery, RecentlySearchedAssetsQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useLazyQuery<RecentlySearchedAssetsQuery, RecentlySearchedAssetsQueryVariables>(RecentlySearchedAssetsDocument, options);
}
export type RecentlySearchedAssetsQueryHookResult = ReturnType<typeof useRecentlySearchedAssetsQuery>;
export type RecentlySearchedAssetsLazyQueryHookResult = ReturnType<typeof useRecentlySearchedAssetsLazyQuery>;
export type RecentlySearchedAssetsQueryResult = Apollo.QueryResult<RecentlySearchedAssetsQuery, RecentlySearchedAssetsQueryVariables>;
export const SearchTokensDocument = gql`
query SearchTokens($searchQuery: String!) {
searchTokens(searchQuery: $searchQuery) {
id
decimals
name
chain
standard
address
symbol
market(currency: USD) {
id
price {
id
value
currency
}
pricePercentChange(duration: DAY) {
id
value
}
volume24H: volume(duration: DAY) {
id
value
currency
}
}
project {
id
logoUrl
safetyLevel
}
}
}
`;
/**
* __useSearchTokensQuery__
*
* To run a query within a React component, call `useSearchTokensQuery` and pass it any options that fit your needs.
* When your component renders, `useSearchTokensQuery` returns an object from Apollo Client that contains loading, error, and data properties
* you can use to render your UI.
*
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
*
* @example
* const { data, loading, error } = useSearchTokensQuery({
* variables: {
* searchQuery: // value for 'searchQuery'
* },
* });
*/
export function useSearchTokensQuery(baseOptions: Apollo.QueryHookOptions<SearchTokensQuery, SearchTokensQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useQuery<SearchTokensQuery, SearchTokensQueryVariables>(SearchTokensDocument, options);
}
export function useSearchTokensLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<SearchTokensQuery, SearchTokensQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useLazyQuery<SearchTokensQuery, SearchTokensQueryVariables>(SearchTokensDocument, options);
}
export type SearchTokensQueryHookResult = ReturnType<typeof useSearchTokensQuery>;
export type SearchTokensLazyQueryHookResult = ReturnType<typeof useSearchTokensLazyQuery>;
export type SearchTokensQueryResult = Apollo.QueryResult<SearchTokensQuery, SearchTokensQueryVariables>;
export const TokenDocument = gql`
query Token($contract: ContractInput!) {
tokens(contracts: [$contract]) {
query Token($chain: Chain!, $address: String = null) {
token(chain: $chain, address: $address) {
id
decimals
name
chain
address
symbol
standard
market(currency: USD) {
id
totalValueLocked {
id
value
currency
}
price {
id
value
currency
}
volume24H: volume(duration: DAY) {
id
value
currency
}
priceHigh52W: priceHighLow(duration: YEAR, highLow: HIGH) {
id
value
}
priceLow52W: priceHighLow(duration: YEAR, highLow: LOW) {
id
value
}
}
project {
id
description
homepageUrl
twitterName
logoUrl
tokens {
id
chain
address
}
@@ -959,7 +1317,8 @@ export const TokenDocument = gql`
* @example
* const { data, loading, error } = useTokenQuery({
* variables: {
* contract: // value for 'contract'
* chain: // value for 'chain'
* address: // value for 'address'
* },
* });
*/
@@ -975,13 +1334,19 @@ export type TokenQueryHookResult = ReturnType<typeof useTokenQuery>;
export type TokenLazyQueryHookResult = ReturnType<typeof useTokenLazyQuery>;
export type TokenQueryResult = Apollo.QueryResult<TokenQuery, TokenQueryVariables>;
export const TokenPriceDocument = gql`
query TokenPrice($contract: ContractInput!, $duration: HistoryDuration!) {
tokens(contracts: [$contract]) {
query TokenPrice($chain: Chain!, $address: String = null, $duration: HistoryDuration!) {
token(chain: $chain, address: $address) {
id
address
chain
market(currency: USD) {
id
price {
id
value
}
priceHistory(duration: $duration) {
id
timestamp
value
}
@@ -1002,7 +1367,8 @@ export const TokenPriceDocument = gql`
* @example
* const { data, loading, error } = useTokenPriceQuery({
* variables: {
* contract: // value for 'contract'
* chain: // value for 'chain'
* address: // value for 'address'
* duration: // value for 'duration'
* },
* });
@@ -1026,25 +1392,32 @@ export const TopTokens100Document = gql`
chain
address
symbol
standard
market(currency: USD) {
id
totalValueLocked {
id
value
currency
}
price {
id
value
currency
}
pricePercentChange(duration: $duration) {
id
currency
value
}
volume(duration: $duration) {
id
value
currency
}
}
project {
id
logoUrl
}
}
@@ -1082,9 +1455,13 @@ export type TopTokens100QueryResult = Apollo.QueryResult<TopTokens100Query, TopT
export const TopTokensSparklineDocument = gql`
query TopTokensSparkline($duration: HistoryDuration!, $chain: Chain!) {
topTokens(pageSize: 100, page: 1, chain: $chain) {
id
address
chain
market(currency: USD) {
id
priceHistory(duration: $duration) {
id
timestamp
value
}
@@ -1121,6 +1498,69 @@ export function useTopTokensSparklineLazyQuery(baseOptions?: Apollo.LazyQueryHoo
export type TopTokensSparklineQueryHookResult = ReturnType<typeof useTopTokensSparklineQuery>;
export type TopTokensSparklineLazyQueryHookResult = ReturnType<typeof useTopTokensSparklineLazyQuery>;
export type TopTokensSparklineQueryResult = Apollo.QueryResult<TopTokensSparklineQuery, TopTokensSparklineQueryVariables>;
export const TrendingTokensDocument = gql`
query TrendingTokens($chain: Chain!) {
topTokens(pageSize: 4, page: 1, chain: $chain, orderBy: VOLUME) {
id
decimals
name
chain
standard
address
symbol
market(currency: USD) {
id
price {
id
value
currency
}
pricePercentChange(duration: DAY) {
id
value
}
volume24H: volume(duration: DAY) {
id
value
currency
}
}
project {
id
logoUrl
safetyLevel
}
}
}
`;
/**
* __useTrendingTokensQuery__
*
* To run a query within a React component, call `useTrendingTokensQuery` and pass it any options that fit your needs.
* When your component renders, `useTrendingTokensQuery` returns an object from Apollo Client that contains loading, error, and data properties
* you can use to render your UI.
*
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
*
* @example
* const { data, loading, error } = useTrendingTokensQuery({
* variables: {
* chain: // value for 'chain'
* },
* });
*/
export function useTrendingTokensQuery(baseOptions: Apollo.QueryHookOptions<TrendingTokensQuery, TrendingTokensQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useQuery<TrendingTokensQuery, TrendingTokensQueryVariables>(TrendingTokensDocument, options);
}
export function useTrendingTokensLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<TrendingTokensQuery, TrendingTokensQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useLazyQuery<TrendingTokensQuery, TrendingTokensQueryVariables>(TrendingTokensDocument, options);
}
export type TrendingTokensQueryHookResult = ReturnType<typeof useTrendingTokensQuery>;
export type TrendingTokensLazyQueryHookResult = ReturnType<typeof useTrendingTokensLazyQuery>;
export type TrendingTokensQueryResult = Apollo.QueryResult<TrendingTokensQuery, TrendingTokensQueryVariables>;
export const AssetDocument = gql`
query Asset($address: String!, $orderBy: NftAssetSortableField, $asc: Boolean, $filter: NftAssetsFilterInput, $first: Int, $after: String, $last: Int, $before: String) {
nftAssets(
@@ -1493,6 +1933,7 @@ export const NftBalanceDocument = gql`
url
}
name
twitterName
nftContracts {
address
chain
@@ -1592,4 +2033,72 @@ export function useNftBalanceLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions
}
export type NftBalanceQueryHookResult = ReturnType<typeof useNftBalanceQuery>;
export type NftBalanceLazyQueryHookResult = ReturnType<typeof useNftBalanceLazyQuery>;
export type NftBalanceQueryResult = Apollo.QueryResult<NftBalanceQuery, NftBalanceQueryVariables>;
export type NftBalanceQueryResult = Apollo.QueryResult<NftBalanceQuery, NftBalanceQueryVariables>;
export const NftRouteDocument = gql`
query NftRoute($chain: Chain = ETHEREUM, $senderAddress: String!, $nftTrades: [NftTradeInput!]!, $tokenTrades: [TokenTradeInput!]) {
nftRoute(
chain: $chain
senderAddress: $senderAddress
nftTrades: $nftTrades
tokenTrades: $tokenTrades
) {
id
calldata
route {
amount
contractAddress
id
marketplace
price {
id
currency
value
}
quotePrice {
id
currency
value
}
tokenId
tokenType
}
sendAmount {
id
currency
value
}
toAddress
}
}
`;
/**
* __useNftRouteQuery__
*
* To run a query within a React component, call `useNftRouteQuery` and pass it any options that fit your needs.
* When your component renders, `useNftRouteQuery` returns an object from Apollo Client that contains loading, error, and data properties
* you can use to render your UI.
*
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
*
* @example
* const { data, loading, error } = useNftRouteQuery({
* variables: {
* chain: // value for 'chain'
* senderAddress: // value for 'senderAddress'
* nftTrades: // value for 'nftTrades'
* tokenTrades: // value for 'tokenTrades'
* },
* });
*/
export function useNftRouteQuery(baseOptions: Apollo.QueryHookOptions<NftRouteQuery, NftRouteQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useQuery<NftRouteQuery, NftRouteQueryVariables>(NftRouteDocument, options);
}
export function useNftRouteLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<NftRouteQuery, NftRouteQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useLazyQuery<NftRouteQuery, NftRouteQueryVariables>(NftRouteDocument, options);
}
export type NftRouteQueryHookResult = ReturnType<typeof useNftRouteQuery>;
export type NftRouteLazyQueryHookResult = ReturnType<typeof useNftRouteLazyQuery>;
export type NftRouteQueryResult = Apollo.QueryResult<NftRouteQuery, NftRouteQueryVariables>;

View File

@@ -1,5 +1,5 @@
import { ApolloClient, InMemoryCache } from '@apollo/client'
import { relayStylePagination } from '@apollo/client/utilities'
import { Reference, relayStylePagination } from '@apollo/client/utilities'
const GRAPHQL_URL = process.env.REACT_APP_AWS_API_ENDPOINT
if (!GRAPHQL_URL) {
@@ -7,6 +7,7 @@ if (!GRAPHQL_URL) {
}
export const apolloClient = new ApolloClient({
connectToDevTools: true,
uri: GRAPHQL_URL,
headers: {
'Content-Type': 'application/json',
@@ -18,6 +19,33 @@ export const apolloClient = new ApolloClient({
fields: {
nftBalances: relayStylePagination(),
nftAssets: relayStylePagination(),
// tell apollo client how to reference Token items in the cache after being fetched by queries that return Token[]
token: {
read(_, { args, toReference }): Reference | undefined {
return toReference({
__typename: 'Token',
chain: args?.chain,
address: args?.address,
})
},
},
},
},
Token: {
// key by chain, address combination so that Token(chain, address) endpoint can read from cache
/**
* NOTE: In any query for `token` or `tokens`, you must include the `chain` and `address` fields
* in order for result to normalize properly in the cache.
*/
keyFields: ['chain', 'address'],
fields: {
address: {
read(address: string | null): string | null {
// backend endpoint sometimes returns checksummed, sometimes lowercased addresses
// always use lowercased addresses in our app for consistency
return address?.toLowerCase() ?? null
},
},
},
},
},

View File

@@ -34,6 +34,7 @@ gql`
url
}
name
twitterName
nftContracts {
address
chain
@@ -166,7 +167,12 @@ export function useNftBalance(
image_url: asset?.collection?.image?.url,
payout_address: queryAsset?.node?.listingFees?.[0]?.payoutAddress,
},
collection: asset?.collection as unknown as GenieCollection,
collection: {
name: asset.collection?.name,
isVerified: asset.collection?.isVerified,
imageUrl: asset.collection?.image?.url,
twitterUrl: asset.collection?.twitterName ? `@${asset.collection?.twitterName}` : undefined,
} as GenieCollection,
collectionIsVerified: asset?.collection?.isVerified,
lastPrice: queryAsset.node.lastPrice?.value,
floorPrice: asset?.collection?.markets?.[0]?.floorPrice?.value,

View File

@@ -0,0 +1,39 @@
import gql from 'graphql-tag'
gql`
query NftRoute(
$chain: Chain = ETHEREUM
$senderAddress: String!
$nftTrades: [NftTradeInput!]!
$tokenTrades: [TokenTradeInput!]
) {
nftRoute(chain: $chain, senderAddress: $senderAddress, nftTrades: $nftTrades, tokenTrades: $tokenTrades) {
id
calldata
route {
amount
contractAddress
id
marketplace
price {
id
currency
value
}
quotePrice {
id
currency
value
}
tokenId
tokenType
}
sendAmount {
id
currency
value
}
toAddress
}
}
`

View File

@@ -1,9 +1,30 @@
import { QueryResult } from '@apollo/client'
import { SupportedChainId } from 'constants/chains'
import { ZERO_ADDRESS } from 'constants/misc'
import { NATIVE_CHAIN_ID, nativeOnChain, WRAPPED_NATIVE_CURRENCY } from 'constants/tokens'
import ms from 'ms.macro'
import { useEffect } from 'react'
import { Chain, HistoryDuration } from './__generated__/types-and-hooks'
export enum PollingInterval {
Slow = ms`5m`,
Normal = ms`1m`,
Fast = ms`12s`, // 12 seconds, block times for mainnet
LightningMcQueen = ms`3s`, // 3 seconds, approx block times for polygon
}
// Polls a query only when the current component is mounted, as useQuery's pollInterval prop will continue to poll after unmount
export function usePollQueryWhileMounted<T, K>(queryResult: QueryResult<T, K>, interval: PollingInterval) {
const { startPolling, stopPolling } = queryResult
useEffect(() => {
startPolling(interval)
return stopPolling
}, [interval, startPolling, stopPolling])
return queryResult
}
export enum TimePeriod {
HOUR,
DAY,
@@ -74,17 +95,8 @@ export const CHAIN_NAME_TO_CHAIN_ID: { [key: string]: SupportedChainId } = {
export const BACKEND_CHAIN_NAMES: Chain[] = [Chain.Ethereum, Chain.Polygon, Chain.Optimism, Chain.Arbitrum, Chain.Celo]
export function getTokenDetailsURL(address: string, chainName?: Chain, chainId?: number) {
if (address === ZERO_ADDRESS && chainId && chainId === SupportedChainId.MAINNET) {
return `/tokens/${CHAIN_ID_TO_BACKEND_NAME[chainId].toLowerCase()}/${NATIVE_CHAIN_ID}`
} else if (chainName) {
return `/tokens/${chainName.toLowerCase()}/${address}`
} else if (chainId) {
const chainName = CHAIN_ID_TO_BACKEND_NAME[chainId]
return chainName ? `/tokens/${chainName.toLowerCase()}/${address}` : ''
} else {
return ''
}
export function getTokenDetailsURL({ address, chain }: { address?: string | null; chain: Chain }) {
return `/tokens/${chain.toLowerCase()}/${address ?? NATIVE_CHAIN_ID}`
}
export function unwrapToken<

View File

@@ -24,7 +24,7 @@ export default function useAllV3TicksQuery(poolAddress: string | undefined, skip
data,
loading: isLoading,
error,
} = useQuery(query, {
} = useQuery<Record<'ticks', Ticks>>(query, {
variables: {
poolAddress: poolAddress?.toLowerCase(),
skip,

File diff suppressed because it is too large Load Diff

View File

@@ -9,30 +9,25 @@ import { useAppDispatch } from 'state/hooks'
import { fetchTokenList } from '../state/lists/actions'
export function useFetchListCallback(): (
listUrl: string,
sendDispatch?: boolean,
skipValidation?: boolean
) => Promise<TokenList> {
export function useFetchListCallback(): (listUrl: string, skipValidation?: boolean) => Promise<TokenList> {
const dispatch = useAppDispatch()
// note: prevent dispatch if using for list search or unsupported list
return useCallback(
async (listUrl: string, sendDispatch = true, skipValidation?: boolean) => {
async (listUrl: string, skipValidation?: boolean) => {
const requestId = nanoid()
sendDispatch && dispatch(fetchTokenList.pending({ requestId, url: listUrl }))
dispatch(fetchTokenList.pending({ requestId, url: listUrl }))
return getTokenList(
listUrl,
(ensName: string) => resolveENSContentHash(ensName, RPC_PROVIDERS[SupportedChainId.MAINNET]),
skipValidation
)
.then((tokenList) => {
sendDispatch && dispatch(fetchTokenList.fulfilled({ url: listUrl, tokenList, requestId }))
dispatch(fetchTokenList.fulfilled({ url: listUrl, tokenList, requestId }))
return tokenList
})
.catch((error) => {
console.debug(`Failed to get list at url ${listUrl}`, error)
sendDispatch && dispatch(fetchTokenList.rejected({ url: listUrl, requestId, errorMessage: error.message }))
dispatch(fetchTokenList.rejected({ url: listUrl, requestId, errorMessage: error.message }))
throw error
})
},

View File

@@ -1,140 +0,0 @@
import { ContractTransaction } from '@ethersproject/contracts'
import { PERMIT2_ADDRESS } from '@uniswap/permit2-sdk'
import { CurrencyAmount, Token } from '@uniswap/sdk-core'
import { useWeb3React } from '@web3-react/core'
import { AVERAGE_L1_BLOCK_TIME } from 'constants/chainInfo'
import useInterval from 'lib/hooks/useInterval'
import { useCallback, useEffect, useMemo, useState } from 'react'
import { useHasPendingApproval } from 'state/transactions/hooks'
import { ApproveTransactionInfo } from 'state/transactions/types'
import { PermitSignature, usePermitAllowance, useUpdatePermitAllowance } from './usePermitAllowance'
import { useTokenAllowance, useUpdateTokenAllowance } from './useTokenAllowance'
enum SyncState {
PENDING,
SYNCING,
SYNCED,
}
export enum PermitState {
INVALID,
LOADING,
APPROVAL_OR_PERMIT_NEEDED,
APPROVAL_LOADING,
APPROVED_AND_PERMITTED,
}
export interface Permit {
state: PermitState
signature?: PermitSignature
callback?: () => Promise<{
response: ContractTransaction
info: ApproveTransactionInfo
} | void>
}
export default function usePermit(amount?: CurrencyAmount<Token>, spender?: string): Permit {
const { account } = useWeb3React()
const { tokenAllowance, isSyncing: isApprovalSyncing } = useTokenAllowance(amount?.currency, account, PERMIT2_ADDRESS)
const updateTokenAllowance = useUpdateTokenAllowance(amount, PERMIT2_ADDRESS)
const isAllowed = useMemo(
() => amount && (tokenAllowance?.greaterThan(amount) || tokenAllowance?.equalTo(amount)),
[amount, tokenAllowance]
)
const permitAllowance = usePermitAllowance(amount?.currency, spender)
const [permitAllowanceAmount, setPermitAllowanceAmount] = useState(permitAllowance?.amount)
useEffect(() => setPermitAllowanceAmount(permitAllowance?.amount), [permitAllowance?.amount])
const isPermitted = useMemo(
() => amount && permitAllowanceAmount?.gte(amount.quotient.toString()),
[amount, permitAllowanceAmount]
)
const [signature, setSignature] = useState<PermitSignature>()
const updatePermitAllowance = useUpdatePermitAllowance(
amount?.currency,
spender,
permitAllowance?.nonce,
setSignature
)
const isSigned = useMemo(
() => amount && signature?.details.token === amount?.currency.address && signature?.spender === spender,
[amount, signature?.details.token, signature?.spender, spender]
)
// Trigger a re-render if either tokenAllowance or signature expire.
useInterval(
() => {
// Calculate now such that the signature will still be valid for the next block.
const now = (Date.now() - AVERAGE_L1_BLOCK_TIME) / 1000
if (signature && signature.sigDeadline < now) {
setSignature(undefined)
}
if (permitAllowance && permitAllowance.expiration < now) {
setPermitAllowanceAmount(undefined)
}
},
AVERAGE_L1_BLOCK_TIME,
true
)
// Permit2 should be marked syncing from the time approval is submitted (pending) until it is
// synced in tokenAllowance, to avoid re-prompting the user for an already-submitted approval.
const [syncState, setSyncState] = useState(SyncState.SYNCED)
const isApprovalLoading = syncState !== SyncState.SYNCED
const hasPendingApproval = useHasPendingApproval(amount?.currency, PERMIT2_ADDRESS)
useEffect(() => {
if (hasPendingApproval) {
setSyncState(SyncState.PENDING)
} else {
setSyncState((state) => {
if (state === SyncState.PENDING && isApprovalSyncing) {
return SyncState.SYNCING
} else if (state === SyncState.SYNCING && !isApprovalSyncing) {
return SyncState.SYNCED
} else {
return state
}
})
}
}, [hasPendingApproval, isApprovalSyncing])
const callback = useCallback(async () => {
let info
if (!isAllowed && !hasPendingApproval) {
info = await updateTokenAllowance()
}
if (!isPermitted && !isSigned) {
await updatePermitAllowance()
}
return info
}, [hasPendingApproval, isAllowed, isPermitted, isSigned, updatePermitAllowance, updateTokenAllowance])
return useMemo(() => {
if (!amount) {
return { state: PermitState.INVALID }
} else if (!tokenAllowance || !permitAllowance) {
return { state: PermitState.LOADING }
} else if (!(isPermitted || isSigned)) {
return { state: PermitState.APPROVAL_OR_PERMIT_NEEDED, callback }
} else if (!isAllowed) {
return {
state: isApprovalLoading ? PermitState.APPROVAL_LOADING : PermitState.APPROVAL_OR_PERMIT_NEEDED,
callback,
}
} else {
return { state: PermitState.APPROVED_AND_PERMITTED, signature: isPermitted ? undefined : signature }
}
}, [
amount,
callback,
isAllowed,
isApprovalLoading,
isPermitted,
isSigned,
permitAllowance,
signature,
tokenAllowance,
])
}

View File

@@ -0,0 +1,126 @@
import { PERMIT2_ADDRESS } from '@uniswap/permit2-sdk'
import { CurrencyAmount, Token } from '@uniswap/sdk-core'
import { useWeb3React } from '@web3-react/core'
import { AVERAGE_L1_BLOCK_TIME } from 'constants/chainInfo'
import { PermitSignature, usePermitAllowance, useUpdatePermitAllowance } from 'hooks/usePermitAllowance'
import { useTokenAllowance, useUpdateTokenAllowance } from 'hooks/useTokenAllowance'
import useInterval from 'lib/hooks/useInterval'
import { useCallback, useEffect, useMemo, useState } from 'react'
import { useHasPendingApproval, useTransactionAdder } from 'state/transactions/hooks'
enum ApprovalState {
PENDING,
SYNCING,
SYNCED,
}
export enum AllowanceState {
LOADING,
REQUIRED,
ALLOWED,
}
interface AllowanceRequired {
state: AllowanceState.REQUIRED
token: Token
isApprovalLoading: boolean
approveAndPermit: () => Promise<void>
}
export type Allowance =
| { state: AllowanceState.LOADING }
| {
state: AllowanceState.ALLOWED
permitSignature?: PermitSignature
}
| AllowanceRequired
export default function usePermit2Allowance(amount?: CurrencyAmount<Token>, spender?: string): Allowance {
const { account } = useWeb3React()
const token = amount?.currency
const { tokenAllowance, isSyncing: isApprovalSyncing } = useTokenAllowance(token, account, PERMIT2_ADDRESS)
const updateTokenAllowance = useUpdateTokenAllowance(amount, PERMIT2_ADDRESS)
const isApproved = useMemo(() => {
if (!amount || !tokenAllowance) return false
return tokenAllowance.greaterThan(amount) || tokenAllowance.equalTo(amount)
}, [amount, tokenAllowance])
// Marks approval as loading from the time it is submitted (pending), until it has confirmed and another block synced.
// This avoids re-prompting the user for an already-submitted but not-yet-observed approval, by marking it loading
// until it has been re-observed. It wll sync immediately, because confirmation fast-forwards the block number.
const [approvalState, setApprovalState] = useState(ApprovalState.SYNCED)
const isApprovalLoading = approvalState !== ApprovalState.SYNCED
const isApprovalPending = useHasPendingApproval(token, PERMIT2_ADDRESS)
useEffect(() => {
if (isApprovalPending) {
setApprovalState(ApprovalState.PENDING)
} else {
setApprovalState((state) => {
if (state === ApprovalState.PENDING && isApprovalSyncing) {
return ApprovalState.SYNCING
} else if (state === ApprovalState.SYNCING && !isApprovalSyncing) {
return ApprovalState.SYNCED
}
return state
})
}
}, [isApprovalPending, isApprovalSyncing])
// Signature and PermitAllowance will expire, so they should be rechecked at an interval.
// Calculate now such that the signature will still be valid for the submitting block.
const [now, setNow] = useState(Date.now() + AVERAGE_L1_BLOCK_TIME)
useInterval(
useCallback(() => setNow((Date.now() + AVERAGE_L1_BLOCK_TIME) / 1000), []),
AVERAGE_L1_BLOCK_TIME
)
const [signature, setSignature] = useState<PermitSignature>()
const isSigned = useMemo(() => {
if (!amount || !signature) return false
return signature.details.token === token?.address && signature.spender === spender && signature.sigDeadline >= now
}, [amount, now, signature, spender, token?.address])
const { permitAllowance, expiration: permitExpiration, nonce } = usePermitAllowance(token, account, spender)
const updatePermitAllowance = useUpdatePermitAllowance(token, spender, nonce, setSignature)
const isPermitted = useMemo(() => {
if (!amount || !permitAllowance || !permitExpiration) return false
return (permitAllowance.greaterThan(amount) || permitAllowance.equalTo(amount)) && permitExpiration >= now
}, [amount, now, permitAllowance, permitExpiration])
const shouldRequestApproval = !(isApproved || isApprovalLoading)
const shouldRequestSignature = !(isPermitted || isSigned)
const addTransaction = useTransactionAdder()
const approveAndPermit = useCallback(async () => {
if (shouldRequestApproval) {
const { response, info } = await updateTokenAllowance()
addTransaction(response, info)
}
if (shouldRequestSignature) {
await updatePermitAllowance()
}
}, [addTransaction, shouldRequestApproval, shouldRequestSignature, updatePermitAllowance, updateTokenAllowance])
return useMemo(() => {
if (token) {
if (!tokenAllowance || !permitAllowance) {
return { state: AllowanceState.LOADING }
} else if (!(isPermitted || isSigned)) {
return { token, state: AllowanceState.REQUIRED, isApprovalLoading: false, approveAndPermit }
} else if (!isApproved) {
return { token, state: AllowanceState.REQUIRED, isApprovalLoading, approveAndPermit }
}
}
return { token, state: AllowanceState.ALLOWED, permitSignature: !isPermitted && isSigned ? signature : undefined }
}, [
approveAndPermit,
isApprovalLoading,
isApproved,
isPermitted,
isSigned,
permitAllowance,
signature,
token,
tokenAllowance,
])
}

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