Compare commits

...

60 Commits

Author SHA1 Message Date
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
140 changed files with 9454 additions and 11454 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

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

View File

@@ -41,7 +41,7 @@ describe('Testing nfts', () => {
})
it('should toggle buy now on details page', () => {
cy.visit(`#/nfts/asset/${COLLECTION_ADDRESS}/8565`)
cy.visit(`#/nfts/asset/${COLLECTION_ADDRESS}/2043`)
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('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,7 +131,7 @@
"@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",
@@ -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.25.1",
"@vanilla-extract/css": "^1.7.2",
"@vanilla-extract/css-utils": "^0.1.2",
"@vanilla-extract/dynamic": "^2.0.2",
@@ -169,7 +170,7 @@
"@web3-react/eip1193": "8.0.26-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.29-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",

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

@@ -1,5 +1,7 @@
import { BaseVariant, FeatureFlag, featureFlagSettings, useUpdateFlag } from 'featureFlags'
import { useFiatOnrampFlag } from 'featureFlags/flags/fiatOnramp'
import { NftListV2Variant, useNftListV2Flag } from 'featureFlags/flags/nftListV2'
import { PayWithAnyTokenVariant, usePayWithAnyTokenFlag } from 'featureFlags/flags/payWithAnyToken'
import { Permit2Variant, usePermit2Flag } from 'featureFlags/flags/permit2'
import { TraceJsonRpcVariant, useTraceJsonRpcFlag } from 'featureFlags/flags/traceJsonRpc'
import { useAtomValue, useUpdateAtom } from 'jotai/utils'
@@ -215,6 +217,18 @@ 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"
/>
<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

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

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

@@ -181,6 +181,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)}

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

@@ -1,81 +0,0 @@
import { Trans } from '@lingui/macro'
import { useWeb3React } from '@web3-react/core'
import { getChainInfoOrDefault, L2ChainInfo } from 'constants/chainInfo'
import { SupportedChainId } from 'constants/chains'
import { AlertTriangle } from 'react-feather'
import styled from 'styled-components/macro'
import { ExternalLink, MEDIA_WIDTHS } from 'theme'
const BodyRow = styled.div`
color: ${({ theme }) => theme.textPrimary};
font-size: 12px;
font-weight: 400;
font-size: 14px;
line-height: 20px;
`
const CautionTriangle = styled(AlertTriangle)`
color: ${({ theme }) => theme.accentWarning};
`
const Link = styled(ExternalLink)`
color: ${({ theme }) => theme.black};
text-decoration: underline;
`
const TitleRow = styled.div`
align-items: center;
display: flex;
justify-content: flex-start;
margin-bottom: 8px;
`
const TitleText = styled.div`
color: ${({ theme }) => theme.textPrimary};
font-weight: 500;
font-size: 16px;
line-height: 24px;
margin: 0px 12px;
`
const Wrapper = styled.div`
background-color: ${({ theme }) => theme.backgroundSurface};
border-radius: 12px;
border: 1px solid ${({ theme }) => theme.backgroundOutline};
bottom: 60px;
display: none;
max-width: 348px;
padding: 16px 20px;
position: fixed;
right: 16px;
@media screen and (min-width: ${MEDIA_WIDTHS.deprecated_upToMedium}px) {
display: block;
}
`
export function ChainConnectivityWarning() {
const { chainId } = useWeb3React()
const info = getChainInfoOrDefault(chainId)
const label = info?.label
return (
<Wrapper>
<TitleRow>
<CautionTriangle />
<TitleText>
<Trans>Network Warning</Trans>
</TitleText>
</TitleRow>
<BodyRow>
{chainId === SupportedChainId.MAINNET ? (
<Trans>You may have lost your network connection.</Trans>
) : (
<Trans>{label} might be down right now, or you may have lost your network connection.</Trans>
)}{' '}
{(info as L2ChainInfo).statusPage !== undefined && (
<span>
<Trans>Check network status</Trans>{' '}
<Link href={(info as L2ChainInfo).statusPage || ''}>
<Trans>here.</Trans>
</Link>
</span>
)}
</BodyRow>
</Wrapper>
)
}

View File

@@ -1,199 +0,0 @@
import { Trans } from '@lingui/macro'
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'
import styled, { keyframes } from 'styled-components/macro'
import { ExternalLink, ThemedText } from 'theme'
import { ExplorerDataType, getExplorerLink } from 'utils/getExplorerLink'
import { MouseoverTooltip } from '../Tooltip'
import { ChainConnectivityWarning } from './ChainConnectivityWarning'
const StyledPolling = styled.div`
align-items: center;
bottom: 0;
color: ${({ theme }) => theme.textTertiary};
display: none;
padding: 1rem;
position: fixed;
right: 0;
transition: 250ms ease color;
a {
color: unset;
}
a:hover {
color: unset;
text-decoration: none;
}
@media screen and (min-width: ${({ theme }) => theme.breakpoint.md}px) {
display: flex;
}
`
const StyledPollingBlockNumber = styled(ThemedText.DeprecatedSmall)<{
breathe: boolean
hovering: boolean
warning: boolean
}>`
color: ${({ theme, warning }) => (warning ? theme.deprecated_yellow3 : theme.accentSuccess)};
transition: opacity 0.25s ease;
opacity: ${({ breathe, hovering }) => (hovering ? 0.7 : breathe ? 1 : 0.5)};
:hover {
opacity: 1;
}
a {
color: unset;
}
a:hover {
text-decoration: none;
color: unset;
}
`
const StyledPollingDot = styled.div<{ warning: boolean }>`
width: 8px;
height: 8px;
min-height: 8px;
min-width: 8px;
border-radius: 50%;
position: relative;
background-color: ${({ theme, warning }) => (warning ? theme.deprecated_yellow3 : theme.accentSuccess)};
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);
}
to {
transform: rotate(360deg);
}
`
const Spinner = styled.div<{ warning: boolean }>`
animation: ${rotate360} 1s cubic-bezier(0.83, 0, 0.17, 1) infinite;
transform: translateZ(0);
border-top: 1px solid transparent;
border-right: 1px solid transparent;
border-bottom: 1px solid transparent;
border-left: 2px solid ${({ theme, warning }) => (warning ? theme.deprecated_yellow3 : theme.accentSuccess)};
background: transparent;
width: 14px;
height: 14px;
border-radius: 50%;
position: relative;
transition: 250ms ease border-color;
left: -3px;
top: -3px;
`
const DEFAULT_MS_BEFORE_WARNING = ms`10m`
const NETWORK_HEALTH_CHECK_MS = ms`10s`
export default function Polling() {
const { chainId } = useWeb3React()
const blockNumber = useBlockNumber()
const [isMounting, setIsMounting] = useState(false)
const [isHover, setIsHover] = useState(false)
const machineTime = useMachineTimeMs(NETWORK_HEALTH_CHECK_MS)
const blockTime = useCurrentBlockTimestamp()
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
const warning = Boolean(!!blockTime && machineTime - blockTime.mul(1000).toNumber() > waitMsBeforeWarning)
useEffect(
() => {
if (!blockNumber) {
return
}
setIsMounting(true)
const mountingTimer = setTimeout(() => setIsMounting(false), 1000)
// this will clear Timeout when component unmount like in willComponentUnmount
return () => {
clearTimeout(mountingTimer)
}
},
[blockNumber] //useEffect will run only one time
//if you pass a value to array, like this [data] than clearTimeout will run every time this value changes (useEffect re-run)
)
//TODO - chainlink gas oracle is really slow. Can we get a better data source?
const blockExternalLinkHref = useMemo(() => {
if (!chainId || !blockNumber) return ''
return getExplorerLink(chainId, blockNumber.toString(), ExplorerDataType.BLOCK)
}, [blockNumber, chainId])
if (isNftPage || isLandingPage) {
return null
}
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
text={<Trans>The most recent block number on this network. Prices update on every block.</Trans>}
>
{blockNumber}&ensp;
</MouseoverTooltip>
</ExternalLink>
</StyledPollingBlockNumber>
<StyledPollingDot warning={warning}>{isMounting && <Spinner warning={warning} />}</StyledPollingDot>{' '}
</StyledPolling>
{warning && <ChainConnectivityWarning />}
</RowFixed>
)
}

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

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

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

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

@@ -186,7 +186,7 @@ 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" />

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

@@ -378,15 +378,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,16 +442,13 @@ 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()
@@ -454,14 +459,13 @@ export const LoadedRow = forwardRef((props: LoadedRowProps, ref: ForwardedRef<HT
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 +473,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,7 +482,7 @@ export const LoadedRow = forwardRef((props: LoadedRowProps, ref: ForwardedRef<HT
>
<TokenRow
header={false}
listNumber={rank}
listNumber={volumeRank}
tokenInfo={
<ClickableName>
<LogoContainer>
@@ -486,8 +490,8 @@ export const LoadedRow = forwardRef((props: LoadedRowProps, ref: ForwardedRef<HT
<L2NetworkLogo networkUrl={L2Icon} />
</LogoContainer>
<TokenInfoCell>
<TokenName>{tokenName}</TokenName>
<TokenSymbol>{tokenSymbol}</TokenSymbol>
<TokenName data-cy="token-name">{token.name}</TokenName>
<TokenSymbol>{token.symbol}</TokenSymbol>
</TokenInfoCell>
</ClickableName>
}

View File

@@ -78,7 +78,7 @@ 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) {
@@ -103,13 +103,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

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

@@ -42,7 +42,7 @@ interface L1ChainInfo extends BaseChainInfo {
readonly defaultListUrl?: string
}
export interface L2ChainInfo extends BaseChainInfo {
interface L2ChainInfo extends BaseChainInfo {
readonly networkType: NetworkType.L2
readonly bridge: string
readonly statusPage?: string

View File

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

View File

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

@@ -119,6 +119,7 @@ export type TopToken = NonNullable<NonNullable<TopTokens100Query>['topTokens']>[
interface UseTopTokensReturnValue {
tokens: TopToken[] | undefined
tokenVolumeRank: Record<string, number>
loadingTokens: boolean
sparklines: SparklineMap
}
@@ -143,11 +144,30 @@ export function useTopTokens(chain: Chain): UseTopTokensReturnValue {
const { data, loading: loadingTokens } = useTopTokens100Query({
variables: { duration, chain },
})
const mappedTokens = useMemo(
const unwrappedTokens = useMemo(
() => data?.topTokens?.map((token) => unwrapToken(chainId, token)) ?? [],
[chainId, data]
)
const filteredTokens = useFilteredTokens(mappedTokens)
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

@@ -514,7 +514,6 @@ export type Portfolio = {
tokenBalances?: Maybe<Array<Maybe<TokenBalance>>>;
tokensTotalDenominatedValue?: Maybe<Amount>;
tokensTotalDenominatedValueChange?: Maybe<AmountChange>;
tokensTotalDenominatedValueHistory?: Maybe<Array<Maybe<TimestampedAmount>>>;
};
@@ -528,11 +527,6 @@ export type PortfolioTokensTotalDenominatedValueChangeArgs = {
duration?: InputMaybe<HistoryDuration>;
};
export type PortfolioTokensTotalDenominatedValueHistoryArgs = {
duration?: InputMaybe<HistoryDuration>;
};
export type Query = {
__typename?: 'Query';
assetActivities?: Maybe<Array<Maybe<AssetActivity>>>;

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

@@ -132,13 +132,21 @@ export type Burn_Filter = {
logIndex_not_in?: InputMaybe<Array<Scalars['BigInt']>>;
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']>>;
@@ -330,7 +338,11 @@ export type Collect_Filter = {
logIndex_not_in?: InputMaybe<Array<Scalars['BigInt']>>;
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']>>;
@@ -656,13 +668,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']>>;
@@ -785,13 +805,21 @@ export type Mint_Filter = {
logIndex_not_in?: InputMaybe<Array<Scalars['BigInt']>>;
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 +846,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']>>;
@@ -1916,7 +1948,11 @@ export type PositionSnapshot_Filter = {
liquidity_not_in?: InputMaybe<Array<Scalars['BigInt']>>;
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']>>;
@@ -2097,7 +2133,11 @@ export type Position_Filter = {
liquidity_not_in?: InputMaybe<Array<Scalars['BigInt']>>;
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']>>;
@@ -3151,7 +3191,11 @@ export type Swap_Filter = {
logIndex_not_in?: InputMaybe<Array<Scalars['BigInt']>>;
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 +3222,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']>>;

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,6 +0,0 @@
import { useLocation } from 'react-router-dom'
export function useIsLandingPage() {
const { pathname } = useLocation()
return pathname.endsWith('/')
}

View File

@@ -1,16 +0,0 @@
import useInterval from 'lib/hooks/useInterval'
import { useCallback, useState } from 'react'
const useMachineTimeMs = (updateInterval: number): number => {
const [now, setNow] = useState(Date.now())
useInterval(
useCallback(() => {
setNow(Date.now())
}, []),
updateInterval
)
return now
}
export default useMachineTimeMs

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,12 @@
import {
AllowanceData,
AllowanceProvider,
AllowanceTransfer,
MaxAllowanceTransferAmount,
PERMIT2_ADDRESS,
PermitSingle,
} from '@uniswap/permit2-sdk'
import { Token } from '@uniswap/sdk-core'
import 'utils/signTypedData'
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 +17,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 {
@@ -91,7 +82,7 @@ export function useUpdatePermitAllowance(
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

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

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

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

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

View File

@@ -3,6 +3,7 @@ import { formatEther } from '@ethersproject/units'
import { sendAnalyticsEvent } from '@uniswap/analytics'
import { NFTEventName } from '@uniswap/analytics-events'
import { useWeb3React } from '@web3-react/core'
import { NftListV2Variant, useNftListV2Flag } from 'featureFlags/flags/nftListV2'
import { useIsNftDetailsPage, useIsNftPage, useIsNftProfilePage } from 'hooks/useIsNftPage'
import { BagFooter } from 'nft/components/bag/BagFooter'
import ListingModal from 'nft/components/bag/profile/ListingModal'
@@ -135,6 +136,7 @@ const Bag = () => {
const isDetailsPage = useIsNftDetailsPage()
const isNFTPage = useIsNftPage()
const isMobile = useIsMobile()
const isNftListV2 = useNftListV2Flag() === NftListV2Variant.Enabled
const sendTransaction = useSendTransaction((state) => state.sendTransaction)
const transactionState = useSendTransaction((state) => state.state)
@@ -349,7 +351,7 @@ const Bag = () => {
color="white"
textAlign="center"
onClick={() => {
isMobile && toggleBag()
;(isMobile || isNftListV2) && toggleBag()
setProfilePageState(ProfilePageStateType.LISTING)
sendAnalyticsEvent(NFTEventName.NFT_PROFILE_PAGE_START_SELL, {
list_quantity: sellAssets.length,

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