Compare commits
54 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9be26788a2 | ||
|
|
ed393de481 | ||
|
|
cf5c393d97 | ||
|
|
68d81a0040 | ||
|
|
53caa51ac3 | ||
|
|
409ba72f9f | ||
|
|
9d9b3dca78 | ||
|
|
a11c7e9573 | ||
|
|
31bbcae1ed | ||
|
|
a1f6c7270e | ||
|
|
8471d9b46f | ||
|
|
5fc4d98faa | ||
|
|
8d9ddf36a2 | ||
|
|
6cfd5fa475 | ||
|
|
f2c5a7c09c | ||
|
|
fb52770953 | ||
|
|
94aa8ae2c9 | ||
|
|
6cb0824a0b | ||
|
|
777887b25d | ||
|
|
d15d5d85f5 | ||
|
|
43218d5655 | ||
|
|
a534ba41ed | ||
|
|
4715115743 | ||
|
|
3389d01213 | ||
|
|
d9a0aa3ff0 | ||
|
|
e9e5d2e43e | ||
|
|
d58dc14bd5 | ||
|
|
909e18cb23 | ||
|
|
a9ab5717de | ||
|
|
94544de74b | ||
|
|
96f24d5a9b | ||
|
|
8e59a352c0 | ||
|
|
3b765b4f05 | ||
|
|
9f4a1f48a5 | ||
|
|
de9533399a | ||
|
|
a02afd50b5 | ||
|
|
1f7ba5ae9f | ||
|
|
3686803c17 | ||
|
|
6f147c1ff3 | ||
|
|
049a09a346 | ||
|
|
4b9a885a34 | ||
|
|
e3918d039f | ||
|
|
9719af66e5 | ||
|
|
14b02eda0f | ||
|
|
60bc2a1660 | ||
|
|
ef3407f299 | ||
|
|
f312a148d0 | ||
|
|
cf5bb5740d | ||
|
|
5f280ffd0e | ||
|
|
97075acb91 | ||
|
|
6089d38daf | ||
|
|
3c6e067e90 | ||
|
|
fd1ee61daf | ||
|
|
de71f07b65 |
27
.github/workflows/revert.yaml
vendored
27
.github/workflows/revert.yaml
vendored
@@ -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 }}
|
||||
@@ -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')
|
||||
|
||||
92
cypress/e2e/token-details.test.ts
Normal file
92
cypress/e2e/token-details.test.ts
Normal 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")
|
||||
})
|
||||
})
|
||||
83
cypress/e2e/token-explore-filter.test.ts
Normal file
83
cypress/e2e/token-explore-filter.test.ts
Normal 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)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
74
cypress/e2e/token-explore.test.ts
Normal file
74
cypress/e2e/token-explore.test.ts
Normal 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')
|
||||
})
|
||||
})
|
||||
@@ -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')
|
||||
})
|
||||
})
|
||||
72
cypress/e2e/universal-search.test.ts
Normal file
72
cypress/e2e/universal-search.test.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
import { getTestSelector } from '../utils'
|
||||
|
||||
describe('Universal search bar', () => {
|
||||
before(() => {
|
||||
cy.visit('/')
|
||||
cy.get('[data-cy="magnifying-icon"]')
|
||||
.parent()
|
||||
.then(($navIcon) => {
|
||||
$navIcon.click()
|
||||
})
|
||||
})
|
||||
|
||||
it('should yield no results found when contract address is search term', () => {
|
||||
// Search for uni token contract address.
|
||||
cy.get('[data-cy="search-bar-input"]').last().type('0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984')
|
||||
cy.get('[data-cy="search-bar"]')
|
||||
.should('contain.text', 'No tokens found.')
|
||||
.and('contain.text', 'No NFT collections found.')
|
||||
})
|
||||
|
||||
it('should yield clickable result for regular token or nft collection search term', () => {
|
||||
// Search for uni token by name.
|
||||
cy.get('[data-cy="search-bar-input"]').last().clear().type('uni')
|
||||
cy.get('[data-cy="searchbar-token-row-UNI"]')
|
||||
.should('contain.text', 'Uniswap')
|
||||
.and('contain.text', 'UNI')
|
||||
.and('contain.text', '$')
|
||||
.and('contain.text', '%')
|
||||
cy.get('[data-cy="searchbar-token-row-UNI"]').click()
|
||||
|
||||
cy.get('div').contains('Uniswap').should('exist')
|
||||
// Stats should have: TVL, 24H Volume, 52W low, 52W high.
|
||||
cy.get(getTestSelector('token-details-stats')).should('exist')
|
||||
cy.get(getTestSelector('token-details-stats')).within(() => {
|
||||
cy.get('[data-cy="tvl"]').should('include.text', '$')
|
||||
cy.get('[data-cy="volume-24h"]').should('include.text', '$')
|
||||
cy.get('[data-cy="52w-low"]').should('include.text', '$')
|
||||
cy.get('[data-cy="52w-high"]').should('include.text', '$')
|
||||
})
|
||||
|
||||
// About section should have description of token.
|
||||
cy.get(getTestSelector('token-details-about-section')).should('exist')
|
||||
cy.contains('UNI is the governance token for Uniswap').should('exist')
|
||||
})
|
||||
|
||||
it('should show recent tokens and popular tokens with empty search term', () => {
|
||||
cy.get('[data-cy="magnifying-icon"]')
|
||||
.parent()
|
||||
.then(($navIcon) => {
|
||||
$navIcon.click()
|
||||
})
|
||||
// Recently searched UNI token should exist.
|
||||
cy.get('[data-cy="search-bar-input"]').last().clear()
|
||||
cy.get('[data-cy="searchbar-dropdown"]')
|
||||
.contains('[data-cy="searchbar-dropdown"]', 'Recent searches')
|
||||
.find('[data-cy="searchbar-token-row-UNI"]')
|
||||
.should('exist')
|
||||
|
||||
// Most popular 3 tokens should be shown.
|
||||
cy.get('[data-cy="searchbar-dropdown"]')
|
||||
.contains('[data-cy="searchbar-dropdown"]', 'Popular tokens')
|
||||
.find('[data-cy^="searchbar-token-row"]')
|
||||
.its('length')
|
||||
.should('be.eq', 3)
|
||||
})
|
||||
|
||||
it.skip('should show blocked badge when blocked token is searched for', () => {
|
||||
// Search for mTSLA, which is a blocked token.
|
||||
cy.get('[data-cy="search-bar-input"]').last().clear().type('mtsla')
|
||||
cy.get('[data-cy="searchbar-token-row-mTSLA"]').find('[data-cy="blocked-icon"]').should('exist')
|
||||
})
|
||||
})
|
||||
@@ -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",
|
||||
@@ -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
41
src/abis/permit2.json
Normal 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"
|
||||
}
|
||||
]
|
||||
@@ -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 {
|
||||
|
||||
@@ -1,6 +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'
|
||||
@@ -222,6 +223,12 @@ export default function FeatureFlagModal() {
|
||||
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}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)}
|
||||
|
||||
@@ -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 />
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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'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} 
|
||||
</MouseoverTooltip>
|
||||
</ExternalLink>
|
||||
</StyledPollingBlockNumber>
|
||||
<StyledPollingDot warning={warning}>{isMounting && <Spinner warning={warning} />}</StyledPollingDot>{' '}
|
||||
</StyledPolling>
|
||||
{warning && <ChainConnectivityWarning />}
|
||||
</RowFixed>
|
||||
)
|
||||
}
|
||||
@@ -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:
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}`}
|
||||
/>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}`}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
|
||||
@@ -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]}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -91,7 +91,6 @@ const IconWrap = styled.span`
|
||||
const DefaultMenuWrap = styled.div`
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 0 8px;
|
||||
`
|
||||
|
||||
const DefaultText = styled.span`
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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%;
|
||||
|
||||
@@ -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={[]} />)
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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])
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -5,12 +5,12 @@ import { useWeb3React } from '@web3-react/core'
|
||||
import { FiatOnrampAnnouncement } from 'components/FiatOnrampAnnouncement'
|
||||
import { IconWrapper } from 'components/Identicon/StatusIcon'
|
||||
import WalletDropdown from 'components/WalletDropdown'
|
||||
import { getConnection, getIsMetaMask } from 'connection/utils'
|
||||
import { getConnection } from 'connection/utils'
|
||||
import { Portal } from 'nft/components/common/Portal'
|
||||
import { useIsNftClaimAvailable } from 'nft/hooks/useIsNftClaimAvailable'
|
||||
import { getIsValidSwapQuote } from 'pages/Swap'
|
||||
import { darken } from 'polished'
|
||||
import { useCallback, useEffect, useMemo, useRef } from 'react'
|
||||
import { useCallback, useMemo, useRef } from 'react'
|
||||
import { AlertTriangle, ChevronDown, ChevronUp } from 'react-feather'
|
||||
import { useAppSelector } from 'state/hooks'
|
||||
import { useDerivedSwapInfo } from 'state/swap/hooks'
|
||||
@@ -22,7 +22,6 @@ import { useOnClickOutside } from '../../hooks/useOnClickOutside'
|
||||
import {
|
||||
useCloseModal,
|
||||
useModalIsOpen,
|
||||
useOpenMetamaskConnectionErrorModal,
|
||||
useToggleWalletDropdown,
|
||||
useToggleWalletModal,
|
||||
} from '../../state/application/hooks'
|
||||
@@ -35,7 +34,6 @@ import StatusIcon from '../Identicon/StatusIcon'
|
||||
import Loader from '../Loader'
|
||||
import { RowBetween } from '../Row'
|
||||
import WalletModal from '../WalletModal'
|
||||
import MetamaskConnectionError from './MetamaskConnectionError'
|
||||
|
||||
// https://stackoverflow.com/a/31617326
|
||||
const FULL_BORDER_RADIUS = 9999
|
||||
@@ -213,16 +211,10 @@ function Web3StatusInner() {
|
||||
toggleWalletDropdown()
|
||||
}, [toggleWalletDropdown])
|
||||
const toggleWalletModal = useToggleWalletModal()
|
||||
const openMetamaskConnectionErrorModal = useOpenMetamaskConnectionErrorModal()
|
||||
const walletIsOpen = useModalIsOpen(ApplicationModal.WALLET_DROPDOWN)
|
||||
const isClaimAvailable = useIsNftClaimAvailable((state) => state.isClaimAvailable)
|
||||
|
||||
const error = useAppSelector((state) => state.connection.errorByConnectionType[getConnection(connector).type])
|
||||
useEffect(() => {
|
||||
if (getIsMetaMask() && error) {
|
||||
openMetamaskConnectionErrorModal()
|
||||
}
|
||||
}, [error, openMetamaskConnectionErrorModal])
|
||||
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 />
|
||||
|
||||
@@ -26,6 +26,7 @@ import {
|
||||
getTokenAddress,
|
||||
} from 'lib/utils/analytics'
|
||||
import { useCallback, useState } from 'react'
|
||||
import { useToggleWalletModal } from 'state/application/hooks'
|
||||
import { useIsDarkMode } from 'state/user/hooks'
|
||||
import { computeRealizedPriceImpact } from 'utils/prices'
|
||||
import { switchChain } from 'utils/switchChain'
|
||||
@@ -57,6 +58,12 @@ export default function Widget({ token, onTokenChange, onReviewSwapClick }: Widg
|
||||
const { settings } = useSyncWidgetSettings()
|
||||
const { transactions } = useSyncWidgetTransactions()
|
||||
|
||||
const toggleWalletModal = useToggleWalletModal()
|
||||
const onConnectWalletClick = useCallback(() => {
|
||||
toggleWalletModal()
|
||||
return false // prevents the in-widget wallet modal from opening
|
||||
}, [toggleWalletModal])
|
||||
|
||||
const onSwitchChain = useCallback(
|
||||
// TODO(WEB-1757): Widget should not break if this rejects - upstream the catch to ignore it.
|
||||
({ chainId }: AddEthereumChainParameter) => switchChain(connector, Number(chainId)).catch(() => undefined),
|
||||
@@ -154,6 +161,7 @@ export default function Widget({ token, onTokenChange, onReviewSwapClick }: Widg
|
||||
theme={theme}
|
||||
width={WIDGET_WIDTH}
|
||||
// defaultChainId is excluded - it is always inferred from the passed provider
|
||||
onConnectWalletClick={onConnectWalletClick}
|
||||
provider={provider}
|
||||
onSwitchChain={onSwitchChain}
|
||||
tokenList={EMPTY_TOKEN_LIST} // prevents loading the default token list, as we use our own token selector UI
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -3,4 +3,5 @@ export enum FeatureFlag {
|
||||
traceJsonRpc = 'traceJsonRpc',
|
||||
permit2 = 'permit2',
|
||||
nftListV2 = 'nftListV2',
|
||||
payWithAnyToken = 'payWithAnyToken',
|
||||
}
|
||||
|
||||
7
src/featureFlags/flags/payWithAnyToken.ts
Normal file
7
src/featureFlags/flags/payWithAnyToken.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { BaseVariant, FeatureFlag, useBaseFlag } from '../index'
|
||||
|
||||
export function usePayWithAnyTokenFlag(): BaseVariant {
|
||||
return useBaseFlag(FeatureFlag.payWithAnyToken)
|
||||
}
|
||||
|
||||
export { BaseVariant as PayWithAnyTokenVariant }
|
||||
@@ -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 {
|
||||
|
||||
@@ -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]
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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>>>;
|
||||
|
||||
@@ -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])
|
||||
}
|
||||
|
||||
@@ -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']>>;
|
||||
|
||||
@@ -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])
|
||||
}
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
import { useLocation } from 'react-router-dom'
|
||||
|
||||
export function useIsLandingPage() {
|
||||
const { pathname } = useLocation()
|
||||
return pathname.endsWith('/')
|
||||
}
|
||||
@@ -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
|
||||
@@ -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,
|
||||
])
|
||||
}
|
||||
126
src/hooks/usePermit2Allowance.ts
Normal file
126
src/hooks/usePermit2Allowance.ts
Normal 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,
|
||||
])
|
||||
}
|
||||
@@ -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])
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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])
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
|
||||
@@ -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
@@ -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,
|
||||
|
||||
@@ -3,22 +3,29 @@ import { parseEther } from '@ethersproject/units'
|
||||
import { Trans } from '@lingui/macro'
|
||||
import { TraceEvent } from '@uniswap/analytics'
|
||||
import { BrowserEvent, InterfaceElementName, NFTEventName } from '@uniswap/analytics-events'
|
||||
import { Currency } from '@uniswap/sdk-core'
|
||||
import { useWeb3React } from '@web3-react/core'
|
||||
import Column from 'components/Column'
|
||||
import Loader from 'components/Loader'
|
||||
import CurrencyLogo from 'components/Logo/CurrencyLogo'
|
||||
import Row from 'components/Row'
|
||||
import { SupportedChainId } from 'constants/chains'
|
||||
import { PayWithAnyTokenVariant, usePayWithAnyTokenFlag } from 'featureFlags/flags/payWithAnyToken'
|
||||
import { useCurrency } from 'hooks/Tokens'
|
||||
import { useBag } from 'nft/hooks/useBag'
|
||||
import { useTokenInput } from 'nft/hooks/useTokenInput'
|
||||
import { useWalletBalance } from 'nft/hooks/useWalletBalance'
|
||||
import { BagStatus } from 'nft/types'
|
||||
import { ethNumberStandardFormatter, formatWeiToDecimal } from 'nft/utils'
|
||||
import { PropsWithChildren, useMemo } from 'react'
|
||||
import { AlertTriangle } from 'react-feather'
|
||||
import { PropsWithChildren, useMemo, useReducer } from 'react'
|
||||
import { AlertTriangle, ChevronDown } from 'react-feather'
|
||||
import { useToggleWalletModal } from 'state/application/hooks'
|
||||
import styled from 'styled-components/macro'
|
||||
import styled, { useTheme } from 'styled-components/macro'
|
||||
import { ThemedText } from 'theme'
|
||||
import { switchChain } from 'utils/switchChain'
|
||||
|
||||
import { BagTokenSelectorModal } from './tokenSelector/BagTokenSelectorModal'
|
||||
|
||||
const FooterContainer = styled.div`
|
||||
padding: 0px 12px;
|
||||
`
|
||||
@@ -39,6 +46,17 @@ const FooterHeader = styled(Column)<{ warningText?: boolean }>`
|
||||
padding-bottom: ${({ warningText }) => (warningText ? '8px' : '20px')};
|
||||
`
|
||||
|
||||
const CurrencyRow = styled(Row)<{ warningText?: boolean }>`
|
||||
padding-top: 4px;
|
||||
padding-bottom: ${({ warningText }) => (warningText ? '8px' : '20px')};
|
||||
justify-content: space-between;
|
||||
align-items: start;
|
||||
`
|
||||
|
||||
const TotalColumn = styled(Column)`
|
||||
text-align: end;
|
||||
`
|
||||
|
||||
const WarningIcon = styled(AlertTriangle)`
|
||||
width: 14px;
|
||||
margin-right: 4px;
|
||||
@@ -53,6 +71,11 @@ const WarningText = styled(ThemedText.BodyPrimary)`
|
||||
text-align: center;
|
||||
`
|
||||
|
||||
const CurrencyInput = styled(Row)`
|
||||
gap: 8px;
|
||||
cursor: pointer;
|
||||
`
|
||||
|
||||
const PayButton = styled(Row)<{ disabled?: boolean }>`
|
||||
background: ${({ theme }) => theme.accentAction};
|
||||
color: ${({ theme }) => theme.accentTextLightPrimary};
|
||||
@@ -116,10 +139,16 @@ export const BagFooter = ({
|
||||
eventProperties,
|
||||
}: BagFooterProps) => {
|
||||
const toggleWalletModal = useToggleWalletModal()
|
||||
const theme = useTheme()
|
||||
const { account, chainId, connector } = useWeb3React()
|
||||
const connected = Boolean(account && chainId)
|
||||
const shouldUsePayWithAnyToken = usePayWithAnyTokenFlag() === PayWithAnyTokenVariant.Enabled
|
||||
const inputCurrency = useTokenInput((state) => state.inputCurrency)
|
||||
const setInputCurrency = useTokenInput((state) => state.setInputCurrency)
|
||||
const defaultCurrency = useCurrency('ETH')
|
||||
|
||||
const setBagExpanded = useBag((state) => state.setBagExpanded)
|
||||
const [showTokenSelector, toggleTokenSelector] = useReducer((state) => !state, false)
|
||||
|
||||
const { balance: balanceInEth } = useWalletBalance()
|
||||
const sufficientBalance = useMemo(() => {
|
||||
@@ -168,28 +197,59 @@ export const BagFooter = ({
|
||||
}, [bagStatus, chainId, connected, connector, fetchAssets, setBagExpanded, sufficientBalance, toggleWalletModal])
|
||||
|
||||
const isPending = PENDING_BAG_STATUSES.includes(bagStatus)
|
||||
const activeCurrency = inputCurrency ?? defaultCurrency
|
||||
|
||||
return (
|
||||
<FooterContainer>
|
||||
<Footer>
|
||||
<FooterHeader gap="xs" warningText={!!warningText}>
|
||||
<Row justify="space-between">
|
||||
<div>
|
||||
<ThemedText.HeadlineSmall>Total</ThemedText.HeadlineSmall>
|
||||
</div>
|
||||
<div>
|
||||
{shouldUsePayWithAnyToken && (
|
||||
<CurrencyRow>
|
||||
<Column gap="xs">
|
||||
<ThemedText.SubHeaderSmall>
|
||||
<Trans>Pay with</Trans>
|
||||
</ThemedText.SubHeaderSmall>
|
||||
<CurrencyInput onClick={toggleTokenSelector}>
|
||||
<CurrencyLogo currency={activeCurrency} size="24px" />
|
||||
<ThemedText.HeadlineSmall fontWeight={500} lineHeight="24px">
|
||||
{activeCurrency?.symbol}
|
||||
</ThemedText.HeadlineSmall>
|
||||
<ChevronDown size={20} color={theme.textSecondary} />
|
||||
</CurrencyInput>
|
||||
</Column>
|
||||
<TotalColumn gap="xs">
|
||||
<ThemedText.SubHeaderSmall marginBottom="4px">
|
||||
<Trans>Total</Trans>
|
||||
</ThemedText.SubHeaderSmall>
|
||||
<ThemedText.HeadlineSmall>
|
||||
{formatWeiToDecimal(totalEthPrice.toString())} ETH
|
||||
{formatWeiToDecimal(totalEthPrice.toString())} {activeCurrency?.symbol ?? 'ETH'}
|
||||
</ThemedText.HeadlineSmall>
|
||||
</div>
|
||||
</Row>
|
||||
<Row justify="flex-end">
|
||||
<ThemedText.BodySmall color="textSecondary" lineHeight="20px">{`${ethNumberStandardFormatter(
|
||||
totalUsdPrice,
|
||||
true
|
||||
)}`}</ThemedText.BodySmall>
|
||||
</Row>
|
||||
</FooterHeader>
|
||||
<ThemedText.BodySmall color="textSecondary" lineHeight="20px">{`${ethNumberStandardFormatter(
|
||||
totalUsdPrice,
|
||||
true
|
||||
)}`}</ThemedText.BodySmall>
|
||||
</TotalColumn>
|
||||
</CurrencyRow>
|
||||
)}
|
||||
{!shouldUsePayWithAnyToken && (
|
||||
<FooterHeader gap="xs" warningText={!!warningText}>
|
||||
<Row justify="space-between">
|
||||
<div>
|
||||
<ThemedText.HeadlineSmall>Total</ThemedText.HeadlineSmall>
|
||||
</div>
|
||||
<div>
|
||||
<ThemedText.HeadlineSmall>
|
||||
{formatWeiToDecimal(totalEthPrice.toString())} ETH
|
||||
</ThemedText.HeadlineSmall>
|
||||
</div>
|
||||
</Row>
|
||||
<Row justify="flex-end">
|
||||
<ThemedText.BodySmall color="textSecondary" lineHeight="20px">{`${ethNumberStandardFormatter(
|
||||
totalUsdPrice,
|
||||
true
|
||||
)}`}</ThemedText.BodySmall>
|
||||
</Row>
|
||||
</FooterHeader>
|
||||
)}
|
||||
<TraceEvent
|
||||
events={[BrowserEvent.onClick]}
|
||||
name={NFTEventName.NFT_BUY_BAG_PAY}
|
||||
@@ -204,6 +264,16 @@ export const BagFooter = ({
|
||||
</ActionButton>
|
||||
</TraceEvent>
|
||||
</Footer>
|
||||
{showTokenSelector && (
|
||||
<BagTokenSelectorModal
|
||||
selectedCurrency={activeCurrency ?? undefined}
|
||||
handleCurrencySelect={(currency: Currency) => {
|
||||
setInputCurrency(currency)
|
||||
toggleTokenSelector()
|
||||
}}
|
||||
overlayClick={toggleTokenSelector}
|
||||
/>
|
||||
)}
|
||||
</FooterContainer>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -133,7 +133,7 @@ export const getTotalEthValue = (sellAssets: WalletAsset[]) => {
|
||||
}
|
||||
return total
|
||||
}, 0)
|
||||
return total ? Math.round(total * 100 + Number.EPSILON) / 100 : 0
|
||||
return total ? Math.round(total * 10000 + Number.EPSILON) / 10000 : 0
|
||||
}
|
||||
|
||||
export const getListings = (sellAssets: WalletAsset[]): [CollectionRow[], ListingRow[]] => {
|
||||
|
||||
119
src/nft/components/bag/tokenSelector/BagTokenSelectorModal.tsx
Normal file
119
src/nft/components/bag/tokenSelector/BagTokenSelectorModal.tsx
Normal file
@@ -0,0 +1,119 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import { Currency, Token } from '@uniswap/sdk-core'
|
||||
import Column from 'components/Column'
|
||||
import Row from 'components/Row'
|
||||
import { useAllTokens } from 'hooks/Tokens'
|
||||
import useNativeCurrency from 'lib/hooks/useNativeCurrency'
|
||||
import { tokenComparator } from 'lib/hooks/useTokenList/sorting'
|
||||
import { Portal } from 'nft/components/common/Portal'
|
||||
import { Overlay } from 'nft/components/modals/Overlay'
|
||||
import { useMemo } from 'react'
|
||||
import { X } from 'react-feather'
|
||||
import { useAllTokenBalances } from 'state/connection/hooks'
|
||||
import styled from 'styled-components/macro'
|
||||
import { ThemedText } from 'theme'
|
||||
import { Z_INDEX } from 'theme/zIndex'
|
||||
|
||||
import { CurrencyRow } from './CurrencyRow'
|
||||
|
||||
const ModalWrapper = styled(Column)`
|
||||
position: fixed;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 420px;
|
||||
height: 368px;
|
||||
z-index: ${Z_INDEX.modalOverTooltip};
|
||||
background: ${({ theme }) => theme.backgroundSurface};
|
||||
border-radius: 20px;
|
||||
border: ${({ theme }) => `1px solid ${theme.backgroundOutline}`};
|
||||
box-shadow: ${({ theme }) => theme.deepShadow};
|
||||
`
|
||||
|
||||
const TitleRow = styled(Row)`
|
||||
padding: 20px;
|
||||
justify-content: space-between;
|
||||
`
|
||||
|
||||
const TokenSelectorContainer = styled(Column)`
|
||||
border-top: 1px solid ${({ theme }) => theme.backgroundOutline};
|
||||
padding: 20px;
|
||||
height: 100%;
|
||||
overflow-y: scroll;
|
||||
gap: 8px;
|
||||
::-webkit-scrollbar {
|
||||
width: 0px;
|
||||
height: 0px;
|
||||
}
|
||||
`
|
||||
|
||||
interface BagTokenSelectorModalProps {
|
||||
selectedCurrency: Currency | undefined
|
||||
handleCurrencySelect: (currency: Currency) => void
|
||||
overlayClick: () => void
|
||||
}
|
||||
|
||||
export const BagTokenSelectorModal = ({
|
||||
selectedCurrency,
|
||||
handleCurrencySelect,
|
||||
overlayClick,
|
||||
}: BagTokenSelectorModalProps) => {
|
||||
const defaultTokens = useAllTokens()
|
||||
const [balances, balancesAreLoading] = useAllTokenBalances()
|
||||
const sortedTokens: Token[] = useMemo(
|
||||
() =>
|
||||
!balancesAreLoading
|
||||
? Object.values(defaultTokens)
|
||||
.filter((token) => {
|
||||
return balances[token.address]?.greaterThan(0)
|
||||
})
|
||||
.sort(tokenComparator.bind(null, balances))
|
||||
: [],
|
||||
[balances, balancesAreLoading, defaultTokens]
|
||||
)
|
||||
|
||||
const native = useNativeCurrency()
|
||||
const wrapped = native.wrapped
|
||||
|
||||
const currencies: Currency[] = useMemo(() => {
|
||||
const tokens = sortedTokens.filter((t) => !t.equals(wrapped))
|
||||
const natives: Currency[] = []
|
||||
if (native.equals(wrapped)) {
|
||||
natives.push(wrapped)
|
||||
} else {
|
||||
natives.push(native)
|
||||
if (balances[wrapped.address]?.greaterThan(0)) {
|
||||
natives.push(wrapped)
|
||||
}
|
||||
}
|
||||
return [...natives, ...tokens]
|
||||
}, [sortedTokens, native, wrapped, balances])
|
||||
|
||||
return (
|
||||
<Portal>
|
||||
<ModalWrapper>
|
||||
<TitleRow>
|
||||
<ThemedText.SubHeader fontWeight={500} lineHeight="24px">
|
||||
<Trans>Select a token</Trans>
|
||||
</ThemedText.SubHeader>
|
||||
<X size={24} cursor="pointer" onClick={overlayClick} />
|
||||
</TitleRow>
|
||||
<TokenSelectorContainer>
|
||||
{currencies.map((currency) => {
|
||||
return (
|
||||
<CurrencyRow
|
||||
key={currency.isToken ? currency.wrapped.address : currency.name}
|
||||
currency={currency}
|
||||
selected={
|
||||
(!selectedCurrency && currency.isNative) || (!!selectedCurrency && selectedCurrency.equals(currency))
|
||||
}
|
||||
selectCurrency={handleCurrencySelect}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</TokenSelectorContainer>
|
||||
</ModalWrapper>
|
||||
<Overlay onClick={overlayClick} />
|
||||
</Portal>
|
||||
)
|
||||
}
|
||||
70
src/nft/components/bag/tokenSelector/CurrencyRow.tsx
Normal file
70
src/nft/components/bag/tokenSelector/CurrencyRow.tsx
Normal file
@@ -0,0 +1,70 @@
|
||||
import { Currency, CurrencyAmount } from '@uniswap/sdk-core'
|
||||
import { useWeb3React } from '@web3-react/core'
|
||||
import Column from 'components/Column'
|
||||
import CurrencyLogo from 'components/Logo/CurrencyLogo'
|
||||
import Row from 'components/Row'
|
||||
import useCurrencyBalance from 'lib/hooks/useCurrencyBalance'
|
||||
import { Check } from 'react-feather'
|
||||
import styled from 'styled-components/macro'
|
||||
import { ThemedText } from 'theme'
|
||||
|
||||
const TokenRow = styled(Row)`
|
||||
padding: 8px 0px;
|
||||
gap: 12px;
|
||||
justify-content: space-between;
|
||||
cursor: pointer;
|
||||
`
|
||||
|
||||
const TokenInfoRow = styled(Row)`
|
||||
gap: 8px;
|
||||
`
|
||||
|
||||
const StyledBalanceText = styled(ThemedText.SubHeader)`
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
text-overflow: ellipsis;
|
||||
text-align: right;
|
||||
`
|
||||
|
||||
const StyledCheck = styled(Check)`
|
||||
color: ${({ theme }) => theme.accentAction};
|
||||
flex-shrink: 0;
|
||||
`
|
||||
|
||||
interface CurrencyRowProps {
|
||||
currency: Currency
|
||||
selected: boolean
|
||||
selectCurrency: (currency: Currency) => void
|
||||
}
|
||||
|
||||
export const CurrencyRow = ({ currency, selected, selectCurrency }: CurrencyRowProps) => {
|
||||
const { account } = useWeb3React()
|
||||
const balance = useCurrencyBalance(account ?? undefined, currency)
|
||||
|
||||
return (
|
||||
<TokenRow onClick={() => selectCurrency(currency)}>
|
||||
<TokenInfoRow>
|
||||
<CurrencyLogo currency={currency} size="36px" />
|
||||
<Column>
|
||||
<ThemedText.SubHeader fontWeight={500} lineHeight="24px">
|
||||
{currency.name}
|
||||
</ThemedText.SubHeader>
|
||||
<ThemedText.BodySmall lineHeight="20px" color="textSecondary">
|
||||
{currency.symbol}
|
||||
</ThemedText.BodySmall>
|
||||
</Column>
|
||||
</TokenInfoRow>
|
||||
{balance && <Balance balance={balance} />}
|
||||
{selected && <StyledCheck size={20} />}
|
||||
</TokenRow>
|
||||
)
|
||||
}
|
||||
|
||||
const Balance = ({ balance }: { balance: CurrencyAmount<Currency> }) => {
|
||||
return (
|
||||
<StyledBalanceText fontWeight={500} lineHeight="24px">
|
||||
{balance.toSignificant(4)}
|
||||
</StyledBalanceText>
|
||||
)
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user