Compare commits

..

104 Commits

Author SHA1 Message Date
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
Jack Short
6089d38daf feat: adding price scaling for sudoswap xyk pool (#5804)
* feat: adding price scaling for sudoswap xyk pool

* big number

* updating pricing logic for xyk

* fixing sweep

* removing isnotxykpool

* sudoswap calc refactoring

* refactoring calcpoolprice to include sudoswap

* refactoring calcpoolprice

* not letting asset items equal to 0 to be added

* removing comment
2023-01-11 16:05:51 -05:00
Mike Grabowski
3c6e067e90 refactor: less confusing MetaMask extension/connection detection (#5816)
* fix

* chore: remove
2023-01-12 00:12:10 +04:00
Mike Grabowski
fd1ee61daf fix: crash when invalid ENS avatar (#5810)
* chore: fix invalid ens

* chore: unwanted
2023-01-11 21:57:29 +04:00
Charles Bachmeier
de71f07b65 fix: collectionNft called before intialized (#5815)
Co-authored-by: Charles Bachmeier <charlie@genie.xyz>
2023-01-11 09:51:39 -08:00
Mike Grabowski
59f9c6c2d8 fix: infinite render loop on connection error (#5808)
fix: infinite render loop
2023-01-10 22:01:04 +04:00
lynn
3efcd3b23a fix: correct link on add liquidity page (#5803)
init
2023-01-09 19:03:55 -05:00
Jordan Frankfurt
726640787d fix: use number formatting lib for tick bounds on LP positions (#5573)
* fix: use number formatting lib for tick bounds on LP positions

* lint

* configure craco to work with conedison

* lint
2023-01-09 15:54:48 -06:00
Charles Bachmeier
889cdf6b66 feat: add nft list page v2 feature flag (#5801)
* add nft list page v2 feature flag

* add new flag file

Co-authored-by: Charles Bachmeier <charlie@genie.xyz>
2023-01-09 11:27:18 -08:00
Charles Bachmeier
400666cd0b chore: migrate MarketplaceRow to styled-components (#5798)
* style ethPrice

* style old price info and fix up ethpricedisplay

* share style and use for remove icon

* style icon

* style fee column

* style returns column

* marketplace wrapper

* needed column

* remove unused style file

Co-authored-by: Charles Bachmeier <charlie@genie.xyz>
2023-01-09 10:20:44 -08:00
Mike Grabowski
7f4fe6cc9b chore: disable Sentry in production (#5795)
disable sentry
2023-01-09 21:49:35 +04:00
Mike Grabowski
dce891ddbd chore: update Sentry version (#5794)
fix: update version:
2023-01-09 21:49:19 +04:00
Charles Bachmeier
bc9bb39a8f chore: Migrate List PriceInput from vanilla-extract to styled-components (#5792)
* wrapper

* wrap currency type

* global price icon

* style warning message

* rename style

* style warning message

* style input border

* remove useMemo

* simplify boolean

Co-authored-by: Charles Bachmeier <charlie@genie.xyz>
2023-01-06 11:41:52 -08:00
181 changed files with 12004 additions and 12068 deletions

View File

@@ -8,4 +8,4 @@ REACT_APP_MOONPAY_LINK="https://us-central1-uniswap-mobile.cloudfunctions.net/si
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=true
REACT_APP_SENTRY_ENABLED=false

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

@@ -9,6 +9,17 @@ module.exports = {
babel: {
plugins: ['@vanilla-extract/babel-plugin'],
},
jest: {
configure(jestConfig) {
return Object.assign({}, jestConfig, {
transformIgnorePatterns: ['@uniswap/conedison/format', '@uniswap/conedison/provider'],
moduleNameMapper: {
'@uniswap/conedison/format': '@uniswap/conedison/dist/format',
'@uniswap/conedison/provider': '@uniswap/conedison/dist/provider',
},
})
},
},
webpack: {
plugins: [
new VanillaExtractPlugin(),

View File

@@ -1,6 +1,7 @@
import { getTestSelector } from '../utils'
const COLLECTION_ADDRESS = '0xbd3531da5cf5857e7cfaa92426877b022e612cf8'
const PUDGY_COLLECTION_ADDRESS = '0xbd3531da5cf5857e7cfaa92426877b022e612cf8'
const BONSAI_COLLECTION_ADDRESS = '0xec9c519d49856fd2f8133a0741b4dbe002ce211b'
describe('Testing nfts', () => {
beforeEach(() => {
@@ -16,7 +17,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 +25,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 +42,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

@@ -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,72 @@
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 no results found when contract address is search term', () => {
// Search for uni token contract address.
cy.get('[data-cy="search-bar-input"]').last().type('0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984')
cy.get('[data-cy="search-bar"]')
.should('contain.text', 'No tokens found.')
.and('contain.text', 'No NFT collections found.')
})
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

@@ -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",
@@ -130,11 +131,11 @@
"@reach/portal": "^0.10.3",
"@react-hook/window-scroll": "^1.3.0",
"@reduxjs/toolkit": "^1.6.1",
"@sentry/react": "7.20.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/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.4",
"@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.27.0",
"@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",

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,6 +1,6 @@
import { Trans } from '@lingui/macro'
import { useWeb3React } from '@web3-react/core'
import { getConnection, getConnectionName, getIsCoinbaseWallet, getIsMetaMask } 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'
@@ -210,14 +210,14 @@ export default function AccountDetails({
const theme = useTheme()
const dispatch = useAppDispatch()
const isMetaMask = getIsMetaMask()
const isCoinbaseWallet = getIsCoinbaseWallet()
const isInjectedMobileBrowser = (isMetaMask || isCoinbaseWallet) && isMobile
const hasMetaMaskExtension = getIsMetaMaskWallet()
const hasCoinbaseExtension = getIsCoinbaseWallet()
const isInjectedMobileBrowser = (hasMetaMaskExtension || hasCoinbaseExtension) && isMobile
function formatConnectorName() {
return (
<WalletName>
<Trans>Connected with</Trans> {getConnectionName(connectionType, isMetaMask)}
<Trans>Connected with</Trans> {getConnectionName(connectionType, hasMetaMaskExtension)}
</WalletName>
)
}
@@ -246,7 +246,7 @@ export default function AccountDetails({
<WalletAction
style={{ fontSize: '.825rem', fontWeight: 400, marginRight: '8px' }}
onClick={() => {
const walletType = getConnectionName(getConnection(connector).type, getIsMetaMask())
const walletType = getConnectionName(getConnection(connector).type)
if (connector.deactivate) {
connector.deactivate()
} else {

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,6 +1,10 @@
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'
@@ -215,6 +219,30 @@ export default function FeatureFlagModal() {
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

@@ -5,7 +5,7 @@ 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 +100,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 +109,7 @@ export function FiatOnrampAnnouncement() {
}, [acknowledge, acks])
const handleClose = useCallback(() => {
setLocalClose(true)
localStorage.setItem(ANNOUNCEMENT_DISMISSED, 'true')
}, [])
@@ -128,7 +130,8 @@ export function FiatOnrampAnnouncement() {
localStorage.getItem(ANNOUNCEMENT_DISMISSED) ||
acks?.renderCount >= MAX_RENDER_COUNT ||
isMobile ||
openModal !== null
openModal !== null ||
localClose
) {
return null
}

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

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

@@ -148,6 +148,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 +188,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

@@ -46,7 +46,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>

View File

@@ -4,7 +4,6 @@ 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 TokenSafetyIcon from 'components/TokenSafety/TokenSafetyIcon'
import { getChainInfo } from 'constants/chainInfo'
import { NATIVE_CHAIN_ID } from 'constants/tokens'
@@ -25,9 +24,6 @@ import styled from 'styled-components/macro'
import { getDeltaArrow } from '../Tokens/TokenDetails/PriceChart'
import * as styles from './SearchBar.css'
const StyledLogoContainer = styled(LogoContainer)`
margin-right: 8px;
`
const PriceChangeContainer = styled.div`
display: flex;
align-items: center;
@@ -160,7 +156,7 @@ export const TokenRow = ({ token, isHovered, setHoveredIndex, toggleOpen, index,
sendAnalyticsEvent(InterfaceEventName.NAVBAR_RESULT_SELECTED, { ...eventProperties })
}, [addToSearchHistory, toggleOpen, token, eventProperties])
const [bridgedAddress, bridgedChain, L2Icon] = useBridgedAddress(token)
const [bridgedAddress, bridgedChain] = useBridgedAddress(token)
const tokenDetailsPath = getTokenDetailsURL(bridgedAddress ?? token.address, undefined, bridgedChain ?? token.chainId)
// Close the modal on escape
useEffect(() => {
@@ -181,6 +177,7 @@ export const TokenRow = ({ token, isHovered, setHoveredIndex, toggleOpen, index,
return (
<Link
data-cy={`searchbar-token-row-${token.symbol}`}
to={tokenDetailsPath}
onClick={handleClick}
onMouseEnter={() => !isHovered && setHoveredIndex(index)}
@@ -189,17 +186,15 @@ 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>
<AssetLogo
isNative={token.address === NATIVE_CHAIN_ID}
address={token.address}
chainId={token.chainId}
symbol={token.symbol}
size="36px"
backupImg={token.logoURI}
style={{ margin: '8px 8px 8px 0' }}
/>
<Column className={styles.suggestionPrimaryContainer}>
<Row gap="4" width="full">
<Box className={styles.primaryText}>{token.name}</Box>

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'
@@ -79,6 +82,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 +125,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

@@ -232,8 +232,12 @@ export default function PositionListItem({ positionDetails }: PositionListItemPr
<Trans>Min: </Trans>
</ExtentsText>
<Trans>
{formatTickPrice(priceLower, tickAtLimit, Bound.LOWER)} <HoverInlineText text={currencyQuote?.symbol} />{' '}
per <HoverInlineText text={currencyBase?.symbol ?? ''} />
{formatTickPrice({
price: priceLower,
atLimit: tickAtLimit,
direction: Bound.LOWER,
})}{' '}
<HoverInlineText text={currencyQuote?.symbol} /> per <HoverInlineText text={currencyBase?.symbol ?? ''} />
</Trans>
</RangeText>{' '}
<HideSmall>
@@ -247,8 +251,13 @@ export default function PositionListItem({ positionDetails }: PositionListItemPr
<Trans>Max:</Trans>
</ExtentsText>
<Trans>
{formatTickPrice(priceUpper, tickAtLimit, Bound.UPPER)} <HoverInlineText text={currencyQuote?.symbol} />{' '}
per <HoverInlineText maxCharacters={10} text={currencyBase?.symbol} />
{formatTickPrice({
price: priceUpper,
atLimit: tickAtLimit,
direction: Bound.UPPER,
})}{' '}
<HoverInlineText text={currencyQuote?.symbol} /> per{' '}
<HoverInlineText maxCharacters={10} text={currencyBase?.symbol} />
</Trans>
</RangeText>
</RangeLineItem>

View File

@@ -125,11 +125,13 @@ export const PositionPreview = ({
<ThemedText.DeprecatedMain fontSize="12px">
<Trans>Min Price</Trans>
</ThemedText.DeprecatedMain>
<ThemedText.DeprecatedMediumHeader textAlign="center">{`${formatTickPrice(
priceLower,
ticksAtLimit,
Bound.LOWER
)}`}</ThemedText.DeprecatedMediumHeader>
<ThemedText.DeprecatedMediumHeader textAlign="center">
{formatTickPrice({
price: priceLower,
atLimit: ticksAtLimit,
direction: Bound.LOWER,
})}
</ThemedText.DeprecatedMediumHeader>
<ThemedText.DeprecatedMain textAlign="center" fontSize="12px">
<Trans>
{quoteCurrency.symbol} per {baseCurrency.symbol}
@@ -146,11 +148,13 @@ export const PositionPreview = ({
<ThemedText.DeprecatedMain fontSize="12px">
<Trans>Max Price</Trans>
</ThemedText.DeprecatedMain>
<ThemedText.DeprecatedMediumHeader textAlign="center">{`${formatTickPrice(
priceUpper,
ticksAtLimit,
Bound.UPPER
)}`}</ThemedText.DeprecatedMediumHeader>
<ThemedText.DeprecatedMediumHeader textAlign="center">
{formatTickPrice({
price: priceUpper,
atLimit: ticksAtLimit,
direction: Bound.UPPER,
})}
</ThemedText.DeprecatedMediumHeader>
<ThemedText.DeprecatedMain textAlign="center" fontSize="12px">
<Trans>
{quoteCurrency.symbol} per {baseCurrency.symbol}

View File

@@ -45,6 +45,7 @@ interface CurrencySearchProps {
showCommonBases?: boolean
showCurrencyAmount?: boolean
disableNonToken?: boolean
onlyShowCurrenciesWithBalance?: boolean
}
export function CurrencySearch({
@@ -56,6 +57,7 @@ export function CurrencySearch({
disableNonToken,
onDismiss,
isOpen,
onlyShowCurrenciesWithBalance,
}: CurrencySearchProps) {
const { chainId } = useWeb3React()
const theme = useTheme()
@@ -92,6 +94,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 +107,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 +128,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 +194,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

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

@@ -2,6 +2,7 @@ import { Trans } from '@lingui/macro'
import { Trace } from '@uniswap/analytics'
import { InterfacePageName } from '@uniswap/analytics-events'
import { Currency } from '@uniswap/sdk-core'
import { Field } from '@uniswap/widgets'
import { useWeb3React } from '@web3-react/core'
import CurrencyLogo from 'components/Logo/CurrencyLogo'
import { AboutSection } from 'components/Tokens/TokenDetails/About'
@@ -20,7 +21,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 +111,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) => {
@@ -168,8 +168,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 +184,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>
@@ -223,6 +219,7 @@ export default function TokenDetails({
<RightPanel>
<Widget
token={token ?? undefined}
defaultField={Field.OUTPUT}
onTokenChange={navigateToWidgetSelectedToken}
onReviewSwapClick={onReviewSwapClick}
/>

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;
@@ -378,15 +360,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 +424,29 @@ interface LoadedRowProps {
tokenListLength: number
token: NonNullable<TopToken>
sparklineMap: SparklineMap
volumeRank: 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, volumeRank } = 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: volumeRank,
token_list_length: tokenListLength,
time_frame: timePeriod,
search_token_address_input: filterString,
@@ -469,7 +454,7 @@ 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)}
onClick={() =>
@@ -478,16 +463,13 @@ export const LoadedRow = forwardRef((props: LoadedRowProps, ref: ForwardedRef<HT
>
<TokenRow
header={false}
listNumber={rank}
listNumber={volumeRank}
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, tokenVolumeRank, 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}
volumeRank={tokenVolumeRank[token.address]}
/>
)
)}

View File

@@ -21,7 +21,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 +56,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 +69,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 +126,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 +133,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 +164,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 +176,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()
@@ -280,21 +275,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 +302,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
@@ -335,7 +329,11 @@ const AuthenticatedHeader = () => {
<ThemedText.BodyPrimary>{error}</ThemedText.BodyPrimary>
) : (
<>
{fiatOnrampAvailabilityLoading ? <StyledLoadingButtonSpinner /> : <CreditCard />}{' '}
{fiatOnrampAvailabilityLoading ? (
<StyledLoadingButtonSpinner />
) : (
<CreditCard height="20px" width="20px" />
)}{' '}
<Trans>Buy crypto</Trans>
</>
)}
@@ -371,7 +369,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,7 +70,7 @@ 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, 'getIsMetaMask').mockReturnValue(false)
jest.spyOn(connectionUtils, 'getIsMetaMaskWallet').mockReturnValue(false)
jest.spyOn(connectionUtils, 'getIsCoinbaseWallet').mockReturnValue(false)
render(<WalletModal pendingTransactions={[]} confirmedTransactions={[]} />)
@@ -82,7 +82,7 @@ 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, 'getIsMetaMask').mockReturnValue(true)
jest.spyOn(connectionUtils, 'getIsMetaMaskWallet').mockReturnValue(true)
jest.spyOn(connectionUtils, 'getIsCoinbaseWallet').mockReturnValue(false)
render(<WalletModal pendingTransactions={[]} confirmedTransactions={[]} />)
@@ -96,7 +96,7 @@ it('loads Wallet Modal on mobile', async () => {
UserAgentMock.isMobile = true
jest.spyOn(connectionUtils, 'getIsInjected').mockReturnValue(false)
jest.spyOn(connectionUtils, 'getIsMetaMask').mockReturnValue(false)
jest.spyOn(connectionUtils, 'getIsMetaMaskWallet').mockReturnValue(false)
jest.spyOn(connectionUtils, 'getIsCoinbaseWallet').mockReturnValue(false)
render(<WalletModal pendingTransactions={[]} confirmedTransactions={[]} />)
@@ -109,7 +109,7 @@ it('loads Wallet Modal on MetaMask browser', async () => {
UserAgentMock.isMobile = true
jest.spyOn(connectionUtils, 'getIsInjected').mockReturnValue(true)
jest.spyOn(connectionUtils, 'getIsMetaMask').mockReturnValue(true)
jest.spyOn(connectionUtils, 'getIsMetaMaskWallet').mockReturnValue(true)
jest.spyOn(connectionUtils, 'getIsCoinbaseWallet').mockReturnValue(false)
render(<WalletModal pendingTransactions={[]} confirmedTransactions={[]} />)
@@ -121,7 +121,7 @@ it('loads Wallet Modal on Coinbase Wallet browser', async () => {
UserAgentMock.isMobile = true
jest.spyOn(connectionUtils, 'getIsInjected').mockReturnValue(true)
jest.spyOn(connectionUtils, 'getIsMetaMask').mockReturnValue(false)
jest.spyOn(connectionUtils, 'getIsMetaMaskWallet').mockReturnValue(false)
jest.spyOn(connectionUtils, 'getIsCoinbaseWallet').mockReturnValue(true)
render(<WalletModal pendingTransactions={[]} confirmedTransactions={[]} />)

View File

@@ -7,7 +7,13 @@ import { sendEvent } from 'components/analytics'
import { AutoColumn } from 'components/Column'
import { AutoRow } from 'components/Row'
import { networkConnection } from 'connection'
import { getConnection, getConnectionName, getIsCoinbaseWallet, getIsInjected, getIsMetaMask } from 'connection/utils'
import {
getConnection,
getConnectionName,
getIsCoinbaseWallet,
getIsInjected,
getIsMetaMaskWallet,
} from 'connection/utils'
import usePrevious from 'hooks/usePrevious'
import { useCallback, useEffect, useState } from 'react'
import { ArrowLeft } from 'react-feather'
@@ -204,7 +210,7 @@ export default function WalletModal({
// When new wallet is successfully set by the user, trigger logging of Amplitude analytics event.
useEffect(() => {
if (account && account !== lastActiveWalletAddress) {
const walletType = getConnectionName(getConnection(connector).type, getIsMetaMask())
const walletType = getConnectionName(getConnection(connector).type)
const isReconnect =
connectedWallets.filter((wallet) => wallet.account === account && wallet.walletType === walletType).length > 0
sendAnalyticsEventAndUserInfo(account, walletType, chainId, isReconnect)
@@ -238,7 +244,7 @@ export default function WalletModal({
sendAnalyticsEvent(InterfaceEventName.WALLET_CONNECT_TXN_COMPLETED, {
result: WalletConnectionResult.FAILED,
wallet_type: getConnectionName(connectionType, getIsMetaMask()),
wallet_type: getConnectionName(connectionType),
})
}
},
@@ -247,11 +253,11 @@ export default function WalletModal({
function getOptions() {
const isInjected = getIsInjected()
const isMetaMask = getIsMetaMask()
const isCoinbaseWallet = getIsCoinbaseWallet()
const hasMetaMaskExtension = getIsMetaMaskWallet()
const hasCoinbaseExtension = getIsCoinbaseWallet()
const isCoinbaseWalletBrowser = isMobile && isCoinbaseWallet
const isMetaMaskBrowser = isMobile && isMetaMask
const isCoinbaseWalletBrowser = isMobile && hasCoinbaseExtension
const isMetaMaskBrowser = isMobile && hasMetaMaskExtension
const isInjectedMobileBrowser = isCoinbaseWalletBrowser || isMetaMaskBrowser
let injectedOption
@@ -259,8 +265,8 @@ export default function WalletModal({
if (!isMobile) {
injectedOption = <InstallMetaMaskOption />
}
} else if (!isCoinbaseWallet) {
if (isMetaMask) {
} else if (!hasCoinbaseExtension) {
if (hasMetaMaskExtension) {
injectedOption = <MetaMaskOption tryActivation={tryActivation} />
} else {
injectedOption = <InjectedOption tryActivation={tryActivation} />

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,
useToggleMetamaskConnectionErrorModal,
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 toggleMetamaskConnectionErrorModal = useToggleMetamaskConnectionErrorModal()
const walletIsOpen = useModalIsOpen(ApplicationModal.WALLET_DROPDOWN)
const isClaimAvailable = useIsNftClaimAvailable((state) => state.isClaimAvailable)
const error = useAppSelector((state) => state.connection.errorByConnectionType[getConnection(connector).type])
useEffect(() => {
if (getIsMetaMask() && error) {
toggleMetamaskConnectionErrorModal()
}
}, [error, toggleMetamaskConnectionErrorModal])
const error = useAppSelector((state) => state.connection.errorByConnectionType[connectionType])
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

@@ -10,6 +10,7 @@ import { Currency, TradeType } from '@uniswap/sdk-core'
import {
AddEthereumChainParameter,
EMPTY_TOKEN_LIST,
Field,
OnReviewSwapClick,
SwapWidget,
SwapWidgetSkeleton,
@@ -26,6 +27,7 @@ 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'
@@ -35,7 +37,7 @@ 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/'
@@ -45,18 +47,32 @@ function useWidgetTheme() {
interface WidgetProps {
token?: Currency
width?: number | string
defaultField: Field
onTokenChange?: (token: Currency) => void
onReviewSwapClick?: OnReviewSwapClick
}
export default function Widget({ token, onTokenChange, onReviewSwapClick }: WidgetProps) {
export default function Widget({
token,
width = DEFAULT_WIDGET_WIDTH,
defaultField,
onTokenChange,
onReviewSwapClick,
}: WidgetProps) {
const { connector, provider } = useWeb3React()
const locale = useActiveLocale()
const theme = useWidgetTheme()
const { inputs, tokenSelector } = useSyncWidgetInputs({ token, onTokenChange })
const { inputs, tokenSelector } = useSyncWidgetInputs({ token, onTokenChange, defaultField })
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
@@ -174,5 +191,5 @@ export default function Widget({ token, onTokenChange, onReviewSwapClick }: Widg
export function WidgetSkeleton() {
const theme = useWidgetTheme()
return <SwapWidgetSkeleton theme={theme} width={WIDGET_WIDTH} />
return <SwapWidgetSkeleton theme={theme} width={DEFAULT_WIDGET_WIDTH} />
}

View File

@@ -21,26 +21,29 @@ function includesDefaultToken(tokens: SwapTokens) {
*/
export function useSyncWidgetInputs({
token,
defaultField,
onTokenChange,
}: {
token?: Currency
defaultField: Field
onTokenChange?: (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>({ [defaultField]: token, default: token })
useEffect(() => {
setTokens((tokens) => {
const update = { ...tokens, default: token }
if (!includesDefaultToken(update)) {
return { [Field.OUTPUT]: update.default, default: update.default }
return { [defaultField]: update.default, default: update.default }
}
return update
})
}, [token])
}, [defaultField, token])
const onAmountChange = useCallback(
(field: Field, amount: string, origin?: 'max') => {

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,8 +12,15 @@ export function getIsInjected(): boolean {
return Boolean(window.ethereum)
}
export function getIsMetaMask(): boolean {
return window.ethereum?.isMetaMask ?? false
export function getIsBraveWallet(): boolean {
return window.ethereum?.isBraveWallet ?? false
}
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 {
@@ -50,10 +57,13 @@ export function getConnection(c: Connector | ConnectionType) {
}
}
export function getConnectionName(connectionType: ConnectionType, isMetaMask?: boolean) {
export function getConnectionName(
connectionType: ConnectionType,
hasMetaMaskExtension: boolean = getIsMetaMaskWallet()
) {
switch (connectionType) {
case ConnectionType.INJECTED:
return isMetaMask ? 'MetaMask' : 'Browser Wallet'
return hasMetaMaskExtension ? 'MetaMask' : 'Browser Wallet'
case ConnectionType.COINBASE_WALLET:
return 'Coinbase Wallet'
case ConnectionType.WALLET_CONNECT:

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

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

@@ -2,4 +2,8 @@ export enum FeatureFlag {
fiatOnramp = 'fiatOnramp',
traceJsonRpc = 'traceJsonRpc',
permit2 = 'permit2',
nftListV2 = 'nftListV2',
payWithAnyToken = 'payWithAnyToken',
swapWidget = 'swapWidget',
gqlRouting = 'gqlRouting',
}

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

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

View File

@@ -0,0 +1,7 @@
import { BaseVariant, FeatureFlag, useBaseFlag } from '../index'
export function usePayWithAnyTokenFlag(): BaseVariant {
return useBaseFlag(FeatureFlag.payWithAnyToken)
}
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

@@ -14,8 +14,8 @@ 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
@@ -23,31 +23,39 @@ gql`
address
symbol
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 +66,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!) {
@@ -26,24 +34,30 @@ gql`
address
symbol
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 +67,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 +82,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 +112,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 +139,7 @@ export type TopToken = NonNullable<NonNullable<TopTokens100Query>['topTokens']>[
interface UseTopTokensReturnValue {
tokens: TopToken[] | undefined
tokenVolumeRank: Record<string, number>
loadingTokens: boolean
sparklines: SparklineMap
}
@@ -127,9 +148,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 +164,34 @@ 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 filteredTokens = useFilteredTokens(mappedTokens)
const unwrappedTokens = useMemo(() => data?.topTokens?.map((token) => unwrapToken(chainId, token)), [chainId, data])
const tokenVolumeRank = useMemo(
() =>
unwrappedTokens
?.sort((a, b) => {
if (!a.market?.volume || !b.market?.volume) return 0
return a.market.volume.value > b.market.volume.value ? -1 : 1
})
.reduce((acc, cur, i) => {
if (!cur.address) return acc
return {
...acc,
[cur.address]: i + 1,
}
}, {}) ?? {},
[unwrappedTokens]
)
const filteredTokens = useFilteredTokens(unwrappedTokens)
const sortedTokens = useSortedTokens(filteredTokens)
return useMemo(() => ({ tokens: sortedTokens, loadingTokens, sparklines }), [loadingTokens, sortedTokens, sparklines])
return useMemo(
() => ({ tokens: sortedTokens, tokenVolumeRank, loadingTokens, sparklines }),
[loadingTokens, tokenVolumeRank, sortedTokens, sparklines]
)
}

View File

@@ -386,6 +386,7 @@ export type NftCollectionTraitStats = {
export type NftCollectionsFilterInput = {
addresses?: InputMaybe<Array<Scalars['String']>>;
nameQuery?: InputMaybe<Scalars['String']>;
};
export type NftContract = IContract & {
@@ -468,12 +469,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 +538,22 @@ 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;
};
/** 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,7 +564,6 @@ export type Portfolio = {
tokenBalances?: Maybe<Array<Maybe<TokenBalance>>>;
tokensTotalDenominatedValue?: Maybe<Amount>;
tokensTotalDenominatedValueChange?: Maybe<AmountChange>;
tokensTotalDenominatedValueHistory?: Maybe<Array<Maybe<TimestampedAmount>>>;
};
@@ -528,11 +577,6 @@ export type PortfolioTokensTotalDenominatedValueChangeArgs = {
duration?: InputMaybe<HistoryDuration>;
};
export type PortfolioTokensTotalDenominatedValueHistoryArgs = {
duration?: InputMaybe<HistoryDuration>;
};
export type Query = {
__typename?: 'Query';
assetActivities?: Maybe<Array<Maybe<AssetActivity>>>;
@@ -540,6 +584,7 @@ export type Query = {
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>>>;
@@ -595,6 +640,14 @@ 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 +720,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 +754,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'];
@@ -794,6 +866,30 @@ export enum TokenStandard {
Native = 'NATIVE'
}
export type TokenTradeInput = {
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 +902,11 @@ export type TokenTransfer = {
transactedValue?: Maybe<Amount>;
};
export type TradePoolInput = {
pair?: InputMaybe<PairInput>;
pool?: InputMaybe<PoolInput>;
};
export type Transaction = {
__typename?: 'Transaction';
blockNumber: Scalars['Int'];
@@ -832,19 +933,21 @@ export enum TransactionStatus {
}
export type TokenQueryVariables = Exact<{
contract: ContractInput;
chain: Chain;
address?: InputMaybe<Scalars['String']>;
}>;
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 TokenQuery = { __typename?: 'Query', token?: { __typename?: 'Token', id: string, decimals?: number, name?: string, chain: Chain, address?: string, symbol?: string, 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 +955,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, 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 +963,7 @@ 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 AssetQueryVariables = Exact<{
address: Scalars['String'];
@@ -901,12 +1004,22 @@ 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', calldata: string, toAddress: string, route?: Array<{ __typename?: 'NftTrade', amount: number, contractAddress: string, id: string, marketplace: NftMarketplace, tokenId: string, tokenType: NftStandard, price: { __typename?: 'TokenAmount', currency: Currency, value: string }, quotePrice?: { __typename?: 'TokenAmount', currency: Currency, value: string } }>, sendAmount: { __typename?: 'TokenAmount', currency: Currency, value: string } } };
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
@@ -914,31 +1027,39 @@ export const TokenDocument = gql`
address
symbol
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 +1080,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 +1097,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 +1130,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'
* },
* });
@@ -1027,24 +1156,30 @@ export const TopTokens100Document = gql`
address
symbol
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 +1217,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
}
@@ -1493,6 +1632,7 @@ export const NftBalanceDocument = gql`
url
}
name
twitterName
nftContracts {
address
chain
@@ -1592,4 +1732,68 @@ 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
) {
calldata
route {
amount
contractAddress
id
marketplace
price {
currency
value
}
quotePrice {
currency
value
}
tokenId
tokenType
}
sendAmount {
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

@@ -1,7 +1,7 @@
import { parseEther } from 'ethers/lib/utils'
import gql from 'graphql-tag'
import { GenieAsset, Markets, Trait } from 'nft/types'
import { isNotXYKPool, wrapScientificNotation } from 'nft/utils'
import { wrapScientificNotation } from 'nft/utils'
import { useCallback, useMemo } from 'react'
import {
@@ -219,7 +219,7 @@ export function useNftAssets(params: AssetFetcherParams) {
return useMemo(() => {
return {
data: assets?.filter((asset) => isNotXYKPool(asset)),
data: assets,
hasNext,
loading,
loadMore,
@@ -279,5 +279,5 @@ export function useSweepNftAssets(params: SweepFetcherParams) {
}),
[data?.nftAssets?.edges, data?.nftAssets?.totalCount]
)
return useMemo(() => ({ data: assets?.filter((asset) => isNotXYKPool(asset)), loading }), [assets, loading])
return useMemo(() => ({ data: assets, loading }), [assets, loading])
}

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,47 @@
import gql from 'graphql-tag'
import { NftTradeInput, TokenTradeInput, useNftRouteQuery } from '../__generated__/types-and-hooks'
gql`
query NftRoute(
$chain: Chain = ETHEREUM
$senderAddress: String!
$nftTrades: [NftTradeInput!]!
$tokenTrades: [TokenTradeInput!]
) {
nftRoute(chain: $chain, senderAddress: $senderAddress, nftTrades: $nftTrades, tokenTrades: $tokenTrades) {
calldata
route {
amount
contractAddress
id
marketplace
price {
currency
value
}
quotePrice {
currency
value
}
tokenId
tokenType
}
sendAmount {
currency
value
}
toAddress
}
}
`
export function useNftRoute(senderAddress: string, nftTrades: NftTradeInput[], tokenTrades?: TokenTradeInput[]) {
return useNftRouteQuery({
variables: {
senderAddress,
nftTrades,
tokenTrades,
},
})
}

View File

@@ -1,9 +1,31 @@
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,

View File

@@ -37,6 +37,7 @@ export type Bundle = {
export type Bundle_Filter = {
/** Filter for the block changed event. */
_change_block?: InputMaybe<BlockChangedFilter>;
and?: InputMaybe<Array<InputMaybe<Bundle_Filter>>>;
ethPriceUSD?: InputMaybe<Scalars['BigDecimal']>;
ethPriceUSD_gt?: InputMaybe<Scalars['BigDecimal']>;
ethPriceUSD_gte?: InputMaybe<Scalars['BigDecimal']>;
@@ -53,6 +54,7 @@ export type Bundle_Filter = {
id_lte?: InputMaybe<Scalars['ID']>;
id_not?: InputMaybe<Scalars['ID']>;
id_not_in?: InputMaybe<Array<Scalars['ID']>>;
or?: InputMaybe<Array<InputMaybe<Bundle_Filter>>>;
};
export enum Bundle_OrderBy {
@@ -114,6 +116,7 @@ export type Burn_Filter = {
amount_lte?: InputMaybe<Scalars['BigInt']>;
amount_not?: InputMaybe<Scalars['BigInt']>;
amount_not_in?: InputMaybe<Array<Scalars['BigInt']>>;
and?: InputMaybe<Array<InputMaybe<Burn_Filter>>>;
id?: InputMaybe<Scalars['ID']>;
id_gt?: InputMaybe<Scalars['ID']>;
id_gte?: InputMaybe<Scalars['ID']>;
@@ -130,15 +133,24 @@ export type Burn_Filter = {
logIndex_lte?: InputMaybe<Scalars['BigInt']>;
logIndex_not?: InputMaybe<Scalars['BigInt']>;
logIndex_not_in?: InputMaybe<Array<Scalars['BigInt']>>;
or?: InputMaybe<Array<InputMaybe<Burn_Filter>>>;
origin?: InputMaybe<Scalars['Bytes']>;
origin_contains?: InputMaybe<Scalars['Bytes']>;
origin_gt?: InputMaybe<Scalars['Bytes']>;
origin_gte?: InputMaybe<Scalars['Bytes']>;
origin_in?: InputMaybe<Array<Scalars['Bytes']>>;
origin_lt?: InputMaybe<Scalars['Bytes']>;
origin_lte?: InputMaybe<Scalars['Bytes']>;
origin_not?: InputMaybe<Scalars['Bytes']>;
origin_not_contains?: InputMaybe<Scalars['Bytes']>;
origin_not_in?: InputMaybe<Array<Scalars['Bytes']>>;
owner?: InputMaybe<Scalars['Bytes']>;
owner_contains?: InputMaybe<Scalars['Bytes']>;
owner_gt?: InputMaybe<Scalars['Bytes']>;
owner_gte?: InputMaybe<Scalars['Bytes']>;
owner_in?: InputMaybe<Array<Scalars['Bytes']>>;
owner_lt?: InputMaybe<Scalars['Bytes']>;
owner_lte?: InputMaybe<Scalars['Bytes']>;
owner_not?: InputMaybe<Scalars['Bytes']>;
owner_not_contains?: InputMaybe<Scalars['Bytes']>;
owner_not_in?: InputMaybe<Array<Scalars['Bytes']>>;
@@ -312,6 +324,7 @@ export type Collect_Filter = {
amountUSD_lte?: InputMaybe<Scalars['BigDecimal']>;
amountUSD_not?: InputMaybe<Scalars['BigDecimal']>;
amountUSD_not_in?: InputMaybe<Array<Scalars['BigDecimal']>>;
and?: InputMaybe<Array<InputMaybe<Collect_Filter>>>;
id?: InputMaybe<Scalars['ID']>;
id_gt?: InputMaybe<Scalars['ID']>;
id_gte?: InputMaybe<Scalars['ID']>;
@@ -328,9 +341,14 @@ export type Collect_Filter = {
logIndex_lte?: InputMaybe<Scalars['BigInt']>;
logIndex_not?: InputMaybe<Scalars['BigInt']>;
logIndex_not_in?: InputMaybe<Array<Scalars['BigInt']>>;
or?: InputMaybe<Array<InputMaybe<Collect_Filter>>>;
owner?: InputMaybe<Scalars['Bytes']>;
owner_contains?: InputMaybe<Scalars['Bytes']>;
owner_gt?: InputMaybe<Scalars['Bytes']>;
owner_gte?: InputMaybe<Scalars['Bytes']>;
owner_in?: InputMaybe<Array<Scalars['Bytes']>>;
owner_lt?: InputMaybe<Scalars['Bytes']>;
owner_lte?: InputMaybe<Scalars['Bytes']>;
owner_not?: InputMaybe<Scalars['Bytes']>;
owner_not_contains?: InputMaybe<Scalars['Bytes']>;
owner_not_in?: InputMaybe<Array<Scalars['Bytes']>>;
@@ -436,6 +454,7 @@ export type Factory = {
export type Factory_Filter = {
/** Filter for the block changed event. */
_change_block?: InputMaybe<BlockChangedFilter>;
and?: InputMaybe<Array<InputMaybe<Factory_Filter>>>;
id?: InputMaybe<Scalars['ID']>;
id_gt?: InputMaybe<Scalars['ID']>;
id_gte?: InputMaybe<Scalars['ID']>;
@@ -444,6 +463,7 @@ export type Factory_Filter = {
id_lte?: InputMaybe<Scalars['ID']>;
id_not?: InputMaybe<Scalars['ID']>;
id_not_in?: InputMaybe<Array<Scalars['ID']>>;
or?: InputMaybe<Array<InputMaybe<Factory_Filter>>>;
owner?: InputMaybe<Scalars['ID']>;
owner_gt?: InputMaybe<Scalars['ID']>;
owner_gte?: InputMaybe<Scalars['ID']>;
@@ -617,6 +637,7 @@ export type Flash_Filter = {
amountUSD_lte?: InputMaybe<Scalars['BigDecimal']>;
amountUSD_not?: InputMaybe<Scalars['BigDecimal']>;
amountUSD_not_in?: InputMaybe<Array<Scalars['BigDecimal']>>;
and?: InputMaybe<Array<InputMaybe<Flash_Filter>>>;
id?: InputMaybe<Scalars['ID']>;
id_gt?: InputMaybe<Scalars['ID']>;
id_gte?: InputMaybe<Scalars['ID']>;
@@ -633,6 +654,7 @@ export type Flash_Filter = {
logIndex_lte?: InputMaybe<Scalars['BigInt']>;
logIndex_not?: InputMaybe<Scalars['BigInt']>;
logIndex_not_in?: InputMaybe<Array<Scalars['BigInt']>>;
or?: InputMaybe<Array<InputMaybe<Flash_Filter>>>;
pool?: InputMaybe<Scalars['String']>;
pool_?: InputMaybe<Pool_Filter>;
pool_contains?: InputMaybe<Scalars['String']>;
@@ -656,13 +678,21 @@ export type Flash_Filter = {
pool_starts_with_nocase?: InputMaybe<Scalars['String']>;
recipient?: InputMaybe<Scalars['Bytes']>;
recipient_contains?: InputMaybe<Scalars['Bytes']>;
recipient_gt?: InputMaybe<Scalars['Bytes']>;
recipient_gte?: InputMaybe<Scalars['Bytes']>;
recipient_in?: InputMaybe<Array<Scalars['Bytes']>>;
recipient_lt?: InputMaybe<Scalars['Bytes']>;
recipient_lte?: InputMaybe<Scalars['Bytes']>;
recipient_not?: InputMaybe<Scalars['Bytes']>;
recipient_not_contains?: InputMaybe<Scalars['Bytes']>;
recipient_not_in?: InputMaybe<Array<Scalars['Bytes']>>;
sender?: InputMaybe<Scalars['Bytes']>;
sender_contains?: InputMaybe<Scalars['Bytes']>;
sender_gt?: InputMaybe<Scalars['Bytes']>;
sender_gte?: InputMaybe<Scalars['Bytes']>;
sender_in?: InputMaybe<Array<Scalars['Bytes']>>;
sender_lt?: InputMaybe<Scalars['Bytes']>;
sender_lte?: InputMaybe<Scalars['Bytes']>;
sender_not?: InputMaybe<Scalars['Bytes']>;
sender_not_contains?: InputMaybe<Scalars['Bytes']>;
sender_not_in?: InputMaybe<Array<Scalars['Bytes']>>;
@@ -767,6 +797,7 @@ export type Mint_Filter = {
amount_lte?: InputMaybe<Scalars['BigInt']>;
amount_not?: InputMaybe<Scalars['BigInt']>;
amount_not_in?: InputMaybe<Array<Scalars['BigInt']>>;
and?: InputMaybe<Array<InputMaybe<Mint_Filter>>>;
id?: InputMaybe<Scalars['ID']>;
id_gt?: InputMaybe<Scalars['ID']>;
id_gte?: InputMaybe<Scalars['ID']>;
@@ -783,15 +814,24 @@ export type Mint_Filter = {
logIndex_lte?: InputMaybe<Scalars['BigInt']>;
logIndex_not?: InputMaybe<Scalars['BigInt']>;
logIndex_not_in?: InputMaybe<Array<Scalars['BigInt']>>;
or?: InputMaybe<Array<InputMaybe<Mint_Filter>>>;
origin?: InputMaybe<Scalars['Bytes']>;
origin_contains?: InputMaybe<Scalars['Bytes']>;
origin_gt?: InputMaybe<Scalars['Bytes']>;
origin_gte?: InputMaybe<Scalars['Bytes']>;
origin_in?: InputMaybe<Array<Scalars['Bytes']>>;
origin_lt?: InputMaybe<Scalars['Bytes']>;
origin_lte?: InputMaybe<Scalars['Bytes']>;
origin_not?: InputMaybe<Scalars['Bytes']>;
origin_not_contains?: InputMaybe<Scalars['Bytes']>;
origin_not_in?: InputMaybe<Array<Scalars['Bytes']>>;
owner?: InputMaybe<Scalars['Bytes']>;
owner_contains?: InputMaybe<Scalars['Bytes']>;
owner_gt?: InputMaybe<Scalars['Bytes']>;
owner_gte?: InputMaybe<Scalars['Bytes']>;
owner_in?: InputMaybe<Array<Scalars['Bytes']>>;
owner_lt?: InputMaybe<Scalars['Bytes']>;
owner_lte?: InputMaybe<Scalars['Bytes']>;
owner_not?: InputMaybe<Scalars['Bytes']>;
owner_not_contains?: InputMaybe<Scalars['Bytes']>;
owner_not_in?: InputMaybe<Array<Scalars['Bytes']>>;
@@ -818,7 +858,11 @@ export type Mint_Filter = {
pool_starts_with_nocase?: InputMaybe<Scalars['String']>;
sender?: InputMaybe<Scalars['Bytes']>;
sender_contains?: InputMaybe<Scalars['Bytes']>;
sender_gt?: InputMaybe<Scalars['Bytes']>;
sender_gte?: InputMaybe<Scalars['Bytes']>;
sender_in?: InputMaybe<Array<Scalars['Bytes']>>;
sender_lt?: InputMaybe<Scalars['Bytes']>;
sender_lte?: InputMaybe<Scalars['Bytes']>;
sender_not?: InputMaybe<Scalars['Bytes']>;
sender_not_contains?: InputMaybe<Scalars['Bytes']>;
sender_not_in?: InputMaybe<Array<Scalars['Bytes']>>;
@@ -1066,6 +1110,7 @@ export type PoolDayData = {
export type PoolDayData_Filter = {
/** Filter for the block changed event. */
_change_block?: InputMaybe<BlockChangedFilter>;
and?: InputMaybe<Array<InputMaybe<PoolDayData_Filter>>>;
close?: InputMaybe<Scalars['BigDecimal']>;
close_gt?: InputMaybe<Scalars['BigDecimal']>;
close_gte?: InputMaybe<Scalars['BigDecimal']>;
@@ -1146,6 +1191,7 @@ export type PoolDayData_Filter = {
open_lte?: InputMaybe<Scalars['BigDecimal']>;
open_not?: InputMaybe<Scalars['BigDecimal']>;
open_not_in?: InputMaybe<Array<Scalars['BigDecimal']>>;
or?: InputMaybe<Array<InputMaybe<PoolDayData_Filter>>>;
pool?: InputMaybe<Scalars['String']>;
pool_?: InputMaybe<Pool_Filter>;
pool_contains?: InputMaybe<Scalars['String']>;
@@ -1291,6 +1337,7 @@ export type PoolHourData = {
export type PoolHourData_Filter = {
/** Filter for the block changed event. */
_change_block?: InputMaybe<BlockChangedFilter>;
and?: InputMaybe<Array<InputMaybe<PoolHourData_Filter>>>;
close?: InputMaybe<Scalars['BigDecimal']>;
close_gt?: InputMaybe<Scalars['BigDecimal']>;
close_gte?: InputMaybe<Scalars['BigDecimal']>;
@@ -1363,6 +1410,7 @@ export type PoolHourData_Filter = {
open_lte?: InputMaybe<Scalars['BigDecimal']>;
open_not?: InputMaybe<Scalars['BigDecimal']>;
open_not_in?: InputMaybe<Array<Scalars['BigDecimal']>>;
or?: InputMaybe<Array<InputMaybe<PoolHourData_Filter>>>;
periodStartUnix?: InputMaybe<Scalars['Int']>;
periodStartUnix_gt?: InputMaybe<Scalars['Int']>;
periodStartUnix_gte?: InputMaybe<Scalars['Int']>;
@@ -1492,6 +1540,7 @@ export enum PoolHourData_OrderBy {
export type Pool_Filter = {
/** Filter for the block changed event. */
_change_block?: InputMaybe<BlockChangedFilter>;
and?: InputMaybe<Array<InputMaybe<Pool_Filter>>>;
burns_?: InputMaybe<Burn_Filter>;
collectedFeesToken0?: InputMaybe<Scalars['BigDecimal']>;
collectedFeesToken0_gt?: InputMaybe<Scalars['BigDecimal']>;
@@ -1599,6 +1648,7 @@ export type Pool_Filter = {
observationIndex_lte?: InputMaybe<Scalars['BigInt']>;
observationIndex_not?: InputMaybe<Scalars['BigInt']>;
observationIndex_not_in?: InputMaybe<Array<Scalars['BigInt']>>;
or?: InputMaybe<Array<InputMaybe<Pool_Filter>>>;
poolDayData_?: InputMaybe<PoolDayData_Filter>;
poolHourData_?: InputMaybe<PoolHourData_Filter>;
sqrtPrice?: InputMaybe<Scalars['BigInt']>;
@@ -1842,6 +1892,7 @@ export type PositionSnapshot = {
export type PositionSnapshot_Filter = {
/** Filter for the block changed event. */
_change_block?: InputMaybe<BlockChangedFilter>;
and?: InputMaybe<Array<InputMaybe<PositionSnapshot_Filter>>>;
blockNumber?: InputMaybe<Scalars['BigInt']>;
blockNumber_gt?: InputMaybe<Scalars['BigInt']>;
blockNumber_gte?: InputMaybe<Scalars['BigInt']>;
@@ -1914,9 +1965,14 @@ export type PositionSnapshot_Filter = {
liquidity_lte?: InputMaybe<Scalars['BigInt']>;
liquidity_not?: InputMaybe<Scalars['BigInt']>;
liquidity_not_in?: InputMaybe<Array<Scalars['BigInt']>>;
or?: InputMaybe<Array<InputMaybe<PositionSnapshot_Filter>>>;
owner?: InputMaybe<Scalars['Bytes']>;
owner_contains?: InputMaybe<Scalars['Bytes']>;
owner_gt?: InputMaybe<Scalars['Bytes']>;
owner_gte?: InputMaybe<Scalars['Bytes']>;
owner_in?: InputMaybe<Array<Scalars['Bytes']>>;
owner_lt?: InputMaybe<Scalars['Bytes']>;
owner_lte?: InputMaybe<Scalars['Bytes']>;
owner_not?: InputMaybe<Scalars['Bytes']>;
owner_not_contains?: InputMaybe<Scalars['Bytes']>;
owner_not_in?: InputMaybe<Array<Scalars['Bytes']>>;
@@ -2031,6 +2087,7 @@ export enum PositionSnapshot_OrderBy {
export type Position_Filter = {
/** Filter for the block changed event. */
_change_block?: InputMaybe<BlockChangedFilter>;
and?: InputMaybe<Array<InputMaybe<Position_Filter>>>;
collectedFeesToken0?: InputMaybe<Scalars['BigDecimal']>;
collectedFeesToken0_gt?: InputMaybe<Scalars['BigDecimal']>;
collectedFeesToken0_gte?: InputMaybe<Scalars['BigDecimal']>;
@@ -2095,9 +2152,14 @@ export type Position_Filter = {
liquidity_lte?: InputMaybe<Scalars['BigInt']>;
liquidity_not?: InputMaybe<Scalars['BigInt']>;
liquidity_not_in?: InputMaybe<Array<Scalars['BigInt']>>;
or?: InputMaybe<Array<InputMaybe<Position_Filter>>>;
owner?: InputMaybe<Scalars['Bytes']>;
owner_contains?: InputMaybe<Scalars['Bytes']>;
owner_gt?: InputMaybe<Scalars['Bytes']>;
owner_gte?: InputMaybe<Scalars['Bytes']>;
owner_in?: InputMaybe<Array<Scalars['Bytes']>>;
owner_lt?: InputMaybe<Scalars['Bytes']>;
owner_lte?: InputMaybe<Scalars['Bytes']>;
owner_not?: InputMaybe<Scalars['Bytes']>;
owner_not_contains?: InputMaybe<Scalars['Bytes']>;
owner_not_in?: InputMaybe<Array<Scalars['Bytes']>>;
@@ -3133,6 +3195,7 @@ export type Swap_Filter = {
amountUSD_lte?: InputMaybe<Scalars['BigDecimal']>;
amountUSD_not?: InputMaybe<Scalars['BigDecimal']>;
amountUSD_not_in?: InputMaybe<Array<Scalars['BigDecimal']>>;
and?: InputMaybe<Array<InputMaybe<Swap_Filter>>>;
id?: InputMaybe<Scalars['ID']>;
id_gt?: InputMaybe<Scalars['ID']>;
id_gte?: InputMaybe<Scalars['ID']>;
@@ -3149,9 +3212,14 @@ export type Swap_Filter = {
logIndex_lte?: InputMaybe<Scalars['BigInt']>;
logIndex_not?: InputMaybe<Scalars['BigInt']>;
logIndex_not_in?: InputMaybe<Array<Scalars['BigInt']>>;
or?: InputMaybe<Array<InputMaybe<Swap_Filter>>>;
origin?: InputMaybe<Scalars['Bytes']>;
origin_contains?: InputMaybe<Scalars['Bytes']>;
origin_gt?: InputMaybe<Scalars['Bytes']>;
origin_gte?: InputMaybe<Scalars['Bytes']>;
origin_in?: InputMaybe<Array<Scalars['Bytes']>>;
origin_lt?: InputMaybe<Scalars['Bytes']>;
origin_lte?: InputMaybe<Scalars['Bytes']>;
origin_not?: InputMaybe<Scalars['Bytes']>;
origin_not_contains?: InputMaybe<Scalars['Bytes']>;
origin_not_in?: InputMaybe<Array<Scalars['Bytes']>>;
@@ -3178,13 +3246,21 @@ export type Swap_Filter = {
pool_starts_with_nocase?: InputMaybe<Scalars['String']>;
recipient?: InputMaybe<Scalars['Bytes']>;
recipient_contains?: InputMaybe<Scalars['Bytes']>;
recipient_gt?: InputMaybe<Scalars['Bytes']>;
recipient_gte?: InputMaybe<Scalars['Bytes']>;
recipient_in?: InputMaybe<Array<Scalars['Bytes']>>;
recipient_lt?: InputMaybe<Scalars['Bytes']>;
recipient_lte?: InputMaybe<Scalars['Bytes']>;
recipient_not?: InputMaybe<Scalars['Bytes']>;
recipient_not_contains?: InputMaybe<Scalars['Bytes']>;
recipient_not_in?: InputMaybe<Array<Scalars['Bytes']>>;
sender?: InputMaybe<Scalars['Bytes']>;
sender_contains?: InputMaybe<Scalars['Bytes']>;
sender_gt?: InputMaybe<Scalars['Bytes']>;
sender_gte?: InputMaybe<Scalars['Bytes']>;
sender_in?: InputMaybe<Array<Scalars['Bytes']>>;
sender_lt?: InputMaybe<Scalars['Bytes']>;
sender_lte?: InputMaybe<Scalars['Bytes']>;
sender_not?: InputMaybe<Scalars['Bytes']>;
sender_not_contains?: InputMaybe<Scalars['Bytes']>;
sender_not_in?: InputMaybe<Array<Scalars['Bytes']>>;
@@ -3339,6 +3415,7 @@ export type TickDayData = {
export type TickDayData_Filter = {
/** Filter for the block changed event. */
_change_block?: InputMaybe<BlockChangedFilter>;
and?: InputMaybe<Array<InputMaybe<TickDayData_Filter>>>;
date?: InputMaybe<Scalars['Int']>;
date_gt?: InputMaybe<Scalars['Int']>;
date_gte?: InputMaybe<Scalars['Int']>;
@@ -3395,6 +3472,7 @@ export type TickDayData_Filter = {
liquidityNet_lte?: InputMaybe<Scalars['BigInt']>;
liquidityNet_not?: InputMaybe<Scalars['BigInt']>;
liquidityNet_not_in?: InputMaybe<Array<Scalars['BigInt']>>;
or?: InputMaybe<Array<InputMaybe<TickDayData_Filter>>>;
pool?: InputMaybe<Scalars['String']>;
pool_?: InputMaybe<Pool_Filter>;
pool_contains?: InputMaybe<Scalars['String']>;
@@ -3495,6 +3573,7 @@ export type TickHourData = {
export type TickHourData_Filter = {
/** Filter for the block changed event. */
_change_block?: InputMaybe<BlockChangedFilter>;
and?: InputMaybe<Array<InputMaybe<TickHourData_Filter>>>;
feesUSD?: InputMaybe<Scalars['BigDecimal']>;
feesUSD_gt?: InputMaybe<Scalars['BigDecimal']>;
feesUSD_gte?: InputMaybe<Scalars['BigDecimal']>;
@@ -3527,6 +3606,7 @@ export type TickHourData_Filter = {
liquidityNet_lte?: InputMaybe<Scalars['BigInt']>;
liquidityNet_not?: InputMaybe<Scalars['BigInt']>;
liquidityNet_not_in?: InputMaybe<Array<Scalars['BigInt']>>;
or?: InputMaybe<Array<InputMaybe<TickHourData_Filter>>>;
periodStartUnix?: InputMaybe<Scalars['Int']>;
periodStartUnix_gt?: InputMaybe<Scalars['Int']>;
periodStartUnix_gte?: InputMaybe<Scalars['Int']>;
@@ -3619,6 +3699,7 @@ export enum TickHourData_OrderBy {
export type Tick_Filter = {
/** Filter for the block changed event. */
_change_block?: InputMaybe<BlockChangedFilter>;
and?: InputMaybe<Array<InputMaybe<Tick_Filter>>>;
collectedFeesToken0?: InputMaybe<Scalars['BigDecimal']>;
collectedFeesToken0_gt?: InputMaybe<Scalars['BigDecimal']>;
collectedFeesToken0_gte?: InputMaybe<Scalars['BigDecimal']>;
@@ -3715,6 +3796,7 @@ export type Tick_Filter = {
liquidityProviderCount_lte?: InputMaybe<Scalars['BigInt']>;
liquidityProviderCount_not?: InputMaybe<Scalars['BigInt']>;
liquidityProviderCount_not_in?: InputMaybe<Array<Scalars['BigInt']>>;
or?: InputMaybe<Array<InputMaybe<Tick_Filter>>>;
pool?: InputMaybe<Scalars['String']>;
poolAddress?: InputMaybe<Scalars['String']>;
poolAddress_contains?: InputMaybe<Scalars['String']>;
@@ -3898,6 +3980,7 @@ export type TokenDayData = {
export type TokenDayData_Filter = {
/** Filter for the block changed event. */
_change_block?: InputMaybe<BlockChangedFilter>;
and?: InputMaybe<Array<InputMaybe<TokenDayData_Filter>>>;
close?: InputMaybe<Scalars['BigDecimal']>;
close_gt?: InputMaybe<Scalars['BigDecimal']>;
close_gte?: InputMaybe<Scalars['BigDecimal']>;
@@ -3954,6 +4037,7 @@ export type TokenDayData_Filter = {
open_lte?: InputMaybe<Scalars['BigDecimal']>;
open_not?: InputMaybe<Scalars['BigDecimal']>;
open_not_in?: InputMaybe<Array<Scalars['BigDecimal']>>;
or?: InputMaybe<Array<InputMaybe<TokenDayData_Filter>>>;
priceUSD?: InputMaybe<Scalars['BigDecimal']>;
priceUSD_gt?: InputMaybe<Scalars['BigDecimal']>;
priceUSD_gte?: InputMaybe<Scalars['BigDecimal']>;
@@ -4063,6 +4147,7 @@ export type TokenHourData = {
export type TokenHourData_Filter = {
/** Filter for the block changed event. */
_change_block?: InputMaybe<BlockChangedFilter>;
and?: InputMaybe<Array<InputMaybe<TokenHourData_Filter>>>;
close?: InputMaybe<Scalars['BigDecimal']>;
close_gt?: InputMaybe<Scalars['BigDecimal']>;
close_gte?: InputMaybe<Scalars['BigDecimal']>;
@@ -4111,6 +4196,7 @@ export type TokenHourData_Filter = {
open_lte?: InputMaybe<Scalars['BigDecimal']>;
open_not?: InputMaybe<Scalars['BigDecimal']>;
open_not_in?: InputMaybe<Array<Scalars['BigDecimal']>>;
or?: InputMaybe<Array<InputMaybe<TokenHourData_Filter>>>;
periodStartUnix?: InputMaybe<Scalars['Int']>;
periodStartUnix_gt?: InputMaybe<Scalars['Int']>;
periodStartUnix_gte?: InputMaybe<Scalars['Int']>;
@@ -4210,6 +4296,7 @@ export enum TokenHourData_OrderBy {
export type Token_Filter = {
/** Filter for the block changed event. */
_change_block?: InputMaybe<BlockChangedFilter>;
and?: InputMaybe<Array<InputMaybe<Token_Filter>>>;
decimals?: InputMaybe<Scalars['BigInt']>;
decimals_gt?: InputMaybe<Scalars['BigInt']>;
decimals_gte?: InputMaybe<Scalars['BigInt']>;
@@ -4262,6 +4349,7 @@ export type Token_Filter = {
name_not_starts_with_nocase?: InputMaybe<Scalars['String']>;
name_starts_with?: InputMaybe<Scalars['String']>;
name_starts_with_nocase?: InputMaybe<Scalars['String']>;
or?: InputMaybe<Array<InputMaybe<Token_Filter>>>;
poolCount?: InputMaybe<Scalars['BigInt']>;
poolCount_gt?: InputMaybe<Scalars['BigInt']>;
poolCount_gte?: InputMaybe<Scalars['BigInt']>;
@@ -4446,6 +4534,7 @@ export type TransactionSwapsArgs = {
export type Transaction_Filter = {
/** Filter for the block changed event. */
_change_block?: InputMaybe<BlockChangedFilter>;
and?: InputMaybe<Array<InputMaybe<Transaction_Filter>>>;
blockNumber?: InputMaybe<Scalars['BigInt']>;
blockNumber_gt?: InputMaybe<Scalars['BigInt']>;
blockNumber_gte?: InputMaybe<Scalars['BigInt']>;
@@ -4482,6 +4571,7 @@ export type Transaction_Filter = {
id_not?: InputMaybe<Scalars['ID']>;
id_not_in?: InputMaybe<Array<Scalars['ID']>>;
mints_?: InputMaybe<Mint_Filter>;
or?: InputMaybe<Array<InputMaybe<Transaction_Filter>>>;
swaps_?: InputMaybe<Swap_Filter>;
timestamp?: InputMaybe<Scalars['BigInt']>;
timestamp_gt?: InputMaybe<Scalars['BigInt']>;
@@ -4521,6 +4611,7 @@ export type UniswapDayData = {
export type UniswapDayData_Filter = {
/** Filter for the block changed event. */
_change_block?: InputMaybe<BlockChangedFilter>;
and?: InputMaybe<Array<InputMaybe<UniswapDayData_Filter>>>;
date?: InputMaybe<Scalars['Int']>;
date_gt?: InputMaybe<Scalars['Int']>;
date_gte?: InputMaybe<Scalars['Int']>;
@@ -4545,6 +4636,7 @@ export type UniswapDayData_Filter = {
id_lte?: InputMaybe<Scalars['ID']>;
id_not?: InputMaybe<Scalars['ID']>;
id_not_in?: InputMaybe<Array<Scalars['ID']>>;
or?: InputMaybe<Array<InputMaybe<UniswapDayData_Filter>>>;
tvlUSD?: InputMaybe<Scalars['BigDecimal']>;
tvlUSD_gt?: InputMaybe<Scalars['BigDecimal']>;
tvlUSD_gte?: InputMaybe<Scalars['BigDecimal']>;

View File

@@ -77,8 +77,8 @@ function useAvatarFromNFT(nftUri = '', enforceOwnership: boolean): { avatar?: st
const [contractAddress, id] = parts[2]?.split('/') ?? []
const isERC721 = protocol === 'eip155' && erc === 'erc721'
const isERC1155 = protocol === 'eip155' && erc === 'erc1155'
const erc721 = useERC721Uri(isERC721 ? contractAddress : undefined, id, enforceOwnership)
const erc1155 = useERC1155Uri(isERC1155 ? contractAddress : undefined, id, enforceOwnership)
const erc721 = useERC721Uri(isERC721 ? contractAddress : undefined, isERC721 ? id : undefined, enforceOwnership)
const erc1155 = useERC1155Uri(isERC1155 ? contractAddress : undefined, isERC1155 ? id : undefined, enforceOwnership)
const uri = erc721.uri || erc1155.uri
const http = uri && uriToHttp(uri)[0]
@@ -136,14 +136,18 @@ function useERC1155Uri(
const contract = useERC1155Contract(contractAddress)
const balance = useSingleCallResult(contract, 'balanceOf', accountArgument)
const uri = useSingleCallResult(contract, 'uri', idArgument)
// ERC-1155 allows a generic {id} in the URL, so prepare to replace if relevant,
// in lowercase hexadecimal (with no 0x prefix) and leading zero padded to 64 hex characters.
const idHex = id ? hexZeroPad(BigNumber.from(id).toHexString(), 32).substring(2) : id
return useMemo(
() => ({
uri: !enforceOwnership || balance.result?.[0] > 0 ? uri.result?.[0]?.replaceAll('{id}', idHex) : undefined,
loading: balance.loading || uri.loading,
}),
[balance.loading, balance.result, enforceOwnership, uri.loading, uri.result, idHex]
)
return useMemo(() => {
try {
// ERC-1155 allows a generic {id} in the URL, so prepare to replace if relevant,
// in lowercase hexadecimal (with no 0x prefix) and leading zero padded to 64 hex characters.
const idHex = id ? hexZeroPad(BigNumber.from(id).toHexString(), 32).substring(2) : id
return {
uri: !enforceOwnership || balance.result?.[0] > 0 ? uri.result?.[0]?.replaceAll('{id}', idHex) : undefined,
loading: balance.loading || uri.loading,
}
} catch (error) {
console.error('Invalid token id', error)
return { loading: false }
}
}, [balance.loading, balance.result, enforceOwnership, uri.loading, uri.result, id])
}

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

View File

@@ -1,14 +1,11 @@
import {
AllowanceData,
AllowanceProvider,
AllowanceTransfer,
MaxAllowanceTransferAmount,
PERMIT2_ADDRESS,
PermitSingle,
} from '@uniswap/permit2-sdk'
import { Token } from '@uniswap/sdk-core'
import { signTypedData } from '@uniswap/conedison/provider'
import { AllowanceTransfer, MaxAllowanceTransferAmount, PERMIT2_ADDRESS, PermitSingle } from '@uniswap/permit2-sdk'
import { CurrencyAmount, Token } from '@uniswap/sdk-core'
import { useWeb3React } from '@web3-react/core'
import useBlockNumber from 'lib/hooks/useBlockNumber'
import PERMIT2_ABI from 'abis/permit2.json'
import { Permit2 } from 'abis/types'
import { useContract } from 'hooks/useContract'
import { useSingleCallResult } from 'lib/hooks/multicall'
import ms from 'ms.macro'
import { useCallback, useEffect, useMemo, useState } from 'react'
@@ -19,35 +16,28 @@ function toDeadline(expiration: number): number {
return Math.floor((Date.now() + expiration) / 1000)
}
export function usePermitAllowance(token?: Token, spender?: string) {
const { account, provider } = useWeb3React()
const allowanceProvider = useMemo(() => provider && new AllowanceProvider(provider, PERMIT2_ADDRESS), [provider])
const [allowanceData, setAllowanceData] = useState<AllowanceData>()
export function usePermitAllowance(token?: Token, owner?: string, spender?: string) {
const contract = useContract<Permit2>(PERMIT2_ADDRESS, PERMIT2_ABI)
const inputs = useMemo(() => [owner, token?.address, spender], [owner, spender, token?.address])
// If there is no allowanceData, recheck every block so a submitted allowance is immediately observed.
const blockNumber = useBlockNumber()
const shouldUpdate = allowanceData ? false : blockNumber
// If there is no allowance yet, re-check next observed block.
// This guarantees that the permitAllowance is synced upon submission and updated upon being synced.
const [blocksPerFetch, setBlocksPerFetch] = useState<1>()
const result = useSingleCallResult(contract, 'allowance', inputs, {
blocksPerFetch,
}).result as Awaited<ReturnType<Permit2['allowance']>> | undefined
useEffect(() => {
if (!account || !token || !spender) return
const rawAmount = result?.amount.toString() // convert to a string before using in a hook, to avoid spurious rerenders
const allowance = useMemo(
() => (token && rawAmount ? CurrencyAmount.fromRawAmount(token, rawAmount) : undefined),
[token, rawAmount]
)
useEffect(() => setBlocksPerFetch(allowance?.equalTo(0) ? 1 : undefined), [allowance])
allowanceProvider
?.getAllowanceData(token.address, account, spender)
.then((data) => {
if (stale) return
setAllowanceData(data)
})
.catch((e) => {
console.warn(`Failed to fetch allowance data: ${e}`)
})
let stale = false
return () => {
stale = true
}
}, [account, allowanceProvider, shouldUpdate, spender, token])
return allowanceData
return useMemo(
() => ({ permitAllowance: allowance, expiration: result?.expiration, nonce: result?.nonce }),
[allowance, result?.expiration, result?.nonce]
)
}
interface Permit extends PermitSingle {
@@ -86,12 +76,13 @@ export function useUpdatePermitAllowance(
}
const { domain, types, values } = AllowanceTransfer.getPermitData(permit, PERMIT2_ADDRESS, chainId)
const signature = await provider.getSigner(account)._signTypedData(domain, types, values)
// Use conedison's signTypedData for better x-wallet compatibility.
const signature = await signTypedData(provider.getSigner(account), domain, types, values)
onPermitSignature?.({ ...permit, signature })
return
} catch (e: unknown) {
const symbol = token?.symbol ?? 'Token'
throw new Error(`${symbol} permit failed: ${e instanceof Error ? e.message : e}`)
throw new Error(`${symbol} permit allowance failed: ${e instanceof Error ? e.message : e}`)
}
}, [account, chainId, nonce, onPermitSignature, provider, spender, token])
}

View File

@@ -48,7 +48,12 @@ export default function useStablecoinPrice(currency?: Currency): Price<Currency,
}, [currency, stablecoin, trade])
const lastPrice = useRef(price)
if (!price || !lastPrice.current || !price.equalTo(lastPrice.current)) {
if (
!price ||
!lastPrice.current ||
!price.equalTo(lastPrice.current) ||
!price.baseCurrency.equals(lastPrice.current.baseCurrency)
) {
lastPrice.current = price
}
return lastPrice.current

View File

@@ -2,6 +2,7 @@ import { Trade } from '@uniswap/router-sdk'
import { Currency, Percent, TradeType } from '@uniswap/sdk-core'
import { useWeb3React } from '@web3-react/core'
import { usePermit2Enabled } from 'featureFlags/flags/permit2'
import { PermitSignature } from 'hooks/usePermitAllowance'
import { SwapCallbackState, useSwapCallback as useLibSwapCallBack } from 'lib/hooks/swap/useSwapCallback'
import { ReactNode, useMemo } from 'react'
@@ -10,7 +11,6 @@ import { TransactionType } from '../state/transactions/types'
import { currencyId } from '../utils/currencyId'
import useENS from './useENS'
import { SignatureData } from './useERC20Permit'
import { Permit } from './usePermit2'
import useTransactionDeadline from './useTransactionDeadline'
import { useUniversalRouterSwapCallback } from './useUniversalRouter'
@@ -21,7 +21,7 @@ export function useSwapCallback(
allowedSlippage: Percent, // in bips
recipientAddressOrName: string | null, // the ENS name or address of the recipient of the trade, or null if swap should be returned to sender
signatureData: SignatureData | undefined | null,
permit: Permit | undefined
permitSignature: PermitSignature | undefined
): { state: SwapCallbackState; callback: null | (() => Promise<string>); error: ReactNode | null } {
const { account } = useWeb3React()
@@ -47,7 +47,7 @@ export function useSwapCallback(
const universalRouterSwapCallback = useUniversalRouterSwapCallback(permit2Enabled ? trade : undefined, {
slippageTolerance: allowedSlippage,
deadline,
permit: permit?.signature,
permit: permitSignature,
})
const swapCallback = permit2Enabled ? universalRouterSwapCallback : libCallback

View File

@@ -1,13 +1,12 @@
import { BigNumberish } from '@ethersproject/bignumber'
import { ContractTransaction } from '@ethersproject/contracts'
import { CurrencyAmount, MaxUint256, Token } from '@uniswap/sdk-core'
import { useTokenContract } from 'hooks/useContract'
import { useSingleCallResult } from 'lib/hooks/multicall'
import { useCallback, useMemo } from 'react'
import { useCallback, useEffect, useMemo, useState } from 'react'
import { ApproveTransactionInfo, TransactionType } from 'state/transactions/types'
import { calculateGasMargin } from 'utils/calculateGasMargin'
import { useTokenContract } from './useContract'
export function useTokenAllowance(
token?: Token,
owner?: string,
@@ -17,23 +16,33 @@ export function useTokenAllowance(
isSyncing: boolean
} {
const contract = useTokenContract(token?.address, false)
const inputs = useMemo(() => [owner, spender], [owner, spender])
const { result, syncing: isSyncing } = useSingleCallResult(contract, 'allowance', inputs)
return useMemo(() => {
const tokenAllowance = token && result && CurrencyAmount.fromRawAmount(token, result.toString())
return { tokenAllowance, isSyncing }
}, [isSyncing, result, token])
// If there is no allowance yet, re-check next observed block.
// This guarantees that the tokenAllowance is marked isSyncing upon approval and updated upon being synced.
const [blocksPerFetch, setBlocksPerFetch] = useState<1>()
const { result, syncing: isSyncing } = useSingleCallResult(contract, 'allowance', inputs, { blocksPerFetch }) as {
result: Awaited<ReturnType<NonNullable<typeof contract>['allowance']>> | undefined
syncing: boolean
}
const rawAmount = result?.toString() // convert to a string before using in a hook, to avoid spurious rerenders
const allowance = useMemo(
() => (token && rawAmount ? CurrencyAmount.fromRawAmount(token, rawAmount) : undefined),
[token, rawAmount]
)
useEffect(() => setBlocksPerFetch(allowance?.equalTo(0) ? 1 : undefined), [allowance])
return useMemo(() => ({ tokenAllowance: allowance, isSyncing }), [allowance, isSyncing])
}
export function useUpdateTokenAllowance(amount: CurrencyAmount<Token> | undefined, spender: string) {
export function useUpdateTokenAllowance(
amount: CurrencyAmount<Token> | undefined,
spender: string
): () => Promise<{ response: ContractTransaction; info: ApproveTransactionInfo }> {
const contract = useTokenContract(amount?.currency.address)
return useCallback(async (): Promise<{
response: ContractTransaction
info: ApproveTransactionInfo
}> => {
return useCallback(async () => {
try {
if (!amount) throw new Error('missing amount')
if (!contract) throw new Error('missing contract')
@@ -58,7 +67,7 @@ export function useUpdateTokenAllowance(amount: CurrencyAmount<Token> | undefine
}
} catch (e: unknown) {
const symbol = amount?.currency.symbol ?? 'Token'
throw new Error(`${symbol} approval failed: ${e instanceof Error ? e.message : e}`)
throw new Error(`${symbol} token allowance failed: ${e instanceof Error ? e.message : e}`)
}
}, [amount, contract, spender])
}

View File

@@ -1,10 +1,14 @@
import { TransactionResponse } from '@ethersproject/abstract-provider'
import { BigNumber } from '@ethersproject/bignumber'
import { t } from '@lingui/macro'
import { sendAnalyticsEvent } from '@uniswap/analytics'
import { SwapEventName } from '@uniswap/analytics-events'
import { Trade } from '@uniswap/router-sdk'
import { Currency, Percent, TradeType } from '@uniswap/sdk-core'
import { SwapRouter, UNIVERSAL_ROUTER_ADDRESS } from '@uniswap/universal-router-sdk'
import { FeeOptions, toHex } from '@uniswap/v3-sdk'
import { useWeb3React } from '@web3-react/core'
import { formatSwapSignedAnalyticsEventProperties } from 'lib/utils/analytics'
import { useCallback } from 'react'
import { calculateGasMargin } from 'utils/calculateGasMargin'
import isZero from 'utils/isZero'
@@ -12,6 +16,8 @@ import { swapErrorToUserReadableMessage } from 'utils/swapErrorToUserReadableMes
import { PermitSignature } from './usePermitAllowance'
class InvalidSwapError extends Error {}
interface SwapOptions {
slippageTolerance: Percent
deadline?: BigNumber
@@ -50,15 +56,30 @@ export function useUniversalRouterSwapCallback(
try {
gasEstimate = await provider.estimateGas(tx)
} catch (gasError) {
await provider.call(tx) // this should throw the actual error
throw new Error('unexpected issue with gas estimation; please try again')
console.warn(gasError)
throw new Error('Your swap is expected to fail')
}
const gasLimit = calculateGasMargin(gasEstimate)
const response = await provider.getSigner().sendTransaction({ ...tx, gasLimit })
const response = await provider
.getSigner()
.sendTransaction({ ...tx, gasLimit })
.then((response) => {
sendAnalyticsEvent(
SwapEventName.SWAP_SIGNED,
formatSwapSignedAnalyticsEventProperties({ trade, txHash: response.hash })
)
if (tx.data !== response.data) {
sendAnalyticsEvent(SwapEventName.SWAP_MODIFIED_IN_WALLET, { txHash: response.hash })
throw new InvalidSwapError(
t`Your swap was modified through your wallet. If this was a mistake, please cancel immediately or risk losing your funds.`
)
}
return response
})
return response
} catch (swapError: unknown) {
const message = swapErrorToUserReadableMessage(swapError)
throw new Error(message)
if (swapError instanceof InvalidSwapError) throw swapError
throw new Error(swapErrorToUserReadableMessage(swapError))
}
}, [
account,

View File

@@ -1,7 +1,7 @@
import { TransactionReceipt } from '@ethersproject/abstract-provider'
import { useWeb3React } from '@web3-react/core'
import { SupportedChainId } from 'constants/chains'
import useBlockNumber from 'lib/hooks/useBlockNumber'
import useBlockNumber, { useFastForwardBlockNumber } from 'lib/hooks/useBlockNumber'
import ms from 'ms.macro'
import { useCallback, useEffect } from 'react'
import { retry, RetryableError, RetryOptions } from 'utils/retry'
@@ -48,6 +48,7 @@ export default function Updater({ pendingTransactions, onCheck, onReceipt }: Upd
const { chainId, provider } = useWeb3React()
const lastBlockNumber = useBlockNumber()
const fastForwardBlockNumber = useFastForwardBlockNumber()
const getReceipt = useCallback(
(hash: string) => {
@@ -78,6 +79,7 @@ export default function Updater({ pendingTransactions, onCheck, onReceipt }: Upd
promise
.then((receipt) => {
if (receipt) {
fastForwardBlockNumber(receipt.blockNumber)
onReceipt({ chainId, hash, receipt })
} else {
onCheck({ chainId, hash, blockNumber: lastBlockNumber })
@@ -94,7 +96,7 @@ export default function Updater({ pendingTransactions, onCheck, onReceipt }: Upd
return () => {
cancels.forEach((cancel) => cancel())
}
}, [chainId, provider, lastBlockNumber, getReceipt, onReceipt, onCheck, pendingTransactions])
}, [chainId, provider, lastBlockNumber, getReceipt, onReceipt, onCheck, pendingTransactions, fastForwardBlockNumber])
return null
}

View File

@@ -6,6 +6,7 @@ const MISSING_PROVIDER = Symbol()
const BlockNumberContext = createContext<
| {
value?: number
fastForward(block: number): void
}
| typeof MISSING_PROVIDER
>(MISSING_PROVIDER)
@@ -23,6 +24,10 @@ export default function useBlockNumber(): number | undefined {
return useBlockNumberContext().value
}
export function useFastForwardBlockNumber(): (block: number) => void {
return useBlockNumberContext().fastForward
}
export function BlockNumberProvider({ children }: { children: ReactNode }) {
const { chainId: activeChainId, provider } = useWeb3React()
const [{ chainId, block }, setChainBlock] = useState<{ chainId?: number; block?: number }>({ chainId: activeChainId })
@@ -68,7 +73,16 @@ export function BlockNumberProvider({ children }: { children: ReactNode }) {
return void 0
}, [activeChainId, provider, onBlock, setChainBlock, windowVisible])
const blockValue = useMemo(() => (chainId === activeChainId ? block : undefined), [activeChainId, block, chainId])
const value = useMemo(() => ({ value: blockValue }), [blockValue])
const value = useMemo(
() => ({
value: chainId === activeChainId ? block : undefined,
fastForward: (update: number) => {
if (block && update > block) {
setChainBlock({ chainId: activeChainId, block: update })
}
},
}),
[activeChainId, block, chainId]
)
return <BlockNumberContext.Provider value={value}>{children}</BlockNumberContext.Provider>
}

View File

@@ -1,11 +1,25 @@
import defaultTokenList from '@uniswap/default-token-list'
import fetch from 'jest-fetch-mock'
import fetchTokenList, { DEFAULT_TOKEN_LIST } from './fetchTokenList'
describe.skip('fetchTokenList', () => {
fetch.enableMocks()
describe('fetchTokenList', () => {
const resolver = jest.fn()
beforeEach(() => {
jest.spyOn(console, 'debug').mockReturnValue(undefined)
resolver.mockReset()
})
it('throws on an invalid list url', async () => {
const url = 'https://example.com'
const url = 'https://example.com/invalid-tokenlist.json'
fetch.mockOnceIf(url, () => {
throw new Error()
})
await expect(fetchTokenList(url, resolver)).rejects.toThrow(`failed to fetch list: ${url}`)
expect(console.debug).toHaveBeenCalled()
expect(resolver).not.toHaveBeenCalled()
})
@@ -13,13 +27,15 @@ describe.skip('fetchTokenList', () => {
const url = 'example.eth'
const contenthash = '0xD3ADB33F'
resolver.mockResolvedValue(contenthash)
await expect(fetchTokenList(url, resolver)).rejects.toThrow()
await expect(fetchTokenList(url, resolver)).rejects.toThrow(
`failed to translate contenthash to URI: ${contenthash}`
)
expect(resolver).toHaveBeenCalledWith(url)
})
it('fetches and validates the default token list', async () => {
const list = await (await fetch(DEFAULT_TOKEN_LIST)).json()
await expect(fetchTokenList(DEFAULT_TOKEN_LIST, resolver)).resolves.toStrictEqual(list)
fetch.mockOnceIf(DEFAULT_TOKEN_LIST, () => Promise.resolve(JSON.stringify(defaultTokenList)))
await expect(fetchTokenList(DEFAULT_TOKEN_LIST, resolver)).resolves.toStrictEqual(defaultTokenList)
expect(resolver).not.toHaveBeenCalled()
})
})

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

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