Compare commits
104 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f232643d8e | ||
|
|
527270e33f | ||
|
|
18cd5ec9d9 | ||
|
|
5ddb565805 | ||
|
|
f0b4b92b88 | ||
|
|
4d82f9fb3a | ||
|
|
654b26dc54 | ||
|
|
c0753ae52f | ||
|
|
16bb9470ae | ||
|
|
6dcfca24cb | ||
|
|
9cac9f8299 | ||
|
|
5def0dd166 | ||
|
|
7229637c4c | ||
|
|
f26b09537d | ||
|
|
8f922b665a | ||
|
|
134b1d708f | ||
|
|
e9bddcb670 | ||
|
|
19e45fd119 | ||
|
|
ae4135fa49 | ||
|
|
89e438bcc5 | ||
|
|
92af2167ee | ||
|
|
db6084d717 | ||
|
|
927d35d59e | ||
|
|
b4e981b2fd | ||
|
|
967a698178 | ||
|
|
7818426b53 | ||
|
|
93e0054f10 | ||
|
|
661d2b6a33 | ||
|
|
c560b94366 | ||
|
|
93a4f00287 | ||
|
|
48833f27e3 | ||
|
|
35a03e2681 | ||
|
|
ac0badfb1d | ||
|
|
149b18f02e | ||
|
|
52a43f3db0 | ||
|
|
0a2a46d506 | ||
|
|
a7c1bd4391 | ||
|
|
13221e6935 | ||
|
|
26fc3caa55 | ||
|
|
6072bb1be0 | ||
|
|
302af21a22 | ||
|
|
b61a2d4111 | ||
|
|
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 | ||
|
|
59f9c6c2d8 | ||
|
|
3efcd3b23a | ||
|
|
726640787d | ||
|
|
889cdf6b66 | ||
|
|
400666cd0b | ||
|
|
7f4fe6cc9b | ||
|
|
dce891ddbd | ||
|
|
bc9bb39a8f |
@@ -8,4 +8,4 @@ REACT_APP_MOONPAY_LINK="https://us-central1-uniswap-mobile.cloudfunctions.net/si
|
||||
REACT_APP_MOONPAY_PUBLISHABLE_KEY="pk_live_uQG4BJC4w3cxnqpcSqAfohdBFDTsY6E"
|
||||
REACT_APP_FIREBASE_KEY="AIzaSyBcZWwTcTJHj_R6ipZcrJkXdq05PuX0Rs0"
|
||||
THE_GRAPH_SCHEMA_ENDPOINT="https://api.thegraph.com/subgraphs/name/uniswap/uniswap-v3"
|
||||
REACT_APP_SENTRY_ENABLED=true
|
||||
REACT_APP_SENTRY_ENABLED=false
|
||||
|
||||
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 }}
|
||||
5
.github/workflows/test.yml
vendored
5
.github/workflows/test.yml
vendored
@@ -30,6 +30,11 @@ jobs:
|
||||
- uses: ./.github/actions/setup
|
||||
- run: yarn prepare
|
||||
- run: yarn test
|
||||
- uses: codecov/codecov-action@v3
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
fail_ci_if_error: false
|
||||
verbose: true
|
||||
|
||||
cypress-build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
10
README.md
10
README.md
@@ -1,5 +1,7 @@
|
||||
# Uniswap Labs Interface
|
||||
|
||||
[](https://codecov.io/gh/Uniswap/interface)
|
||||
|
||||
[](https://github.com/Uniswap/interface/actions/workflows/unit-tests.yaml)
|
||||
[](https://github.com/Uniswap/interface/actions/workflows/integration-tests.yaml)
|
||||
[](https://github.com/Uniswap/interface/actions/workflows/lint.yml)
|
||||
@@ -40,10 +42,10 @@ For steps on local deployment, development, and code contribution, please see [C
|
||||
|
||||
The Uniswap Interface supports swapping, adding liquidity, removing liquidity and migrating liquidity for Uniswap protocol V2.
|
||||
|
||||
- Swap on Uniswap V2: https://app.uniswap.org/#/swap?use=v2
|
||||
- View V2 liquidity: https://app.uniswap.org/#/pool/v2
|
||||
- Add V2 liquidity: https://app.uniswap.org/#/add/v2
|
||||
- Migrate V2 liquidity to V3: https://app.uniswap.org/#/migrate/v2
|
||||
- Swap on Uniswap V2: <https://app.uniswap.org/#/swap?use=v2>
|
||||
- View V2 liquidity: <https://app.uniswap.org/#/pool/v2>
|
||||
- Add V2 liquidity: <https://app.uniswap.org/#/add/v2>
|
||||
- Migrate V2 liquidity to V3: <https://app.uniswap.org/#/migrate/v2>
|
||||
|
||||
## Accessing Uniswap V1
|
||||
|
||||
|
||||
@@ -9,6 +9,17 @@ module.exports = {
|
||||
babel: {
|
||||
plugins: ['@vanilla-extract/babel-plugin'],
|
||||
},
|
||||
jest: {
|
||||
configure(jestConfig) {
|
||||
return Object.assign({}, jestConfig, {
|
||||
transformIgnorePatterns: ['@uniswap/conedison/format', '@uniswap/conedison/provider'],
|
||||
moduleNameMapper: {
|
||||
'@uniswap/conedison/format': '@uniswap/conedison/dist/format',
|
||||
'@uniswap/conedison/provider': '@uniswap/conedison/dist/provider',
|
||||
},
|
||||
})
|
||||
},
|
||||
},
|
||||
webpack: {
|
||||
plugins: [
|
||||
new VanillaExtractPlugin(),
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { getTestSelector } from '../utils'
|
||||
|
||||
const COLLECTION_ADDRESS = '0xbd3531da5cf5857e7cfaa92426877b022e612cf8'
|
||||
const PUDGY_COLLECTION_ADDRESS = '0xbd3531da5cf5857e7cfaa92426877b022e612cf8'
|
||||
const BONSAI_COLLECTION_ADDRESS = '0xec9c519d49856fd2f8133a0741b4dbe002ce211b'
|
||||
|
||||
describe('Testing nfts', () => {
|
||||
beforeEach(() => {
|
||||
@@ -16,7 +17,7 @@ describe('Testing nfts', () => {
|
||||
})
|
||||
|
||||
it('should load pudgy penguin collection page', () => {
|
||||
cy.visit(`/#/nfts/collection/${COLLECTION_ADDRESS}`)
|
||||
cy.visit(`/#/nfts/collection/${PUDGY_COLLECTION_ADDRESS}`)
|
||||
cy.get(getTestSelector('nft-collection-asset')).should('exist')
|
||||
cy.get(getTestSelector('nft-collection-filter-buy-now')).should('not.exist')
|
||||
cy.get(getTestSelector('nft-filter')).first().click()
|
||||
@@ -24,13 +25,13 @@ describe('Testing nfts', () => {
|
||||
})
|
||||
|
||||
it('should be able to navigate to activity', () => {
|
||||
cy.visit(`/#/nfts/collection/${COLLECTION_ADDRESS}`)
|
||||
cy.visit(`/#/nfts/collection/${PUDGY_COLLECTION_ADDRESS}`)
|
||||
cy.get(getTestSelector('nft-activity')).first().click()
|
||||
cy.get(getTestSelector('nft-activity-row')).should('exist')
|
||||
})
|
||||
|
||||
it('should go to the details page', () => {
|
||||
cy.visit(`/#/nfts/collection/${COLLECTION_ADDRESS}`)
|
||||
cy.visit(`/#/nfts/collection/${PUDGY_COLLECTION_ADDRESS}`)
|
||||
cy.get(getTestSelector('nft-filter')).first().click()
|
||||
cy.get(getTestSelector('nft-collection-filter-buy-now')).click()
|
||||
cy.get(getTestSelector('nft-details-link')).first().click()
|
||||
@@ -41,7 +42,7 @@ describe('Testing nfts', () => {
|
||||
})
|
||||
|
||||
it('should toggle buy now on details page', () => {
|
||||
cy.visit(`#/nfts/asset/${COLLECTION_ADDRESS}/8565`)
|
||||
cy.visit(`#/nfts/asset/${BONSAI_COLLECTION_ADDRESS}/7580`)
|
||||
cy.get(getTestSelector('nft-details-description-text')).should('exist')
|
||||
cy.get(getTestSelector('nft-details-description')).click()
|
||||
cy.get(getTestSelector('nft-details-description-text')).should('not.exist')
|
||||
|
||||
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')
|
||||
})
|
||||
})
|
||||
17
package.json
17
package.json
@@ -96,6 +96,7 @@
|
||||
"cypress": "^10.3.1",
|
||||
"env-cmd": "^10.1.0",
|
||||
"eslint": "^7.11.0",
|
||||
"jest-fetch-mock": "^3.0.3",
|
||||
"jest-styled-components": "^7.0.8",
|
||||
"ms.macro": "^2.0.0",
|
||||
"patch-package": "^6.4.7",
|
||||
@@ -130,11 +131,11 @@
|
||||
"@reach/portal": "^0.10.3",
|
||||
"@react-hook/window-scroll": "^1.3.0",
|
||||
"@reduxjs/toolkit": "^1.6.1",
|
||||
"@sentry/react": "7.20.1",
|
||||
"@sentry/react": "^7.29.0",
|
||||
"@types/react-window-infinite-loader": "^1.0.6",
|
||||
"@uniswap/analytics": "1.2.0",
|
||||
"@uniswap/analytics-events": "^2.1.0",
|
||||
"@uniswap/conedison": "^1.1.1",
|
||||
"@uniswap/conedison": "^1.3.0",
|
||||
"@uniswap/governance": "^1.0.2",
|
||||
"@uniswap/liquidity-staker": "^1.0.2",
|
||||
"@uniswap/merkle-distributor": "1.0.1",
|
||||
@@ -144,14 +145,14 @@
|
||||
"@uniswap/sdk-core": "^3.0.1",
|
||||
"@uniswap/smart-order-router": "^2.10.0",
|
||||
"@uniswap/token-lists": "^1.0.0-beta.30",
|
||||
"@uniswap/universal-router-sdk": "1.3.0",
|
||||
"@uniswap/universal-router-sdk": "1.3.4",
|
||||
"@uniswap/v2-core": "1.0.0",
|
||||
"@uniswap/v2-periphery": "^1.1.0-beta.0",
|
||||
"@uniswap/v2-sdk": "^3.0.1",
|
||||
"@uniswap/v3-core": "1.0.0",
|
||||
"@uniswap/v3-periphery": "^1.1.1",
|
||||
"@uniswap/v3-sdk": "^3.9.0",
|
||||
"@uniswap/widgets": "2.22.11",
|
||||
"@uniswap/widgets": "^2.27.0",
|
||||
"@vanilla-extract/css": "^1.7.2",
|
||||
"@vanilla-extract/css-utils": "^0.1.2",
|
||||
"@vanilla-extract/dynamic": "^2.0.2",
|
||||
@@ -164,16 +165,16 @@
|
||||
"@visx/responsive": "^2.10.0",
|
||||
"@visx/shape": "^2.11.1",
|
||||
"@walletconnect/ethereum-provider": "^1.8.0",
|
||||
"@web3-react/coinbase-wallet": "8.0.34-beta.0",
|
||||
"@web3-react/coinbase-wallet": "8.0.35-beta.0",
|
||||
"@web3-react/core": "8.0.35-beta.0",
|
||||
"@web3-react/eip1193": "8.0.26-beta.0",
|
||||
"@web3-react/eip1193": "8.0.27-beta.0",
|
||||
"@web3-react/empty": "8.0.20-beta.0",
|
||||
"@web3-react/gnosis-safe": "8.0.7-beta.0",
|
||||
"@web3-react/metamask": "8.0.28-beta.0",
|
||||
"@web3-react/metamask": "8.0.30-beta.0",
|
||||
"@web3-react/network": "8.0.27-beta.0",
|
||||
"@web3-react/types": "8.0.20-beta.0",
|
||||
"@web3-react/url": "8.0.25-beta.0",
|
||||
"@web3-react/walletconnect": "8.0.36-beta.0",
|
||||
"@web3-react/walletconnect": "8.0.37-beta.0",
|
||||
"array.prototype.flat": "^1.2.4",
|
||||
"array.prototype.flatmap": "^1.2.4",
|
||||
"cids": "^1.0.0",
|
||||
|
||||
41
src/abis/permit2.json
Normal file
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,9 +1,8 @@
|
||||
import { TraceEvent } from '@uniswap/analytics'
|
||||
import { BrowserEvent, InterfaceElementName, SharedEventName } from '@uniswap/analytics-events'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { useIsDarkMode } from 'state/user/hooks'
|
||||
import styled from 'styled-components/macro'
|
||||
import { BREAKPOINTS, ExternalLink } from 'theme'
|
||||
import { BREAKPOINTS, ExternalLink, StyledRouterLink } from 'theme'
|
||||
|
||||
import { DiscordIcon, GithubIcon, TwitterIcon } from './Icons'
|
||||
import darkUnicornImgSrc from './images/unicornEmbossDark.png'
|
||||
@@ -97,14 +96,10 @@ const ExternalTextLink = styled(ExternalLink)`
|
||||
color: ${({ theme }) => theme.textSecondary};
|
||||
`
|
||||
|
||||
const TextLink = styled(Link)`
|
||||
const TextLink = styled(StyledRouterLink)`
|
||||
font-size: 16px;
|
||||
line-height: 20px;
|
||||
color: ${({ theme }) => theme.textSecondary};
|
||||
text-decoration: none;
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
`
|
||||
|
||||
const Copyright = styled.span`
|
||||
@@ -120,7 +115,7 @@ const LogoSectionContent = () => {
|
||||
<>
|
||||
<StyledLogo src={isDarkMode ? darkUnicornImgSrc : lightUnicornImgSrc} alt="Uniswap Logo" />
|
||||
<SocialLinks>
|
||||
<SocialLink href="https://github.com/Uniswap" target="_blank" rel="noopener noreferrer">
|
||||
<SocialLink href="https://discord.gg/FCfyBSbCU5" target="_blank" rel="noopener noreferrer">
|
||||
<DiscordIcon size={32} />
|
||||
</SocialLink>
|
||||
<TraceEvent
|
||||
@@ -132,7 +127,7 @@ const LogoSectionContent = () => {
|
||||
<TwitterIcon size={32} />
|
||||
</SocialLink>
|
||||
</TraceEvent>
|
||||
<SocialLink href="https://discord.gg/FCfyBSbCU5" target="_blank" rel="noopener noreferrer">
|
||||
<SocialLink href="https://github.com/Uniswap" target="_blank" rel="noopener noreferrer">
|
||||
<GithubIcon size={32} />
|
||||
</SocialLink>
|
||||
</SocialLinks>
|
||||
@@ -158,15 +153,9 @@ export const AboutFooter = () => {
|
||||
</LinkGroup>
|
||||
<LinkGroup>
|
||||
<LinkGroupTitle>Protocol</LinkGroupTitle>
|
||||
<ExternalTextLink href="https://uniswap.org/community" target="_blank" rel="noopener noreferrer">
|
||||
Community
|
||||
</ExternalTextLink>
|
||||
<ExternalTextLink href="https://uniswap.org/governance" target="_blank" rel="noopener noreferrer">
|
||||
Governance
|
||||
</ExternalTextLink>
|
||||
<ExternalTextLink href="https://uniswap.org/developers" target="_blank" rel="noopener noreferrer">
|
||||
Developers
|
||||
</ExternalTextLink>
|
||||
<ExternalTextLink href="https://uniswap.org/community">Community</ExternalTextLink>
|
||||
<ExternalTextLink href="https://uniswap.org/governance">Governance</ExternalTextLink>
|
||||
<ExternalTextLink href="https://uniswap.org/developers">Developers</ExternalTextLink>
|
||||
</LinkGroup>
|
||||
<LinkGroup>
|
||||
<LinkGroupTitle>Company</LinkGroupTitle>
|
||||
@@ -175,18 +164,14 @@ export const AboutFooter = () => {
|
||||
name={SharedEventName.ELEMENT_CLICKED}
|
||||
element={InterfaceElementName.CAREERS_LINK}
|
||||
>
|
||||
<ExternalTextLink href="https://boards.greenhouse.io/uniswaplabs" target="_blank" rel="noopener noreferrer">
|
||||
Careers
|
||||
</ExternalTextLink>
|
||||
<ExternalTextLink href="https://boards.greenhouse.io/uniswaplabs">Careers</ExternalTextLink>
|
||||
</TraceEvent>
|
||||
<TraceEvent
|
||||
events={[BrowserEvent.onClick]}
|
||||
name={SharedEventName.ELEMENT_CLICKED}
|
||||
element={InterfaceElementName.BLOG_LINK}
|
||||
>
|
||||
<ExternalTextLink href="https://uniswap.org/blog" target="_blank" rel="noopener noreferrer">
|
||||
Blog
|
||||
</ExternalTextLink>
|
||||
<ExternalTextLink href="https://uniswap.org/blog">Blog</ExternalTextLink>
|
||||
</TraceEvent>
|
||||
</LinkGroup>
|
||||
<LinkGroup>
|
||||
@@ -209,9 +194,7 @@ export const AboutFooter = () => {
|
||||
name={SharedEventName.ELEMENT_CLICKED}
|
||||
element={InterfaceElementName.SUPPORT_LINK}
|
||||
>
|
||||
<ExternalTextLink href="https://support.uniswap.org/hc/en-us" target="_blank" rel="noopener noreferrer">
|
||||
Help Center
|
||||
</ExternalTextLink>
|
||||
<ExternalTextLink href="https://support.uniswap.org/hc/en-us">Help Center</ExternalTextLink>
|
||||
</TraceEvent>
|
||||
</LinkGroup>
|
||||
</FooterLinks>
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -97,7 +97,8 @@ export const ButtonPrimary = styled(BaseButton)`
|
||||
export const SmallButtonPrimary = styled(ButtonPrimary)`
|
||||
width: auto;
|
||||
font-size: 16px;
|
||||
padding: 10px 16px;
|
||||
padding: ${({ padding }) => padding ?? '8px 12px'};
|
||||
|
||||
border-radius: 12px;
|
||||
`
|
||||
|
||||
|
||||
@@ -17,10 +17,10 @@ interface DoubleCurrencyLogoProps {
|
||||
currency1?: Currency
|
||||
}
|
||||
|
||||
const HigherLogo = styled(CurrencyLogo)`
|
||||
const HigherLogoWrapper = styled.div`
|
||||
z-index: 1;
|
||||
`
|
||||
const CoveredLogo = styled(CurrencyLogo)<{ sizeraw: number }>`
|
||||
const CoveredLogoWapper = styled.div<{ sizeraw: number }>`
|
||||
position: absolute;
|
||||
left: ${({ sizeraw }) => '-' + (sizeraw / 2).toString() + 'px'} !important;
|
||||
`
|
||||
@@ -33,8 +33,16 @@ export default function DoubleCurrencyLogo({
|
||||
}: DoubleCurrencyLogoProps) {
|
||||
return (
|
||||
<Wrapper sizeraw={size} margin={margin}>
|
||||
{currency0 && <HigherLogo currency={currency0} size={size.toString() + 'px'} />}
|
||||
{currency1 && <CoveredLogo currency={currency1} size={size.toString() + 'px'} sizeraw={size} />}
|
||||
{currency0 && (
|
||||
<HigherLogoWrapper>
|
||||
<CurrencyLogo hideL2Icon currency={currency0} size={size.toString() + 'px'} />
|
||||
</HigherLogoWrapper>
|
||||
)}
|
||||
{currency1 && (
|
||||
<CoveredLogoWapper sizeraw={size}>
|
||||
<CurrencyLogo hideL2Icon currency={currency1} size={size.toString() + 'px'} />
|
||||
</CoveredLogoWapper>
|
||||
)}
|
||||
</Wrapper>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import { BaseVariant, FeatureFlag, featureFlagSettings, useUpdateFlag } from 'featureFlags'
|
||||
import { useFiatOnrampFlag } from 'featureFlags/flags/fiatOnramp'
|
||||
import { GqlRoutingVariant, useGqlRoutingFlag } from 'featureFlags/flags/gqlRouting'
|
||||
import { NftListV2Variant, useNftListV2Flag } from 'featureFlags/flags/nftListV2'
|
||||
import { PayWithAnyTokenVariant, usePayWithAnyTokenFlag } from 'featureFlags/flags/payWithAnyToken'
|
||||
import { Permit2Variant, usePermit2Flag } from 'featureFlags/flags/permit2'
|
||||
import { SwapWidgetVariant, useSwapWidgetFlag } from 'featureFlags/flags/swapWidget'
|
||||
import { TraceJsonRpcVariant, useTraceJsonRpcFlag } from 'featureFlags/flags/traceJsonRpc'
|
||||
import { useAtomValue, useUpdateAtom } from 'jotai/utils'
|
||||
import { Children, PropsWithChildren, ReactElement, ReactNode, useCallback, useState } from 'react'
|
||||
@@ -215,6 +219,30 @@ export default function FeatureFlagModal() {
|
||||
featureFlag={FeatureFlag.fiatOnramp}
|
||||
label="Fiat on-ramp"
|
||||
/>
|
||||
<FeatureFlagOption
|
||||
variant={NftListV2Variant}
|
||||
value={useNftListV2Flag()}
|
||||
featureFlag={FeatureFlag.nftListV2}
|
||||
label="NFT Listing Page v2"
|
||||
/>
|
||||
<FeatureFlagOption
|
||||
variant={PayWithAnyTokenVariant}
|
||||
value={usePayWithAnyTokenFlag()}
|
||||
featureFlag={FeatureFlag.payWithAnyToken}
|
||||
label="Pay With Any Token"
|
||||
/>
|
||||
<FeatureFlagOption
|
||||
variant={SwapWidgetVariant}
|
||||
value={useSwapWidgetFlag()}
|
||||
featureFlag={FeatureFlag.swapWidget}
|
||||
label="Swap Widget"
|
||||
/>
|
||||
<FeatureFlagOption
|
||||
variant={GqlRoutingVariant}
|
||||
value={useGqlRoutingFlag()}
|
||||
featureFlag={FeatureFlag.gqlRouting}
|
||||
label="GraphQL NFT Routing"
|
||||
/>
|
||||
<FeatureFlagGroup name="Debug">
|
||||
<FeatureFlagOption
|
||||
variant={TraceJsonRpcVariant}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { getChainInfo } from 'constants/chainInfo'
|
||||
import { SupportedChainId } from 'constants/chains'
|
||||
import useTokenLogoSource from 'hooks/useAssetLogoSource'
|
||||
import React from 'react'
|
||||
@@ -29,10 +30,28 @@ export type AssetLogoBaseProps = {
|
||||
backupImg?: string | null
|
||||
size?: string
|
||||
style?: React.CSSProperties
|
||||
hideL2Icon?: boolean
|
||||
}
|
||||
type AssetLogoProps = AssetLogoBaseProps & { isNative?: boolean; address?: string | null; chainId?: number }
|
||||
|
||||
// TODO(cartcrom): add prop to optionally render an L2Icon w/ the logo
|
||||
const LogoContainer = styled.div`
|
||||
position: relative;
|
||||
display: flex;
|
||||
`
|
||||
|
||||
const L2NetworkLogo = styled.div<{ networkUrl?: string; parentSize: string }>`
|
||||
--size: ${({ parentSize }) => `calc(${parentSize} / 2)`};
|
||||
width: var(--size);
|
||||
height: var(--size);
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
bottom: 0;
|
||||
background: url(${({ networkUrl }) => networkUrl});
|
||||
background-repeat: no-repeat;
|
||||
background-size: ${({ parentSize }) => `calc(${parentSize} / 2) calc(${parentSize} / 2)`};
|
||||
display: ${({ networkUrl }) => !networkUrl && 'none'};
|
||||
`
|
||||
|
||||
/**
|
||||
* Renders an image by prioritizing a list of sources, and then eventually a fallback triangle alert
|
||||
*/
|
||||
@@ -44,25 +63,27 @@ export default function AssetLogo({
|
||||
backupImg,
|
||||
size = '24px',
|
||||
style,
|
||||
...rest
|
||||
hideL2Icon = false,
|
||||
}: AssetLogoProps) {
|
||||
const imageProps = {
|
||||
alt: `${symbol ?? 'token'} logo`,
|
||||
size,
|
||||
style,
|
||||
...rest,
|
||||
}
|
||||
|
||||
const [src, nextSrc] = useTokenLogoSource(address, chainId, isNative, backupImg)
|
||||
const L2Icon = getChainInfo(chainId)?.circleLogoUrl
|
||||
|
||||
if (src) {
|
||||
return <LogoImage {...imageProps} src={src} onError={nextSrc} />
|
||||
} else {
|
||||
return (
|
||||
<MissingImageLogo size={size}>
|
||||
{/* use only first 3 characters of Symbol for design reasons */}
|
||||
{symbol?.toUpperCase().replace('$', '').replace(/\s+/g, '').slice(0, 3)}
|
||||
</MissingImageLogo>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<LogoContainer style={style}>
|
||||
{src ? (
|
||||
<LogoImage {...imageProps} src={src} onError={nextSrc} />
|
||||
) : (
|
||||
<MissingImageLogo size={size}>
|
||||
{/* use only first 3 characters of Symbol for design reasons */}
|
||||
{symbol?.toUpperCase().replace('$', '').replace(/\s+/g, '').slice(0, 3)}
|
||||
</MissingImageLogo>
|
||||
)}
|
||||
{!hideL2Icon && <L2NetworkLogo networkUrl={L2Icon} parentSize={size} />}
|
||||
</LogoContainer>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ export default function CurrencyLogo(
|
||||
address={props.currency?.wrapped.address}
|
||||
symbol={props.symbol ?? props.currency?.symbol}
|
||||
backupImg={(props.currency as TokenInfo)?.logoURI}
|
||||
hideL2Icon={props.hideL2Icon ?? true}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -166,6 +166,9 @@ export const MenuDropdown = () => {
|
||||
<SecondaryLinkedText href="https://docs.uniswap.org/">
|
||||
<Trans>Documentation</Trans> ↗
|
||||
</SecondaryLinkedText>
|
||||
<SecondaryLinkedText href="https://uniswap.canny.io/feature-requests">
|
||||
<Trans>Feedback</Trans> ↗
|
||||
</SecondaryLinkedText>
|
||||
<SecondaryLinkedText
|
||||
onClick={() => {
|
||||
toggleOpen()
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -4,7 +4,6 @@ import { formatUSDPrice } from '@uniswap/conedison/format'
|
||||
import { useWeb3React } from '@web3-react/core'
|
||||
import clsx from 'clsx'
|
||||
import AssetLogo from 'components/Logo/AssetLogo'
|
||||
import { L2NetworkLogo, LogoContainer } from 'components/Tokens/TokenTable/TokenRow'
|
||||
import TokenSafetyIcon from 'components/TokenSafety/TokenSafetyIcon'
|
||||
import { getChainInfo } from 'constants/chainInfo'
|
||||
import { NATIVE_CHAIN_ID } from 'constants/tokens'
|
||||
@@ -25,9 +24,6 @@ import styled from 'styled-components/macro'
|
||||
import { getDeltaArrow } from '../Tokens/TokenDetails/PriceChart'
|
||||
import * as styles from './SearchBar.css'
|
||||
|
||||
const StyledLogoContainer = styled(LogoContainer)`
|
||||
margin-right: 8px;
|
||||
`
|
||||
const PriceChangeContainer = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -160,7 +156,7 @@ export const TokenRow = ({ token, isHovered, setHoveredIndex, toggleOpen, index,
|
||||
sendAnalyticsEvent(InterfaceEventName.NAVBAR_RESULT_SELECTED, { ...eventProperties })
|
||||
}, [addToSearchHistory, toggleOpen, token, eventProperties])
|
||||
|
||||
const [bridgedAddress, bridgedChain, L2Icon] = useBridgedAddress(token)
|
||||
const [bridgedAddress, bridgedChain] = useBridgedAddress(token)
|
||||
const tokenDetailsPath = getTokenDetailsURL(bridgedAddress ?? token.address, undefined, bridgedChain ?? token.chainId)
|
||||
// Close the modal on escape
|
||||
useEffect(() => {
|
||||
@@ -181,6 +177,7 @@ export const TokenRow = ({ token, isHovered, setHoveredIndex, toggleOpen, index,
|
||||
|
||||
return (
|
||||
<Link
|
||||
data-cy={`searchbar-token-row-${token.symbol}`}
|
||||
to={tokenDetailsPath}
|
||||
onClick={handleClick}
|
||||
onMouseEnter={() => !isHovered && setHoveredIndex(index)}
|
||||
@@ -189,17 +186,15 @@ export const TokenRow = ({ token, isHovered, setHoveredIndex, toggleOpen, index,
|
||||
style={{ background: isHovered ? vars.color.lightGrayOverlay : 'none' }}
|
||||
>
|
||||
<Row style={{ width: '65%' }}>
|
||||
<StyledLogoContainer>
|
||||
<AssetLogo
|
||||
isNative={token.address === NATIVE_CHAIN_ID}
|
||||
address={token.address}
|
||||
chainId={token.chainId}
|
||||
symbol={token.symbol}
|
||||
size="36px"
|
||||
backupImg={token.logoURI}
|
||||
/>
|
||||
<L2NetworkLogo networkUrl={L2Icon} size="16px" />
|
||||
</StyledLogoContainer>
|
||||
<AssetLogo
|
||||
isNative={token.address === NATIVE_CHAIN_ID}
|
||||
address={token.address}
|
||||
chainId={token.chainId}
|
||||
symbol={token.symbol}
|
||||
size="36px"
|
||||
backupImg={token.logoURI}
|
||||
style={{ margin: '8px 8px 8px 0' }}
|
||||
/>
|
||||
<Column className={styles.suggestionPrimaryContainer}>
|
||||
<Row gap="4" width="full">
|
||||
<Box className={styles.primaryText}>{token.name}</Box>
|
||||
|
||||
@@ -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 />
|
||||
|
||||
@@ -3,11 +3,9 @@ import { useWeb3React } from '@web3-react/core'
|
||||
import { RowFixed } from 'components/Row'
|
||||
import { getChainInfo } from 'constants/chainInfo'
|
||||
import useCurrentBlockTimestamp from 'hooks/useCurrentBlockTimestamp'
|
||||
import useGasPrice from 'hooks/useGasPrice'
|
||||
import { useIsLandingPage } from 'hooks/useIsLandingPage'
|
||||
import { useIsNftPage } from 'hooks/useIsNftPage'
|
||||
import useMachineTimeMs from 'hooks/useMachineTime'
|
||||
import JSBI from 'jsbi'
|
||||
import useBlockNumber from 'lib/hooks/useBlockNumber'
|
||||
import ms from 'ms.macro'
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
@@ -71,17 +69,6 @@ const StyledPollingDot = styled.div<{ warning: boolean }>`
|
||||
transition: 250ms ease background-color;
|
||||
`
|
||||
|
||||
const StyledGasDot = styled.div`
|
||||
background-color: ${({ theme }) => theme.textTertiary};
|
||||
border-radius: 50%;
|
||||
height: 4px;
|
||||
min-height: 4px;
|
||||
min-width: 4px;
|
||||
position: relative;
|
||||
transition: 250ms ease background-color;
|
||||
width: 4px;
|
||||
`
|
||||
|
||||
const rotate360 = keyframes`
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
@@ -123,9 +110,6 @@ export default function Polling() {
|
||||
const isNftPage = useIsNftPage()
|
||||
const isLandingPage = useIsLandingPage()
|
||||
|
||||
const ethGasPrice = useGasPrice()
|
||||
const priceGwei = ethGasPrice ? JSBI.divide(ethGasPrice, JSBI.BigInt(1000000000)) : undefined
|
||||
|
||||
const waitMsBeforeWarning =
|
||||
(chainId ? getChainInfo(chainId)?.blockWaitMsBeforeWarning : DEFAULT_MS_BEFORE_WARNING) ?? DEFAULT_MS_BEFORE_WARNING
|
||||
|
||||
@@ -163,25 +147,6 @@ export default function Polling() {
|
||||
return (
|
||||
<RowFixed>
|
||||
<StyledPolling onMouseEnter={() => setIsHover(true)} onMouseLeave={() => setIsHover(false)}>
|
||||
<ExternalLink href="https://etherscan.io/gastracker">
|
||||
{!!priceGwei && (
|
||||
<RowFixed style={{ marginRight: '8px' }}>
|
||||
<ThemedText.DeprecatedMain fontSize="11px" mr="8px">
|
||||
<MouseoverTooltip
|
||||
text={
|
||||
<Trans>
|
||||
The current fast gas amount for sending a transaction on L1. Gas fees are paid in Ethereum'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
|
||||
|
||||
@@ -232,8 +232,12 @@ export default function PositionListItem({ positionDetails }: PositionListItemPr
|
||||
<Trans>Min: </Trans>
|
||||
</ExtentsText>
|
||||
<Trans>
|
||||
{formatTickPrice(priceLower, tickAtLimit, Bound.LOWER)} <HoverInlineText text={currencyQuote?.symbol} />{' '}
|
||||
per <HoverInlineText text={currencyBase?.symbol ?? ''} />
|
||||
{formatTickPrice({
|
||||
price: priceLower,
|
||||
atLimit: tickAtLimit,
|
||||
direction: Bound.LOWER,
|
||||
})}{' '}
|
||||
<HoverInlineText text={currencyQuote?.symbol} /> per <HoverInlineText text={currencyBase?.symbol ?? ''} />
|
||||
</Trans>
|
||||
</RangeText>{' '}
|
||||
<HideSmall>
|
||||
@@ -247,8 +251,13 @@ export default function PositionListItem({ positionDetails }: PositionListItemPr
|
||||
<Trans>Max:</Trans>
|
||||
</ExtentsText>
|
||||
<Trans>
|
||||
{formatTickPrice(priceUpper, tickAtLimit, Bound.UPPER)} <HoverInlineText text={currencyQuote?.symbol} />{' '}
|
||||
per <HoverInlineText maxCharacters={10} text={currencyBase?.symbol} />
|
||||
{formatTickPrice({
|
||||
price: priceUpper,
|
||||
atLimit: tickAtLimit,
|
||||
direction: Bound.UPPER,
|
||||
})}{' '}
|
||||
<HoverInlineText text={currencyQuote?.symbol} /> per{' '}
|
||||
<HoverInlineText maxCharacters={10} text={currencyBase?.symbol} />
|
||||
</Trans>
|
||||
</RangeText>
|
||||
</RangeLineItem>
|
||||
|
||||
@@ -125,11 +125,13 @@ export const PositionPreview = ({
|
||||
<ThemedText.DeprecatedMain fontSize="12px">
|
||||
<Trans>Min Price</Trans>
|
||||
</ThemedText.DeprecatedMain>
|
||||
<ThemedText.DeprecatedMediumHeader textAlign="center">{`${formatTickPrice(
|
||||
priceLower,
|
||||
ticksAtLimit,
|
||||
Bound.LOWER
|
||||
)}`}</ThemedText.DeprecatedMediumHeader>
|
||||
<ThemedText.DeprecatedMediumHeader textAlign="center">
|
||||
{formatTickPrice({
|
||||
price: priceLower,
|
||||
atLimit: ticksAtLimit,
|
||||
direction: Bound.LOWER,
|
||||
})}
|
||||
</ThemedText.DeprecatedMediumHeader>
|
||||
<ThemedText.DeprecatedMain textAlign="center" fontSize="12px">
|
||||
<Trans>
|
||||
{quoteCurrency.symbol} per {baseCurrency.symbol}
|
||||
@@ -146,11 +148,13 @@ export const PositionPreview = ({
|
||||
<ThemedText.DeprecatedMain fontSize="12px">
|
||||
<Trans>Max Price</Trans>
|
||||
</ThemedText.DeprecatedMain>
|
||||
<ThemedText.DeprecatedMediumHeader textAlign="center">{`${formatTickPrice(
|
||||
priceUpper,
|
||||
ticksAtLimit,
|
||||
Bound.UPPER
|
||||
)}`}</ThemedText.DeprecatedMediumHeader>
|
||||
<ThemedText.DeprecatedMediumHeader textAlign="center">
|
||||
{formatTickPrice({
|
||||
price: priceUpper,
|
||||
atLimit: ticksAtLimit,
|
||||
direction: Bound.UPPER,
|
||||
})}
|
||||
</ThemedText.DeprecatedMediumHeader>
|
||||
<ThemedText.DeprecatedMain textAlign="center" fontSize="12px">
|
||||
<Trans>
|
||||
{quoteCurrency.symbol} per {baseCurrency.symbol}
|
||||
|
||||
@@ -45,6 +45,7 @@ interface CurrencySearchProps {
|
||||
showCommonBases?: boolean
|
||||
showCurrencyAmount?: boolean
|
||||
disableNonToken?: boolean
|
||||
onlyShowCurrenciesWithBalance?: boolean
|
||||
}
|
||||
|
||||
export function CurrencySearch({
|
||||
@@ -56,6 +57,7 @@ export function CurrencySearch({
|
||||
disableNonToken,
|
||||
onDismiss,
|
||||
isOpen,
|
||||
onlyShowCurrenciesWithBalance,
|
||||
}: CurrencySearchProps) {
|
||||
const { chainId } = useWeb3React()
|
||||
const theme = useTheme()
|
||||
@@ -92,6 +94,10 @@ export function CurrencySearch({
|
||||
!balancesAreLoading
|
||||
? filteredTokens
|
||||
.filter((token) => {
|
||||
if (onlyShowCurrenciesWithBalance) {
|
||||
return balances[token.address]?.greaterThan(0)
|
||||
}
|
||||
|
||||
// If there is no query, filter out unselected user-added tokens with no balance.
|
||||
if (!debouncedQuery && token instanceof UserAddedToken) {
|
||||
if (selectedCurrency?.equals(token) || otherSelectedCurrency?.equals(token)) return true
|
||||
@@ -101,7 +107,15 @@ export function CurrencySearch({
|
||||
})
|
||||
.sort(tokenComparator.bind(null, balances))
|
||||
: [],
|
||||
[balances, balancesAreLoading, debouncedQuery, filteredTokens, otherSelectedCurrency, selectedCurrency]
|
||||
[
|
||||
balances,
|
||||
balancesAreLoading,
|
||||
debouncedQuery,
|
||||
filteredTokens,
|
||||
otherSelectedCurrency,
|
||||
selectedCurrency,
|
||||
onlyShowCurrenciesWithBalance,
|
||||
]
|
||||
)
|
||||
const isLoading = Boolean(balancesAreLoading && !tokenLoaderTimerElapsed)
|
||||
|
||||
@@ -114,11 +128,23 @@ export function CurrencySearch({
|
||||
const s = debouncedQuery.toLowerCase().trim()
|
||||
|
||||
const tokens = filteredSortedTokens.filter((t) => !(t.equals(wrapped) || (disableNonToken && t.isNative)))
|
||||
const natives = (disableNonToken || native.equals(wrapped) ? [wrapped] : [native, wrapped]).filter(
|
||||
(n) => n.symbol?.toLowerCase()?.indexOf(s) !== -1 || n.name?.toLowerCase()?.indexOf(s) !== -1
|
||||
)
|
||||
const shouldShowWrapped =
|
||||
!onlyShowCurrenciesWithBalance || (!balancesAreLoading && balances[wrapped.address]?.greaterThan(0))
|
||||
const natives = (
|
||||
disableNonToken || native.equals(wrapped) ? [wrapped] : shouldShowWrapped ? [native, wrapped] : [native]
|
||||
).filter((n) => n.symbol?.toLowerCase()?.indexOf(s) !== -1 || n.name?.toLowerCase()?.indexOf(s) !== -1)
|
||||
|
||||
return [...natives, ...tokens]
|
||||
}, [debouncedQuery, filteredSortedTokens, wrapped, disableNonToken, native])
|
||||
}, [
|
||||
debouncedQuery,
|
||||
filteredSortedTokens,
|
||||
onlyShowCurrenciesWithBalance,
|
||||
balancesAreLoading,
|
||||
balances,
|
||||
wrapped,
|
||||
disableNonToken,
|
||||
native,
|
||||
])
|
||||
|
||||
const handleCurrencySelect = useCallback(
|
||||
(currency: Currency, hasWarning?: boolean) => {
|
||||
@@ -168,7 +194,9 @@ export function CurrencySearch({
|
||||
|
||||
// if no results on main list, show option to expand into inactive
|
||||
const filteredInactiveTokens = useSearchInactiveTokenLists(
|
||||
filteredTokens.length === 0 || (debouncedQuery.length > 2 && !isAddressSearch) ? debouncedQuery : undefined
|
||||
!onlyShowCurrenciesWithBalance && (filteredTokens.length === 0 || (debouncedQuery.length > 2 && !isAddressSearch))
|
||||
? debouncedQuery
|
||||
: undefined
|
||||
)
|
||||
|
||||
// Timeout token loader after 3 seconds to avoid hanging in a loading state.
|
||||
|
||||
@@ -17,6 +17,7 @@ interface CurrencySearchModalProps {
|
||||
showCommonBases?: boolean
|
||||
showCurrencyAmount?: boolean
|
||||
disableNonToken?: boolean
|
||||
onlyShowCurrenciesWithBalance?: boolean
|
||||
}
|
||||
|
||||
enum CurrencyModalView {
|
||||
@@ -34,6 +35,7 @@ export default memo(function CurrencySearchModal({
|
||||
showCommonBases = false,
|
||||
showCurrencyAmount = true,
|
||||
disableNonToken = false,
|
||||
onlyShowCurrenciesWithBalance = false,
|
||||
}: CurrencySearchModalProps) {
|
||||
const [modalView, setModalView] = useState<CurrencyModalView>(CurrencyModalView.search)
|
||||
const lastOpen = useLast(isOpen)
|
||||
@@ -84,6 +86,7 @@ export default memo(function CurrencySearchModal({
|
||||
showCommonBases={showCommonBases}
|
||||
showCurrencyAmount={showCurrencyAmount}
|
||||
disableNonToken={disableNonToken}
|
||||
onlyShowCurrenciesWithBalance={onlyShowCurrenciesWithBalance}
|
||||
/>
|
||||
)
|
||||
break
|
||||
|
||||
@@ -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}`}
|
||||
/>
|
||||
|
||||
@@ -81,7 +81,7 @@ export default function BalanceSummary({ token }: { token: Currency }) {
|
||||
<Trans>Your balance on {label}</Trans>
|
||||
</ThemedText.SubHeaderSmall>
|
||||
<BalanceRow>
|
||||
<CurrencyLogo currency={token} size="2rem" />
|
||||
<CurrencyLogo currency={token} size="2rem" hideL2Icon={false} />
|
||||
<BalanceContainer>
|
||||
<BalanceAmountsContainer>
|
||||
<BalanceItem>
|
||||
|
||||
@@ -13,7 +13,7 @@ import TimePeriodSelector from './TimeSelector'
|
||||
function usePriceHistory(tokenPriceData: TokenPriceQuery): PricePoint[] | undefined {
|
||||
// Appends the current price to the end of the priceHistory array
|
||||
const priceHistory = useMemo(() => {
|
||||
const market = tokenPriceData.tokens?.[0]?.market
|
||||
const market = tokenPriceData.token?.market
|
||||
const priceHistory = market?.priceHistory?.filter(isPricePoint)
|
||||
const currentPrice = market?.price?.value
|
||||
if (Array.isArray(priceHistory) && currentPrice !== undefined) {
|
||||
@@ -58,7 +58,7 @@ function Chart({
|
||||
const timePeriod = useAtomValue(pageTimePeriodAtom)
|
||||
|
||||
return (
|
||||
<ChartContainer>
|
||||
<ChartContainer data-testid="chart-container">
|
||||
<ParentSize>
|
||||
{({ width }) => <PriceChart prices={prices ?? null} width={width} height={436} timePeriod={timePeriod} />}
|
||||
</ParentSize>
|
||||
|
||||
@@ -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}`}
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import { WidgetSkeleton } from 'components/Widget'
|
||||
import { WIDGET_WIDTH } from 'components/Widget'
|
||||
import { DEFAULT_WIDGET_WIDTH } from 'components/Widget'
|
||||
import { ArrowLeft } from 'react-feather'
|
||||
import { useParams } from 'react-router-dom'
|
||||
import styled, { useTheme } from 'styled-components/macro'
|
||||
import { textFadeIn } from 'theme/styles'
|
||||
|
||||
import { LoadingBubble } from '../loading'
|
||||
import { LogoContainer } from '../TokenTable/TokenRow'
|
||||
import { AboutContainer, AboutHeader } from './About'
|
||||
import { BreadcrumbNavLink } from './BreadcrumbNavLink'
|
||||
import { TokenPrice } from './PriceChart'
|
||||
@@ -44,7 +43,7 @@ export const RightPanel = styled.div`
|
||||
display: none;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
width: ${WIDGET_WIDTH}px;
|
||||
width: ${DEFAULT_WIDGET_WIDTH}px;
|
||||
|
||||
@media screen and (min-width: ${({ theme }) => theme.breakpoint.lg}px) {
|
||||
display: flex;
|
||||
@@ -227,9 +226,7 @@ export default function TokenDetailsSkeleton() {
|
||||
</BreadcrumbNavLink>
|
||||
<TokenInfoContainer>
|
||||
<TokenNameCell>
|
||||
<LogoContainer>
|
||||
<TokenLogoBubble />
|
||||
</LogoContainer>
|
||||
<TokenLogoBubble />
|
||||
<TitleBubble />
|
||||
</TokenNameCell>
|
||||
</TokenInfoContainer>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Trans } from '@lingui/macro'
|
||||
import { Trace } from '@uniswap/analytics'
|
||||
import { InterfacePageName } from '@uniswap/analytics-events'
|
||||
import { Currency } from '@uniswap/sdk-core'
|
||||
import { Field } from '@uniswap/widgets'
|
||||
import { useWeb3React } from '@web3-react/core'
|
||||
import CurrencyLogo from 'components/Logo/CurrencyLogo'
|
||||
import { AboutSection } from 'components/Tokens/TokenDetails/About'
|
||||
@@ -20,7 +21,6 @@ import TokenDetailsSkeleton, {
|
||||
TokenNameCell,
|
||||
} from 'components/Tokens/TokenDetails/Skeleton'
|
||||
import StatsSection from 'components/Tokens/TokenDetails/StatsSection'
|
||||
import { L2NetworkLogo, LogoContainer } from 'components/Tokens/TokenTable/TokenRow'
|
||||
import TokenSafetyMessage from 'components/TokenSafety/TokenSafetyMessage'
|
||||
import TokenSafetyModal from 'components/TokenSafety/TokenSafetyModal'
|
||||
import Widget from 'components/Widget'
|
||||
@@ -111,7 +111,7 @@ export default function TokenDetails({
|
||||
|
||||
const pageChainId = CHAIN_NAME_TO_CHAIN_ID[chain]
|
||||
|
||||
const tokenQueryData = tokenQuery.tokens?.[0]
|
||||
const tokenQueryData = tokenQuery.token
|
||||
const crossChainMap = useMemo(
|
||||
() =>
|
||||
tokenQueryData?.project?.tokens.reduce((map, current) => {
|
||||
@@ -168,8 +168,6 @@ export default function TokenDetails({
|
||||
[continueSwap, setContinueSwap]
|
||||
)
|
||||
|
||||
const L2Icon = getChainInfo(pageChainId)?.circleLogoUrl
|
||||
|
||||
// address will never be undefined if token is defined; address is checked here to appease typechecker
|
||||
if (token === undefined || !address) {
|
||||
return <InvalidTokenDetails chainName={address && getChainInfo(pageChainId)?.label} />
|
||||
@@ -186,12 +184,10 @@ export default function TokenDetails({
|
||||
<BreadcrumbNavLink to={`/tokens/${chain.toLowerCase()}`}>
|
||||
<ArrowLeft data-testid="token-details-return-button" size={14} /> Tokens
|
||||
</BreadcrumbNavLink>
|
||||
<TokenInfoContainer>
|
||||
<TokenInfoContainer data-testid="token-info-container">
|
||||
<TokenNameCell>
|
||||
<LogoContainer>
|
||||
<CurrencyLogo currency={token} size="32px" />
|
||||
<L2NetworkLogo networkUrl={L2Icon} size="16px" />
|
||||
</LogoContainer>
|
||||
<CurrencyLogo currency={token} size="32px" hideL2Icon={false} />
|
||||
|
||||
{token.name ?? <Trans>Name not found</Trans>}
|
||||
<TokenSymbol>{token.symbol ?? <Trans>Symbol not found</Trans>}</TokenSymbol>
|
||||
</TokenNameCell>
|
||||
@@ -223,6 +219,7 @@ export default function TokenDetails({
|
||||
<RightPanel>
|
||||
<Widget
|
||||
token={token ?? undefined}
|
||||
defaultField={Field.OUTPUT}
|
||||
onTokenChange={navigateToWidgetSelectedToken}
|
||||
onReviewSwapClick={onReviewSwapClick}
|
||||
/>
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -6,7 +6,6 @@ import { ParentSize } from '@visx/responsive'
|
||||
import SparklineChart from 'components/Charts/SparklineChart'
|
||||
import QueryTokenLogo from 'components/Logo/QueryTokenLogo'
|
||||
import { MouseoverTooltip } from 'components/Tooltip'
|
||||
import { getChainInfo } from 'constants/chainInfo'
|
||||
import { SparklineMap, TopToken } from 'graphql/data/TopTokens'
|
||||
import { CHAIN_NAME_TO_CHAIN_ID, getTokenDetailsURL } from 'graphql/data/util'
|
||||
import { useAtomValue } from 'jotai/utils'
|
||||
@@ -279,23 +278,6 @@ export const SparkLineLoadingBubble = styled(LongLoadingBubble)`
|
||||
height: 4px;
|
||||
`
|
||||
|
||||
export const L2NetworkLogo = styled.div<{ networkUrl?: string; size?: string }>`
|
||||
height: ${({ size }) => size ?? '12px'};
|
||||
width: ${({ size }) => size ?? '12px'};
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
bottom: 0;
|
||||
background: url(${({ networkUrl }) => networkUrl});
|
||||
background-repeat: no-repeat;
|
||||
background-size: ${({ size }) => (size ? `${size} ${size}` : '12px 12px')};
|
||||
display: ${({ networkUrl }) => !networkUrl && 'none'};
|
||||
`
|
||||
export const LogoContainer = styled.div`
|
||||
position: relative;
|
||||
align-items: center;
|
||||
display: flex;
|
||||
`
|
||||
|
||||
const InfoIconContainer = styled.div`
|
||||
margin-left: 2px;
|
||||
display: flex;
|
||||
@@ -378,15 +360,23 @@ function TokenRow({
|
||||
const rowCells = (
|
||||
<>
|
||||
<ListNumberCell header={header}>{listNumber}</ListNumberCell>
|
||||
<NameCell>{tokenInfo}</NameCell>
|
||||
<PriceCell sortable={header}>{price}</PriceCell>
|
||||
<PercentChangeCell sortable={header}>{percentChange}</PercentChangeCell>
|
||||
<TvlCell sortable={header}>{tvl}</TvlCell>
|
||||
<VolumeCell sortable={header}>{volume}</VolumeCell>
|
||||
<NameCell data-testid="name-cell">{tokenInfo}</NameCell>
|
||||
<PriceCell data-testid="price-cell" sortable={header}>
|
||||
{price}
|
||||
</PriceCell>
|
||||
<PercentChangeCell data-testid="percent-change-cell" sortable={header}>
|
||||
{percentChange}
|
||||
</PercentChangeCell>
|
||||
<TvlCell data-testid="tvl-cell" sortable={header}>
|
||||
{tvl}
|
||||
</TvlCell>
|
||||
<VolumeCell data-testid="volume-cell" sortable={header}>
|
||||
{volume}
|
||||
</VolumeCell>
|
||||
<SparkLineCell>{sparkLine}</SparkLineCell>
|
||||
</>
|
||||
)
|
||||
if (header) return <StyledHeaderRow>{rowCells}</StyledHeaderRow>
|
||||
if (header) return <StyledHeaderRow data-testid="header-row">{rowCells}</StyledHeaderRow>
|
||||
return <StyledTokenRow {...rest}>{rowCells}</StyledTokenRow>
|
||||
}
|
||||
|
||||
@@ -434,34 +424,29 @@ interface LoadedRowProps {
|
||||
tokenListLength: number
|
||||
token: NonNullable<TopToken>
|
||||
sparklineMap: SparklineMap
|
||||
volumeRank: number
|
||||
}
|
||||
|
||||
/* Loaded State: row component with token information */
|
||||
export const LoadedRow = forwardRef((props: LoadedRowProps, ref: ForwardedRef<HTMLDivElement>) => {
|
||||
const { tokenListIndex, tokenListLength, token } = props
|
||||
const tokenAddress = token.address
|
||||
const tokenName = token.name
|
||||
const tokenSymbol = token.symbol
|
||||
const { tokenListIndex, tokenListLength, token, volumeRank } = props
|
||||
const filterString = useAtomValue(filterStringAtom)
|
||||
const sortAscending = useAtomValue(sortAscendingAtom)
|
||||
|
||||
const lowercaseChainName = useParams<{ chainName?: string }>().chainName?.toUpperCase() ?? 'ethereum'
|
||||
const filterNetwork = lowercaseChainName.toUpperCase()
|
||||
const chainId = CHAIN_NAME_TO_CHAIN_ID[filterNetwork]
|
||||
const L2Icon = getChainInfo(chainId)?.circleLogoUrl
|
||||
const timePeriod = useAtomValue(filterTimeAtom)
|
||||
const delta = token.market?.pricePercentChange?.value
|
||||
const arrow = getDeltaArrow(delta)
|
||||
const smallArrow = getDeltaArrow(delta, 14)
|
||||
const formattedDelta = formatDelta(delta)
|
||||
const rank = sortAscending ? tokenListLength - tokenListIndex : tokenListIndex + 1
|
||||
|
||||
const exploreTokenSelectedEventProperties = {
|
||||
chain_id: chainId,
|
||||
token_address: tokenAddress,
|
||||
token_symbol: tokenSymbol,
|
||||
token_address: token.address,
|
||||
token_symbol: token.symbol,
|
||||
token_list_index: tokenListIndex,
|
||||
token_list_rank: rank,
|
||||
token_list_rank: volumeRank,
|
||||
token_list_length: tokenListLength,
|
||||
time_frame: timePeriod,
|
||||
search_token_address_input: filterString,
|
||||
@@ -469,7 +454,7 @@ export const LoadedRow = forwardRef((props: LoadedRowProps, ref: ForwardedRef<HT
|
||||
|
||||
// TODO: currency logo sizing mobile (32px) vs. desktop (24px)
|
||||
return (
|
||||
<div ref={ref} data-testid={`token-table-row-${tokenName}`}>
|
||||
<div ref={ref} data-testid={`token-table-row-${token.symbol}`}>
|
||||
<StyledLink
|
||||
to={getTokenDetailsURL(token.address ?? '', token.chain)}
|
||||
onClick={() =>
|
||||
@@ -478,16 +463,13 @@ export const LoadedRow = forwardRef((props: LoadedRowProps, ref: ForwardedRef<HT
|
||||
>
|
||||
<TokenRow
|
||||
header={false}
|
||||
listNumber={rank}
|
||||
listNumber={volumeRank}
|
||||
tokenInfo={
|
||||
<ClickableName>
|
||||
<LogoContainer>
|
||||
<QueryTokenLogo token={token} />
|
||||
<L2NetworkLogo networkUrl={L2Icon} />
|
||||
</LogoContainer>
|
||||
<QueryTokenLogo token={token} />
|
||||
<TokenInfoCell>
|
||||
<TokenName>{tokenName}</TokenName>
|
||||
<TokenSymbol>{tokenSymbol}</TokenSymbol>
|
||||
<TokenName data-cy="token-name">{token.name}</TokenName>
|
||||
<TokenSymbol>{token.symbol}</TokenSymbol>
|
||||
</TokenInfoCell>
|
||||
</ClickableName>
|
||||
}
|
||||
|
||||
@@ -76,12 +76,11 @@ function LoadingTokenTable({ rowCount = PAGE_SIZE }: { rowCount?: number }) {
|
||||
}
|
||||
|
||||
export default function TokenTable() {
|
||||
// TODO: consider moving prefetched call into app.tsx and passing it here, use a preloaded call & updated on interval every 60s
|
||||
const chainName = validateUrlChainParam(useParams<{ chainName?: string }>().chainName)
|
||||
const { tokens, loadingTokens, sparklines } = useTopTokens(chainName)
|
||||
const { tokens, tokenVolumeRank, loadingTokens, sparklines } = useTopTokens(chainName)
|
||||
|
||||
/* loading and error state */
|
||||
if (loadingTokens) {
|
||||
if (loadingTokens && !tokens) {
|
||||
return <LoadingTokenTable rowCount={PAGE_SIZE} />
|
||||
} else if (!tokens) {
|
||||
return (
|
||||
@@ -103,13 +102,14 @@ export default function TokenTable() {
|
||||
<TokenDataContainer>
|
||||
{tokens.map(
|
||||
(token, index) =>
|
||||
token && (
|
||||
token?.address && (
|
||||
<LoadedRow
|
||||
key={token?.address}
|
||||
key={token.address}
|
||||
tokenListIndex={index}
|
||||
tokenListLength={tokens.length}
|
||||
token={token}
|
||||
sparklineMap={sparklines}
|
||||
volumeRank={tokenVolumeRank[token.address]}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
|
||||
@@ -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,
|
||||
useToggleMetamaskConnectionErrorModal,
|
||||
useToggleWalletDropdown,
|
||||
useToggleWalletModal,
|
||||
} from '../../state/application/hooks'
|
||||
@@ -35,7 +34,6 @@ import StatusIcon from '../Identicon/StatusIcon'
|
||||
import Loader from '../Loader'
|
||||
import { RowBetween } from '../Row'
|
||||
import WalletModal from '../WalletModal'
|
||||
import MetamaskConnectionError from './MetamaskConnectionError'
|
||||
|
||||
// https://stackoverflow.com/a/31617326
|
||||
const FULL_BORDER_RADIUS = 9999
|
||||
@@ -213,16 +211,10 @@ function Web3StatusInner() {
|
||||
toggleWalletDropdown()
|
||||
}, [toggleWalletDropdown])
|
||||
const toggleWalletModal = useToggleWalletModal()
|
||||
const toggleMetamaskConnectionErrorModal = useToggleMetamaskConnectionErrorModal()
|
||||
const walletIsOpen = useModalIsOpen(ApplicationModal.WALLET_DROPDOWN)
|
||||
const isClaimAvailable = useIsNftClaimAvailable((state) => state.isClaimAvailable)
|
||||
|
||||
const error = useAppSelector((state) => state.connection.errorByConnectionType[getConnection(connector).type])
|
||||
useEffect(() => {
|
||||
if (getIsMetaMask() && error) {
|
||||
toggleMetamaskConnectionErrorModal()
|
||||
}
|
||||
}, [error, toggleMetamaskConnectionErrorModal])
|
||||
const error = useAppSelector((state) => state.connection.errorByConnectionType[connectionType])
|
||||
|
||||
const allTransactions = useAllTransactions()
|
||||
|
||||
@@ -326,7 +318,6 @@ export default function Web3Status() {
|
||||
<Web3StatusInner />
|
||||
<FiatOnrampAnnouncement />
|
||||
<WalletModal ENSName={ENSName ?? undefined} pendingTransactions={pending} confirmedTransactions={confirmed} />
|
||||
<MetamaskConnectionError />
|
||||
<Portal>
|
||||
<span ref={walletRef}>
|
||||
<WalletDropdown />
|
||||
|
||||
@@ -10,6 +10,7 @@ import { Currency, TradeType } from '@uniswap/sdk-core'
|
||||
import {
|
||||
AddEthereumChainParameter,
|
||||
EMPTY_TOKEN_LIST,
|
||||
Field,
|
||||
OnReviewSwapClick,
|
||||
SwapWidget,
|
||||
SwapWidgetSkeleton,
|
||||
@@ -26,6 +27,7 @@ import {
|
||||
getTokenAddress,
|
||||
} from 'lib/utils/analytics'
|
||||
import { useCallback, useState } from 'react'
|
||||
import { useToggleWalletModal } from 'state/application/hooks'
|
||||
import { useIsDarkMode } from 'state/user/hooks'
|
||||
import { computeRealizedPriceImpact } from 'utils/prices'
|
||||
import { switchChain } from 'utils/switchChain'
|
||||
@@ -35,7 +37,7 @@ import { useSyncWidgetSettings } from './settings'
|
||||
import { DARK_THEME, LIGHT_THEME } from './theme'
|
||||
import { useSyncWidgetTransactions } from './transactions'
|
||||
|
||||
export const WIDGET_WIDTH = 360
|
||||
export const DEFAULT_WIDGET_WIDTH = 360
|
||||
|
||||
const WIDGET_ROUTER_URL = 'https://api.uniswap.org/v1/'
|
||||
|
||||
@@ -45,18 +47,32 @@ function useWidgetTheme() {
|
||||
|
||||
interface WidgetProps {
|
||||
token?: Currency
|
||||
width?: number | string
|
||||
defaultField: Field
|
||||
onTokenChange?: (token: Currency) => void
|
||||
onReviewSwapClick?: OnReviewSwapClick
|
||||
}
|
||||
|
||||
export default function Widget({ token, onTokenChange, onReviewSwapClick }: WidgetProps) {
|
||||
export default function Widget({
|
||||
token,
|
||||
width = DEFAULT_WIDGET_WIDTH,
|
||||
defaultField,
|
||||
onTokenChange,
|
||||
onReviewSwapClick,
|
||||
}: WidgetProps) {
|
||||
const { connector, provider } = useWeb3React()
|
||||
const locale = useActiveLocale()
|
||||
const theme = useWidgetTheme()
|
||||
const { inputs, tokenSelector } = useSyncWidgetInputs({ token, onTokenChange })
|
||||
const { inputs, tokenSelector } = useSyncWidgetInputs({ token, onTokenChange, defaultField })
|
||||
const { settings } = useSyncWidgetSettings()
|
||||
const { transactions } = useSyncWidgetTransactions()
|
||||
|
||||
const toggleWalletModal = useToggleWalletModal()
|
||||
const onConnectWalletClick = useCallback(() => {
|
||||
toggleWalletModal()
|
||||
return false // prevents the in-widget wallet modal from opening
|
||||
}, [toggleWalletModal])
|
||||
|
||||
const onSwitchChain = useCallback(
|
||||
// TODO(WEB-1757): Widget should not break if this rejects - upstream the catch to ignore it.
|
||||
({ chainId }: AddEthereumChainParameter) => switchChain(connector, Number(chainId)).catch(() => undefined),
|
||||
@@ -152,8 +168,9 @@ export default function Widget({ token, onTokenChange, onReviewSwapClick }: Widg
|
||||
routerUrl={WIDGET_ROUTER_URL}
|
||||
locale={locale}
|
||||
theme={theme}
|
||||
width={WIDGET_WIDTH}
|
||||
width={width}
|
||||
// defaultChainId is excluded - it is always inferred from the passed provider
|
||||
onConnectWalletClick={onConnectWalletClick}
|
||||
provider={provider}
|
||||
onSwitchChain={onSwitchChain}
|
||||
tokenList={EMPTY_TOKEN_LIST} // prevents loading the default token list, as we use our own token selector UI
|
||||
@@ -174,5 +191,5 @@ export default function Widget({ token, onTokenChange, onReviewSwapClick }: Widg
|
||||
|
||||
export function WidgetSkeleton() {
|
||||
const theme = useWidgetTheme()
|
||||
return <SwapWidgetSkeleton theme={theme} width={WIDGET_WIDTH} />
|
||||
return <SwapWidgetSkeleton theme={theme} width={DEFAULT_WIDGET_WIDTH} />
|
||||
}
|
||||
|
||||
@@ -21,26 +21,29 @@ function includesDefaultToken(tokens: SwapTokens) {
|
||||
*/
|
||||
export function useSyncWidgetInputs({
|
||||
token,
|
||||
defaultField,
|
||||
onTokenChange,
|
||||
}: {
|
||||
token?: Currency
|
||||
defaultField: Field
|
||||
|
||||
onTokenChange?: (token: Currency) => void
|
||||
}) {
|
||||
const trace = useTrace({ section: InterfaceSectionName.WIDGET })
|
||||
|
||||
const [type, setType] = useState<SwapValue['type']>(TradeType.EXACT_INPUT)
|
||||
const [amount, setAmount] = useState<SwapValue['amount']>(EMPTY_AMOUNT)
|
||||
const [tokens, setTokens] = useState<SwapTokens>({ [Field.OUTPUT]: token, default: token })
|
||||
const [tokens, setTokens] = useState<SwapTokens>({ [defaultField]: token, default: token })
|
||||
|
||||
useEffect(() => {
|
||||
setTokens((tokens) => {
|
||||
const update = { ...tokens, default: token }
|
||||
if (!includesDefaultToken(update)) {
|
||||
return { [Field.OUTPUT]: update.default, default: update.default }
|
||||
return { [defaultField]: update.default, default: update.default }
|
||||
}
|
||||
return update
|
||||
})
|
||||
}, [token])
|
||||
}, [defaultField, token])
|
||||
|
||||
const onAmountChange = useCallback(
|
||||
(field: Field, amount: string, origin?: 'max') => {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Percent } from '@uniswap/sdk-core'
|
||||
import { Slippage, SwapController, SwapEventHandlers } from '@uniswap/widgets'
|
||||
import { RouterPreference, Slippage, SwapController, SwapEventHandlers } from '@uniswap/widgets'
|
||||
import { DEFAULT_DEADLINE_FROM_NOW } from 'constants/misc'
|
||||
import { useCallback, useMemo, useState } from 'react'
|
||||
import { useUserSlippageTolerance, useUserTransactionTTL } from 'state/user/hooks'
|
||||
@@ -37,6 +37,8 @@ export function useSyncWidgetSettings() {
|
||||
[setAppSlippage]
|
||||
)
|
||||
|
||||
const [routerPreference, onRouterPreferenceChange] = useState(RouterPreference.API)
|
||||
|
||||
const onSettingsReset = useCallback(() => {
|
||||
setWidgetTtl(undefined)
|
||||
setAppTtl(DEFAULT_DEADLINE_FROM_NOW)
|
||||
@@ -46,11 +48,15 @@ export function useSyncWidgetSettings() {
|
||||
|
||||
const settings: SwapController['settings'] = useMemo(() => {
|
||||
const auto = appSlippage === 'auto'
|
||||
return { slippage: { auto, max: widgetSlippage }, transactionTtl: widgetTtl }
|
||||
}, [widgetSlippage, widgetTtl, appSlippage])
|
||||
return {
|
||||
slippage: { auto, max: widgetSlippage },
|
||||
transactionTtl: widgetTtl,
|
||||
routerPreference,
|
||||
}
|
||||
}, [appSlippage, widgetSlippage, widgetTtl, routerPreference])
|
||||
const settingsHandlers: SwapEventHandlers = useMemo(
|
||||
() => ({ onSettingsReset, onSlippageChange, onTransactionDeadlineChange }),
|
||||
[onSettingsReset, onSlippageChange, onTransactionDeadlineChange]
|
||||
() => ({ onSettingsReset, onSlippageChange, onTransactionDeadlineChange, onRouterPreferenceChange }),
|
||||
[onSettingsReset, onSlippageChange, onTransactionDeadlineChange, onRouterPreferenceChange]
|
||||
)
|
||||
|
||||
return { settings: { settings, ...settingsHandlers } }
|
||||
|
||||
@@ -1,45 +1,68 @@
|
||||
import { Theme } from '@uniswap/widgets'
|
||||
import { darkTheme, lightTheme } from 'theme/colors'
|
||||
import { Z_INDEX } from 'theme/zIndex'
|
||||
|
||||
const zIndex = {
|
||||
modal: Z_INDEX.modal,
|
||||
}
|
||||
|
||||
const fonts = {
|
||||
fontFamily: 'Inter custom',
|
||||
}
|
||||
|
||||
export const LIGHT_THEME = {
|
||||
export const LIGHT_THEME: Theme = {
|
||||
// surface
|
||||
container: lightTheme.backgroundSurface,
|
||||
interactive: lightTheme.backgroundInteractive,
|
||||
module: lightTheme.backgroundModule,
|
||||
accent: lightTheme.accentAction,
|
||||
dialog: lightTheme.backgroundBackdrop,
|
||||
accentSoft: lightTheme.accentActionSoft,
|
||||
container: lightTheme.backgroundSurface,
|
||||
module: lightTheme.backgroundModule,
|
||||
interactive: lightTheme.backgroundInteractive,
|
||||
outline: lightTheme.backgroundOutline,
|
||||
dialog: lightTheme.backgroundBackdrop,
|
||||
scrim: lightTheme.backgroundScrim,
|
||||
// text
|
||||
onAccent: lightTheme.white,
|
||||
primary: lightTheme.textPrimary,
|
||||
secondary: lightTheme.textSecondary,
|
||||
hint: lightTheme.textTertiary,
|
||||
onInteractive: lightTheme.accentTextDarkPrimary,
|
||||
// shadow
|
||||
deepShadow: lightTheme.deepShadow,
|
||||
networkDefaultShadow: lightTheme.networkDefaultShadow,
|
||||
|
||||
// state
|
||||
success: lightTheme.accentSuccess,
|
||||
warning: lightTheme.accentWarning,
|
||||
error: lightTheme.accentCritical,
|
||||
|
||||
...fonts,
|
||||
zIndex,
|
||||
}
|
||||
|
||||
export const DARK_THEME = {
|
||||
export const DARK_THEME: Theme = {
|
||||
// surface
|
||||
container: darkTheme.backgroundSurface,
|
||||
interactive: darkTheme.backgroundInteractive,
|
||||
module: darkTheme.backgroundModule,
|
||||
accent: darkTheme.accentAction,
|
||||
dialog: darkTheme.backgroundBackdrop,
|
||||
accentSoft: darkTheme.accentActionSoft,
|
||||
container: darkTheme.backgroundSurface,
|
||||
module: darkTheme.backgroundModule,
|
||||
interactive: darkTheme.backgroundInteractive,
|
||||
outline: darkTheme.backgroundOutline,
|
||||
dialog: darkTheme.backgroundBackdrop,
|
||||
scrim: darkTheme.backgroundScrim,
|
||||
// text
|
||||
onAccent: darkTheme.white,
|
||||
primary: darkTheme.textPrimary,
|
||||
secondary: darkTheme.textSecondary,
|
||||
hint: darkTheme.textTertiary,
|
||||
onInteractive: darkTheme.accentTextLightPrimary,
|
||||
// shadow
|
||||
deepShadow: darkTheme.deepShadow,
|
||||
networkDefaultShadow: darkTheme.networkDefaultShadow,
|
||||
// state
|
||||
success: darkTheme.accentSuccess,
|
||||
warning: darkTheme.accentWarning,
|
||||
error: darkTheme.accentCritical,
|
||||
|
||||
...fonts,
|
||||
zIndex,
|
||||
}
|
||||
|
||||
@@ -1,15 +1,23 @@
|
||||
import { sendAnalyticsEvent, useTrace } from '@uniswap/analytics'
|
||||
import { InterfaceEventName, InterfaceSectionName, SwapEventName } from '@uniswap/analytics-events'
|
||||
import { Trade } from '@uniswap/router-sdk'
|
||||
import { Currency, Percent } from '@uniswap/sdk-core'
|
||||
import {
|
||||
OnTxSuccess,
|
||||
TradeType,
|
||||
Transaction,
|
||||
TransactionEventHandlers,
|
||||
TransactionInfo,
|
||||
TransactionType,
|
||||
TransactionType as WidgetTransactionType,
|
||||
} from '@uniswap/widgets'
|
||||
import { useWeb3React } from '@web3-react/core'
|
||||
import { WrapType } from 'hooks/useWrapCallback'
|
||||
import { formatSwapSignedAnalyticsEventProperties, formatToDecimal, getTokenAddress } from 'lib/utils/analytics'
|
||||
import {
|
||||
formatPercentInBasisPointsNumber,
|
||||
formatSwapSignedAnalyticsEventProperties,
|
||||
formatToDecimal,
|
||||
getTokenAddress,
|
||||
} from 'lib/utils/analytics'
|
||||
import { useCallback, useMemo } from 'react'
|
||||
import { useTransactionAdder } from 'state/transactions/hooks'
|
||||
import {
|
||||
@@ -19,6 +27,42 @@ import {
|
||||
WrapTransactionInfo,
|
||||
} from 'state/transactions/types'
|
||||
import { currencyId } from 'utils/currencyId'
|
||||
import { computeRealizedPriceImpact } from 'utils/prices'
|
||||
|
||||
interface AnalyticsEventProps {
|
||||
trade: Trade<Currency, Currency, TradeType>
|
||||
gasUsed: string | undefined
|
||||
blockNumber: number | undefined
|
||||
hash: string | undefined
|
||||
allowedSlippage: Percent
|
||||
succeeded: boolean
|
||||
}
|
||||
|
||||
const formatAnalyticsEventProperties = ({
|
||||
trade,
|
||||
hash,
|
||||
allowedSlippage,
|
||||
succeeded,
|
||||
gasUsed,
|
||||
blockNumber,
|
||||
}: AnalyticsEventProps) => ({
|
||||
estimated_network_fee_usd: gasUsed,
|
||||
transaction_hash: hash,
|
||||
token_in_address: getTokenAddress(trade.inputAmount.currency),
|
||||
token_out_address: getTokenAddress(trade.outputAmount.currency),
|
||||
token_in_symbol: trade.inputAmount.currency.symbol,
|
||||
token_out_symbol: trade.outputAmount.currency.symbol,
|
||||
token_in_amount: formatToDecimal(trade.inputAmount, trade.inputAmount.currency.decimals),
|
||||
token_out_amount: formatToDecimal(trade.outputAmount, trade.outputAmount.currency.decimals),
|
||||
price_impact_basis_points: formatPercentInBasisPointsNumber(computeRealizedPriceImpact(trade)),
|
||||
allowed_slippage_basis_points: formatPercentInBasisPointsNumber(allowedSlippage),
|
||||
chain_id:
|
||||
trade.inputAmount.currency.chainId === trade.outputAmount.currency.chainId
|
||||
? trade.inputAmount.currency.chainId
|
||||
: undefined,
|
||||
swap_quote_block_number: blockNumber,
|
||||
succeeded,
|
||||
})
|
||||
|
||||
/** Integrates the Widget's transactions, showing the widget's transactions in the app. */
|
||||
export function useSyncWidgetTransactions() {
|
||||
@@ -46,7 +90,7 @@ export function useSyncWidgetTransactions() {
|
||||
amount: transactionAmount
|
||||
? formatToDecimal(transactionAmount, transactionAmount?.currency.decimals)
|
||||
: undefined,
|
||||
type: type === WidgetTransactionType.WRAP ? WrapType.WRAP : WrapType.UNWRAP,
|
||||
type: type === WidgetTransactionType.WRAP ? TransactionType.WRAP : TransactionType.UNWRAP,
|
||||
...trace,
|
||||
}
|
||||
sendAnalyticsEvent(InterfaceEventName.WRAP_TOKEN_TXN_SUBMITTED, eventProperties)
|
||||
@@ -94,7 +138,24 @@ export function useSyncWidgetTransactions() {
|
||||
[addTransaction, chainId, trace]
|
||||
)
|
||||
|
||||
const txHandlers: TransactionEventHandlers = useMemo(() => ({ onTxSubmit }), [onTxSubmit])
|
||||
const onTxSuccess: OnTxSuccess = useCallback((hash: string, tx) => {
|
||||
if (tx.info.type === TransactionType.SWAP) {
|
||||
const { trade, slippageTolerance } = tx.info
|
||||
sendAnalyticsEvent(
|
||||
SwapEventName.SWAP_TRANSACTION_COMPLETED,
|
||||
formatAnalyticsEventProperties({
|
||||
trade,
|
||||
hash,
|
||||
gasUsed: tx.receipt?.gasUsed?.toString(),
|
||||
blockNumber: tx.receipt?.blockNumber,
|
||||
allowedSlippage: slippageTolerance,
|
||||
succeeded: tx.receipt?.status === 1,
|
||||
})
|
||||
)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const txHandlers: TransactionEventHandlers = useMemo(() => ({ onTxSubmit, onTxSuccess }), [onTxSubmit, onTxSuccess])
|
||||
|
||||
return { transactions: { ...txHandlers } }
|
||||
}
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -20,7 +20,8 @@ export const COMMON_CONTRACT_NAMES: Record<number, { [address: string]: string }
|
||||
},
|
||||
}
|
||||
|
||||
export const DEFAULT_AVERAGE_BLOCK_TIME_IN_SECS = 13
|
||||
// in PoS, ethereum block time is 12s, see https://ethereum.org/en/developers/docs/blocks/#block-time
|
||||
export const DEFAULT_AVERAGE_BLOCK_TIME_IN_SECS = 12
|
||||
|
||||
// Block time here is slightly higher (~1s) than average in order to avoid ongoing proposals past the displayed time
|
||||
export const AVERAGE_BLOCK_TIME_IN_SECS: { [chainId: number]: number } = {
|
||||
|
||||
@@ -8,7 +8,6 @@ const COINGECKO_LIST = 'https://tokens.coingecko.com/uniswap/all.json'
|
||||
const COMPOUND_LIST = 'https://raw.githubusercontent.com/compound-finance/token-list/master/compound.tokenlist.json'
|
||||
const GEMINI_LIST = 'https://www.gemini.com/uniswap/manifest.json'
|
||||
const KLEROS_LIST = 't2crtokens.eth'
|
||||
const ROLL_LIST = 'https://app.tryroll.com/tokens.json'
|
||||
const SET_LIST = 'https://raw.githubusercontent.com/SetProtocol/uniswap-tokenlist/main/set.tokenlist.json'
|
||||
const WRAPPED_LIST = 'wrapped.tokensoft.eth'
|
||||
|
||||
@@ -30,7 +29,6 @@ export const DEFAULT_INACTIVE_LIST_URLS: string[] = [
|
||||
GEMINI_LIST,
|
||||
WRAPPED_LIST,
|
||||
SET_LIST,
|
||||
ROLL_LIST,
|
||||
ARBITRUM_LIST,
|
||||
OPTIMISM_LIST,
|
||||
CELO_LIST,
|
||||
|
||||
@@ -2,4 +2,8 @@ export enum FeatureFlag {
|
||||
fiatOnramp = 'fiatOnramp',
|
||||
traceJsonRpc = 'traceJsonRpc',
|
||||
permit2 = 'permit2',
|
||||
nftListV2 = 'nftListV2',
|
||||
payWithAnyToken = 'payWithAnyToken',
|
||||
swapWidget = 'swapWidget',
|
||||
gqlRouting = 'gqlRouting',
|
||||
}
|
||||
|
||||
7
src/featureFlags/flags/gqlRouting.ts
Normal file
7
src/featureFlags/flags/gqlRouting.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { BaseVariant, FeatureFlag, useBaseFlag } from '../index'
|
||||
|
||||
export function useGqlRoutingFlag(): BaseVariant {
|
||||
return useBaseFlag(FeatureFlag.gqlRouting)
|
||||
}
|
||||
|
||||
export { BaseVariant as GqlRoutingVariant }
|
||||
7
src/featureFlags/flags/nftListV2.ts
Normal file
7
src/featureFlags/flags/nftListV2.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { BaseVariant } from '../index'
|
||||
|
||||
export function useNftListV2Flag(): BaseVariant {
|
||||
return BaseVariant.Enabled
|
||||
}
|
||||
|
||||
export { BaseVariant as NftListV2Variant }
|
||||
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 {
|
||||
|
||||
11
src/featureFlags/flags/swapWidget.ts
Normal file
11
src/featureFlags/flags/swapWidget.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { BaseVariant, FeatureFlag, useBaseFlag } from '../index'
|
||||
|
||||
export function useSwapWidgetFlag(): BaseVariant {
|
||||
return useBaseFlag(FeatureFlag.swapWidget, BaseVariant.Control)
|
||||
}
|
||||
|
||||
export function useSwapWidgetEnabled(): boolean {
|
||||
return useSwapWidgetFlag() === BaseVariant.Enabled
|
||||
}
|
||||
|
||||
export { BaseVariant as SwapWidgetVariant }
|
||||
@@ -14,8 +14,8 @@ The difference between Token and TokenProject:
|
||||
TokenProjectMarket is aggregated market data (aggregated over multiple dexes and centralized exchanges) that we get from coingecko.
|
||||
*/
|
||||
gql`
|
||||
query Token($contract: ContractInput!) {
|
||||
tokens(contracts: [$contract]) {
|
||||
query Token($chain: Chain!, $address: String = null) {
|
||||
token(chain: $chain, address: $address) {
|
||||
id
|
||||
decimals
|
||||
name
|
||||
@@ -23,31 +23,39 @@ gql`
|
||||
address
|
||||
symbol
|
||||
market(currency: USD) {
|
||||
id
|
||||
totalValueLocked {
|
||||
id
|
||||
value
|
||||
currency
|
||||
}
|
||||
price {
|
||||
id
|
||||
value
|
||||
currency
|
||||
}
|
||||
volume24H: volume(duration: DAY) {
|
||||
id
|
||||
value
|
||||
currency
|
||||
}
|
||||
priceHigh52W: priceHighLow(duration: YEAR, highLow: HIGH) {
|
||||
id
|
||||
value
|
||||
}
|
||||
priceLow52W: priceHighLow(duration: YEAR, highLow: LOW) {
|
||||
id
|
||||
value
|
||||
}
|
||||
}
|
||||
project {
|
||||
id
|
||||
description
|
||||
homepageUrl
|
||||
twitterName
|
||||
logoUrl
|
||||
tokens {
|
||||
id
|
||||
chain
|
||||
address
|
||||
}
|
||||
@@ -58,7 +66,7 @@ gql`
|
||||
|
||||
export type { Chain, TokenQuery } from './__generated__/types-and-hooks'
|
||||
|
||||
export type TokenQueryData = NonNullable<TokenQuery['tokens']>[number]
|
||||
export type TokenQueryData = TokenQuery['token']
|
||||
|
||||
// TODO: Return a QueryToken from useTokenQuery instead of TokenQueryData to make it more usable in Currency-centric interfaces.
|
||||
export class QueryToken extends WrappedTokenInfo {
|
||||
|
||||
@@ -1,14 +1,19 @@
|
||||
import gql from 'graphql-tag'
|
||||
|
||||
// TODO: Implemnt this as a refetchable fragment on tokenQuery when backend adds support
|
||||
gql`
|
||||
query TokenPrice($contract: ContractInput!, $duration: HistoryDuration!) {
|
||||
tokens(contracts: [$contract]) {
|
||||
query TokenPrice($chain: Chain!, $address: String = null, $duration: HistoryDuration!) {
|
||||
token(chain: $chain, address: $address) {
|
||||
id
|
||||
address
|
||||
chain
|
||||
market(currency: USD) {
|
||||
id
|
||||
price {
|
||||
id
|
||||
value
|
||||
}
|
||||
priceHistory(duration: $duration) {
|
||||
id
|
||||
timestamp
|
||||
value
|
||||
}
|
||||
|
||||
@@ -15,7 +15,15 @@ import {
|
||||
useTopTokens100Query,
|
||||
useTopTokensSparklineQuery,
|
||||
} from './__generated__/types-and-hooks'
|
||||
import { CHAIN_NAME_TO_CHAIN_ID, isPricePoint, PricePoint, toHistoryDuration, unwrapToken } from './util'
|
||||
import {
|
||||
CHAIN_NAME_TO_CHAIN_ID,
|
||||
isPricePoint,
|
||||
PollingInterval,
|
||||
PricePoint,
|
||||
toHistoryDuration,
|
||||
unwrapToken,
|
||||
usePollQueryWhileMounted,
|
||||
} from './util'
|
||||
|
||||
gql`
|
||||
query TopTokens100($duration: HistoryDuration!, $chain: Chain!) {
|
||||
@@ -26,24 +34,30 @@ gql`
|
||||
address
|
||||
symbol
|
||||
market(currency: USD) {
|
||||
id
|
||||
totalValueLocked {
|
||||
id
|
||||
value
|
||||
currency
|
||||
}
|
||||
price {
|
||||
id
|
||||
value
|
||||
currency
|
||||
}
|
||||
pricePercentChange(duration: $duration) {
|
||||
id
|
||||
currency
|
||||
value
|
||||
}
|
||||
volume(duration: $duration) {
|
||||
id
|
||||
value
|
||||
currency
|
||||
}
|
||||
}
|
||||
project {
|
||||
id
|
||||
logoUrl
|
||||
}
|
||||
}
|
||||
@@ -53,9 +67,13 @@ gql`
|
||||
gql`
|
||||
query TopTokensSparkline($duration: HistoryDuration!, $chain: Chain!) {
|
||||
topTokens(pageSize: 100, page: 1, chain: $chain) {
|
||||
id
|
||||
address
|
||||
chain
|
||||
market(currency: USD) {
|
||||
id
|
||||
priceHistory(duration: $duration) {
|
||||
id
|
||||
timestamp
|
||||
value
|
||||
}
|
||||
@@ -64,11 +82,12 @@ gql`
|
||||
}
|
||||
`
|
||||
|
||||
function useSortedTokens(tokens: NonNullable<TopTokens100Query['topTokens']>) {
|
||||
function useSortedTokens(tokens: TopTokens100Query['topTokens']) {
|
||||
const sortMethod = useAtomValue(sortMethodAtom)
|
||||
const sortAscending = useAtomValue(sortAscendingAtom)
|
||||
|
||||
return useMemo(() => {
|
||||
if (!tokens) return undefined
|
||||
let tokenArray = Array.from(tokens)
|
||||
switch (sortMethod) {
|
||||
case TokenSortMethod.PRICE:
|
||||
@@ -93,12 +112,13 @@ function useSortedTokens(tokens: NonNullable<TopTokens100Query['topTokens']>) {
|
||||
}, [tokens, sortMethod, sortAscending])
|
||||
}
|
||||
|
||||
function useFilteredTokens(tokens: NonNullable<TopTokens100Query['topTokens']>) {
|
||||
function useFilteredTokens(tokens: TopTokens100Query['topTokens']) {
|
||||
const filterString = useAtomValue(filterStringAtom)
|
||||
|
||||
const lowercaseFilterString = useMemo(() => filterString.toLowerCase(), [filterString])
|
||||
|
||||
return useMemo(() => {
|
||||
if (!tokens) return undefined
|
||||
let returnTokens = tokens
|
||||
if (lowercaseFilterString) {
|
||||
returnTokens = returnTokens?.filter((token) => {
|
||||
@@ -119,6 +139,7 @@ export type TopToken = NonNullable<NonNullable<TopTokens100Query>['topTokens']>[
|
||||
|
||||
interface UseTopTokensReturnValue {
|
||||
tokens: TopToken[] | undefined
|
||||
tokenVolumeRank: Record<string, number>
|
||||
loadingTokens: boolean
|
||||
sparklines: SparklineMap
|
||||
}
|
||||
@@ -127,9 +148,12 @@ export function useTopTokens(chain: Chain): UseTopTokensReturnValue {
|
||||
const chainId = CHAIN_NAME_TO_CHAIN_ID[chain]
|
||||
const duration = toHistoryDuration(useAtomValue(filterTimeAtom))
|
||||
|
||||
const { data: sparklineQuery } = useTopTokensSparklineQuery({
|
||||
variables: { duration, chain },
|
||||
})
|
||||
const { data: sparklineQuery } = usePollQueryWhileMounted(
|
||||
useTopTokensSparklineQuery({
|
||||
variables: { duration, chain },
|
||||
}),
|
||||
PollingInterval.Slow
|
||||
)
|
||||
|
||||
const sparklines = useMemo(() => {
|
||||
const unwrappedTokens = sparklineQuery?.topTokens?.map((topToken) => unwrapToken(chainId, topToken))
|
||||
@@ -140,14 +164,34 @@ export function useTopTokens(chain: Chain): UseTopTokensReturnValue {
|
||||
return map
|
||||
}, [chainId, sparklineQuery?.topTokens])
|
||||
|
||||
const { data, loading: loadingTokens } = useTopTokens100Query({
|
||||
variables: { duration, chain },
|
||||
})
|
||||
const mappedTokens = useMemo(
|
||||
() => data?.topTokens?.map((token) => unwrapToken(chainId, token)) ?? [],
|
||||
[chainId, data]
|
||||
const { data, loading: loadingTokens } = usePollQueryWhileMounted(
|
||||
useTopTokens100Query({
|
||||
variables: { duration, chain },
|
||||
}),
|
||||
PollingInterval.Fast
|
||||
)
|
||||
const filteredTokens = useFilteredTokens(mappedTokens)
|
||||
|
||||
const unwrappedTokens = useMemo(() => data?.topTokens?.map((token) => unwrapToken(chainId, token)), [chainId, data])
|
||||
const tokenVolumeRank = useMemo(
|
||||
() =>
|
||||
unwrappedTokens
|
||||
?.sort((a, b) => {
|
||||
if (!a.market?.volume || !b.market?.volume) return 0
|
||||
return a.market.volume.value > b.market.volume.value ? -1 : 1
|
||||
})
|
||||
.reduce((acc, cur, i) => {
|
||||
if (!cur.address) return acc
|
||||
return {
|
||||
...acc,
|
||||
[cur.address]: i + 1,
|
||||
}
|
||||
}, {}) ?? {},
|
||||
[unwrappedTokens]
|
||||
)
|
||||
const filteredTokens = useFilteredTokens(unwrappedTokens)
|
||||
const sortedTokens = useSortedTokens(filteredTokens)
|
||||
return useMemo(() => ({ tokens: sortedTokens, loadingTokens, sparklines }), [loadingTokens, sortedTokens, sparklines])
|
||||
return useMemo(
|
||||
() => ({ tokens: sortedTokens, tokenVolumeRank, loadingTokens, sparklines }),
|
||||
[loadingTokens, tokenVolumeRank, sortedTokens, sparklines]
|
||||
)
|
||||
}
|
||||
|
||||
244
src/graphql/data/__generated__/types-and-hooks.ts
generated
244
src/graphql/data/__generated__/types-and-hooks.ts
generated
@@ -386,6 +386,7 @@ export type NftCollectionTraitStats = {
|
||||
|
||||
export type NftCollectionsFilterInput = {
|
||||
addresses?: InputMaybe<Array<Scalars['String']>>;
|
||||
nameQuery?: InputMaybe<Scalars['String']>;
|
||||
};
|
||||
|
||||
export type NftContract = IContract & {
|
||||
@@ -468,12 +469,45 @@ export enum NftRarityProvider {
|
||||
RaritySniper = 'RARITY_SNIPER'
|
||||
}
|
||||
|
||||
export type NftRouteResponse = {
|
||||
__typename?: 'NftRouteResponse';
|
||||
calldata: Scalars['String'];
|
||||
id: Scalars['ID'];
|
||||
route?: Maybe<Array<NftTrade>>;
|
||||
sendAmount: TokenAmount;
|
||||
toAddress: Scalars['String'];
|
||||
};
|
||||
|
||||
export enum NftStandard {
|
||||
Erc721 = 'ERC721',
|
||||
Erc1155 = 'ERC1155',
|
||||
Noncompliant = 'NONCOMPLIANT'
|
||||
}
|
||||
|
||||
export type NftTrade = {
|
||||
__typename?: 'NftTrade';
|
||||
amount: Scalars['Int'];
|
||||
contractAddress: Scalars['String'];
|
||||
id: Scalars['ID'];
|
||||
marketplace: NftMarketplace;
|
||||
/** price represents the current price of the NFT, which can be different from quotePrice */
|
||||
price: TokenAmount;
|
||||
/** quotePrice represents the last quoted price of the NFT */
|
||||
quotePrice?: Maybe<TokenAmount>;
|
||||
tokenId: Scalars['String'];
|
||||
tokenType: NftStandard;
|
||||
};
|
||||
|
||||
export type NftTradeInput = {
|
||||
amount: Scalars['Int'];
|
||||
contractAddress: Scalars['String'];
|
||||
id: Scalars['ID'];
|
||||
marketplace: NftMarketplace;
|
||||
quotePrice?: InputMaybe<TokenAmountInput>;
|
||||
tokenId: Scalars['String'];
|
||||
tokenType: NftStandard;
|
||||
};
|
||||
|
||||
export type NftTransfer = {
|
||||
__typename?: 'NftTransfer';
|
||||
asset: NftAsset;
|
||||
@@ -504,6 +538,22 @@ export type PageInfo = {
|
||||
startCursor?: Maybe<Scalars['String']>;
|
||||
};
|
||||
|
||||
/** v2 pool parameters as defined by https://github.com/Uniswap/v2-sdk/blob/main/src/entities/pair.ts */
|
||||
export type PairInput = {
|
||||
tokenAmountA: TokenAmountInput;
|
||||
tokenAmountB: TokenAmountInput;
|
||||
};
|
||||
|
||||
/** v3 pool parameters as defined by https://github.com/Uniswap/v3-sdk/blob/main/src/entities/pool.ts */
|
||||
export type PoolInput = {
|
||||
fee: Scalars['Int'];
|
||||
liquidity: Scalars['String'];
|
||||
sqrtRatioX96: Scalars['String'];
|
||||
tickCurrent: Scalars['String'];
|
||||
tokenA: TokenInput;
|
||||
tokenB: TokenInput;
|
||||
};
|
||||
|
||||
export type Portfolio = {
|
||||
__typename?: 'Portfolio';
|
||||
assetActivities?: Maybe<Array<Maybe<AssetActivity>>>;
|
||||
@@ -514,7 +564,6 @@ export type Portfolio = {
|
||||
tokenBalances?: Maybe<Array<Maybe<TokenBalance>>>;
|
||||
tokensTotalDenominatedValue?: Maybe<Amount>;
|
||||
tokensTotalDenominatedValueChange?: Maybe<AmountChange>;
|
||||
tokensTotalDenominatedValueHistory?: Maybe<Array<Maybe<TimestampedAmount>>>;
|
||||
};
|
||||
|
||||
|
||||
@@ -528,11 +577,6 @@ export type PortfolioTokensTotalDenominatedValueChangeArgs = {
|
||||
duration?: InputMaybe<HistoryDuration>;
|
||||
};
|
||||
|
||||
|
||||
export type PortfolioTokensTotalDenominatedValueHistoryArgs = {
|
||||
duration?: InputMaybe<HistoryDuration>;
|
||||
};
|
||||
|
||||
export type Query = {
|
||||
__typename?: 'Query';
|
||||
assetActivities?: Maybe<Array<Maybe<AssetActivity>>>;
|
||||
@@ -540,6 +584,7 @@ export type Query = {
|
||||
nftBalances?: Maybe<NftBalanceConnection>;
|
||||
nftCollections?: Maybe<NftCollectionConnection>;
|
||||
nftCollectionsById?: Maybe<Array<Maybe<NftCollection>>>;
|
||||
nftRoute?: Maybe<NftRouteResponse>;
|
||||
portfolios?: Maybe<Array<Maybe<Portfolio>>>;
|
||||
searchTokenProjects?: Maybe<Array<Maybe<TokenProject>>>;
|
||||
searchTokens?: Maybe<Array<Maybe<Token>>>;
|
||||
@@ -595,6 +640,14 @@ export type QueryNftCollectionsByIdArgs = {
|
||||
};
|
||||
|
||||
|
||||
export type QueryNftRouteArgs = {
|
||||
chain?: InputMaybe<Chain>;
|
||||
nftTrades: Array<NftTradeInput>;
|
||||
senderAddress: Scalars['String'];
|
||||
tokenTrades?: InputMaybe<Array<TokenTradeInput>>;
|
||||
};
|
||||
|
||||
|
||||
export type QueryPortfoliosArgs = {
|
||||
ownerAddresses: Array<Scalars['String']>;
|
||||
useAltDataSource?: InputMaybe<Scalars['Boolean']>;
|
||||
@@ -667,6 +720,18 @@ export type TokenMarketArgs = {
|
||||
currency?: InputMaybe<Currency>;
|
||||
};
|
||||
|
||||
export type TokenAmount = {
|
||||
__typename?: 'TokenAmount';
|
||||
currency: Currency;
|
||||
id: Scalars['ID'];
|
||||
value: Scalars['String'];
|
||||
};
|
||||
|
||||
export type TokenAmountInput = {
|
||||
amount: Scalars['String'];
|
||||
token: TokenInput;
|
||||
};
|
||||
|
||||
export type TokenApproval = {
|
||||
__typename?: 'TokenApproval';
|
||||
approvedAddress: Scalars['String'];
|
||||
@@ -689,6 +754,13 @@ export type TokenBalance = {
|
||||
tokenProjectMarket?: Maybe<TokenProjectMarket>;
|
||||
};
|
||||
|
||||
export type TokenInput = {
|
||||
address: Scalars['String'];
|
||||
chainId: Scalars['Int'];
|
||||
decimals: Scalars['Int'];
|
||||
isNative: Scalars['Boolean'];
|
||||
};
|
||||
|
||||
export type TokenMarket = {
|
||||
__typename?: 'TokenMarket';
|
||||
id: Scalars['ID'];
|
||||
@@ -794,6 +866,30 @@ export enum TokenStandard {
|
||||
Native = 'NATIVE'
|
||||
}
|
||||
|
||||
export type TokenTradeInput = {
|
||||
routes?: InputMaybe<TokenTradeRoutesInput>;
|
||||
slippageToleranceBasisPoints?: InputMaybe<Scalars['Int']>;
|
||||
tokenAmount: TokenAmountInput;
|
||||
};
|
||||
|
||||
export type TokenTradeRouteInput = {
|
||||
inputAmount: TokenAmountInput;
|
||||
outputAmount: TokenAmountInput;
|
||||
pools: Array<TradePoolInput>;
|
||||
};
|
||||
|
||||
export type TokenTradeRoutesInput = {
|
||||
mixedRoutes?: InputMaybe<Array<TokenTradeRouteInput>>;
|
||||
tradeType: TokenTradeType;
|
||||
v2Routes?: InputMaybe<Array<TokenTradeRouteInput>>;
|
||||
v3Routes?: InputMaybe<Array<TokenTradeRouteInput>>;
|
||||
};
|
||||
|
||||
export enum TokenTradeType {
|
||||
ExactInput = 'EXACT_INPUT',
|
||||
ExactOutput = 'EXACT_OUTPUT'
|
||||
}
|
||||
|
||||
export type TokenTransfer = {
|
||||
__typename?: 'TokenTransfer';
|
||||
asset: Token;
|
||||
@@ -806,6 +902,11 @@ export type TokenTransfer = {
|
||||
transactedValue?: Maybe<Amount>;
|
||||
};
|
||||
|
||||
export type TradePoolInput = {
|
||||
pair?: InputMaybe<PairInput>;
|
||||
pool?: InputMaybe<PoolInput>;
|
||||
};
|
||||
|
||||
export type Transaction = {
|
||||
__typename?: 'Transaction';
|
||||
blockNumber: Scalars['Int'];
|
||||
@@ -832,19 +933,21 @@ export enum TransactionStatus {
|
||||
}
|
||||
|
||||
export type TokenQueryVariables = Exact<{
|
||||
contract: ContractInput;
|
||||
chain: Chain;
|
||||
address?: InputMaybe<Scalars['String']>;
|
||||
}>;
|
||||
|
||||
|
||||
export type TokenQuery = { __typename?: 'Query', tokens?: Array<{ __typename?: 'Token', id: string, decimals?: number, name?: string, chain: Chain, address?: string, symbol?: string, market?: { __typename?: 'TokenMarket', totalValueLocked?: { __typename?: 'Amount', value: number, currency?: Currency }, price?: { __typename?: 'Amount', value: number, currency?: Currency }, volume24H?: { __typename?: 'Amount', value: number, currency?: Currency }, priceHigh52W?: { __typename?: 'Amount', value: number }, priceLow52W?: { __typename?: 'Amount', value: number } }, project?: { __typename?: 'TokenProject', description?: string, homepageUrl?: string, twitterName?: string, logoUrl?: string, tokens: Array<{ __typename?: 'Token', chain: Chain, address?: string }> } }> };
|
||||
export type TokenQuery = { __typename?: 'Query', token?: { __typename?: 'Token', id: string, decimals?: number, name?: string, chain: Chain, address?: string, symbol?: string, market?: { __typename?: 'TokenMarket', id: string, totalValueLocked?: { __typename?: 'Amount', id: string, value: number, currency?: Currency }, price?: { __typename?: 'Amount', id: string, value: number, currency?: Currency }, volume24H?: { __typename?: 'Amount', id: string, value: number, currency?: Currency }, priceHigh52W?: { __typename?: 'Amount', id: string, value: number }, priceLow52W?: { __typename?: 'Amount', id: string, value: number } }, project?: { __typename?: 'TokenProject', id: string, description?: string, homepageUrl?: string, twitterName?: string, logoUrl?: string, tokens: Array<{ __typename?: 'Token', id: string, chain: Chain, address?: string }> } } };
|
||||
|
||||
export type TokenPriceQueryVariables = Exact<{
|
||||
contract: ContractInput;
|
||||
chain: Chain;
|
||||
address?: InputMaybe<Scalars['String']>;
|
||||
duration: HistoryDuration;
|
||||
}>;
|
||||
|
||||
|
||||
export type TokenPriceQuery = { __typename?: 'Query', tokens?: Array<{ __typename?: 'Token', market?: { __typename?: 'TokenMarket', price?: { __typename?: 'Amount', value: number }, priceHistory?: Array<{ __typename?: 'TimestampedAmount', timestamp: number, value: number }> } }> };
|
||||
export type TokenPriceQuery = { __typename?: 'Query', token?: { __typename?: 'Token', id: string, address?: string, chain: Chain, market?: { __typename?: 'TokenMarket', id: string, price?: { __typename?: 'Amount', id: string, value: number }, priceHistory?: Array<{ __typename?: 'TimestampedAmount', id: string, timestamp: number, value: number }> } } };
|
||||
|
||||
export type TopTokens100QueryVariables = Exact<{
|
||||
duration: HistoryDuration;
|
||||
@@ -852,7 +955,7 @@ export type TopTokens100QueryVariables = Exact<{
|
||||
}>;
|
||||
|
||||
|
||||
export type TopTokens100Query = { __typename?: 'Query', topTokens?: Array<{ __typename?: 'Token', id: string, name?: string, chain: Chain, address?: string, symbol?: string, market?: { __typename?: 'TokenMarket', totalValueLocked?: { __typename?: 'Amount', value: number, currency?: Currency }, price?: { __typename?: 'Amount', value: number, currency?: Currency }, pricePercentChange?: { __typename?: 'Amount', currency?: Currency, value: number }, volume?: { __typename?: 'Amount', value: number, currency?: Currency } }, project?: { __typename?: 'TokenProject', logoUrl?: string } }> };
|
||||
export type TopTokens100Query = { __typename?: 'Query', topTokens?: Array<{ __typename?: 'Token', id: string, name?: string, chain: Chain, address?: string, symbol?: string, market?: { __typename?: 'TokenMarket', id: string, totalValueLocked?: { __typename?: 'Amount', id: string, value: number, currency?: Currency }, price?: { __typename?: 'Amount', id: string, value: number, currency?: Currency }, pricePercentChange?: { __typename?: 'Amount', id: string, currency?: Currency, value: number }, volume?: { __typename?: 'Amount', id: string, value: number, currency?: Currency } }, project?: { __typename?: 'TokenProject', id: string, logoUrl?: string } }> };
|
||||
|
||||
export type TopTokensSparklineQueryVariables = Exact<{
|
||||
duration: HistoryDuration;
|
||||
@@ -860,7 +963,7 @@ export type TopTokensSparklineQueryVariables = Exact<{
|
||||
}>;
|
||||
|
||||
|
||||
export type TopTokensSparklineQuery = { __typename?: 'Query', topTokens?: Array<{ __typename?: 'Token', address?: string, market?: { __typename?: 'TokenMarket', priceHistory?: Array<{ __typename?: 'TimestampedAmount', timestamp: number, value: number }> } }> };
|
||||
export type TopTokensSparklineQuery = { __typename?: 'Query', topTokens?: Array<{ __typename?: 'Token', id: string, address?: string, chain: Chain, market?: { __typename?: 'TokenMarket', id: string, priceHistory?: Array<{ __typename?: 'TimestampedAmount', id: string, timestamp: number, value: number }> } }> };
|
||||
|
||||
export type AssetQueryVariables = Exact<{
|
||||
address: Scalars['String'];
|
||||
@@ -901,12 +1004,22 @@ export type NftBalanceQueryVariables = Exact<{
|
||||
}>;
|
||||
|
||||
|
||||
export type NftBalanceQuery = { __typename?: 'Query', nftBalances?: { __typename?: 'NftBalanceConnection', edges: Array<{ __typename?: 'NftBalanceEdge', node: { __typename?: 'NftBalance', listedMarketplaces?: Array<NftMarketplace>, ownedAsset?: { __typename?: 'NftAsset', id: string, animationUrl?: string, description?: string, flaggedBy?: string, name?: string, ownerAddress?: string, suspiciousFlag?: boolean, tokenId: string, collection?: { __typename?: 'NftCollection', isVerified?: boolean, name?: string, image?: { __typename?: 'Image', url: string }, nftContracts?: Array<{ __typename?: 'NftContract', address: string, chain: Chain, name?: string, standard?: NftStandard, symbol?: string, totalSupply?: number }>, markets?: Array<{ __typename?: 'NftCollectionMarket', floorPrice?: { __typename?: 'TimestampedAmount', value: number } }> }, image?: { __typename?: 'Image', url: string }, originalImage?: { __typename?: 'Image', url: string }, smallImage?: { __typename?: 'Image', url: string }, thumbnail?: { __typename?: 'Image', url: string }, listings?: { __typename?: 'NftOrderConnection', edges: Array<{ __typename?: 'NftOrderEdge', node: { __typename?: 'NftOrder', createdAt: number, marketplace: NftMarketplace, endAt?: number, price: { __typename?: 'Amount', value: number, currency?: Currency } } }> } }, listingFees?: Array<{ __typename?: 'NftFee', payoutAddress: string, basisPoints: number }>, lastPrice?: { __typename?: 'TimestampedAmount', currency?: Currency, timestamp: number, value: number } } }>, pageInfo: { __typename?: 'PageInfo', endCursor?: string, hasNextPage?: boolean, hasPreviousPage?: boolean, startCursor?: string } } };
|
||||
export type NftBalanceQuery = { __typename?: 'Query', nftBalances?: { __typename?: 'NftBalanceConnection', edges: Array<{ __typename?: 'NftBalanceEdge', node: { __typename?: 'NftBalance', listedMarketplaces?: Array<NftMarketplace>, ownedAsset?: { __typename?: 'NftAsset', id: string, animationUrl?: string, description?: string, flaggedBy?: string, name?: string, ownerAddress?: string, suspiciousFlag?: boolean, tokenId: string, collection?: { __typename?: 'NftCollection', isVerified?: boolean, name?: string, twitterName?: string, image?: { __typename?: 'Image', url: string }, nftContracts?: Array<{ __typename?: 'NftContract', address: string, chain: Chain, name?: string, standard?: NftStandard, symbol?: string, totalSupply?: number }>, markets?: Array<{ __typename?: 'NftCollectionMarket', floorPrice?: { __typename?: 'TimestampedAmount', value: number } }> }, image?: { __typename?: 'Image', url: string }, originalImage?: { __typename?: 'Image', url: string }, smallImage?: { __typename?: 'Image', url: string }, thumbnail?: { __typename?: 'Image', url: string }, listings?: { __typename?: 'NftOrderConnection', edges: Array<{ __typename?: 'NftOrderEdge', node: { __typename?: 'NftOrder', createdAt: number, marketplace: NftMarketplace, endAt?: number, price: { __typename?: 'Amount', value: number, currency?: Currency } } }> } }, listingFees?: Array<{ __typename?: 'NftFee', payoutAddress: string, basisPoints: number }>, lastPrice?: { __typename?: 'TimestampedAmount', currency?: Currency, timestamp: number, value: number } } }>, pageInfo: { __typename?: 'PageInfo', endCursor?: string, hasNextPage?: boolean, hasPreviousPage?: boolean, startCursor?: string } } };
|
||||
|
||||
export type NftRouteQueryVariables = Exact<{
|
||||
chain?: InputMaybe<Chain>;
|
||||
senderAddress: Scalars['String'];
|
||||
nftTrades: Array<NftTradeInput> | NftTradeInput;
|
||||
tokenTrades?: InputMaybe<Array<TokenTradeInput> | TokenTradeInput>;
|
||||
}>;
|
||||
|
||||
|
||||
export type NftRouteQuery = { __typename?: 'Query', nftRoute?: { __typename?: 'NftRouteResponse', calldata: string, toAddress: string, route?: Array<{ __typename?: 'NftTrade', amount: number, contractAddress: string, id: string, marketplace: NftMarketplace, tokenId: string, tokenType: NftStandard, price: { __typename?: 'TokenAmount', currency: Currency, value: string }, quotePrice?: { __typename?: 'TokenAmount', currency: Currency, value: string } }>, sendAmount: { __typename?: 'TokenAmount', currency: Currency, value: string } } };
|
||||
|
||||
|
||||
export const TokenDocument = gql`
|
||||
query Token($contract: ContractInput!) {
|
||||
tokens(contracts: [$contract]) {
|
||||
query Token($chain: Chain!, $address: String = null) {
|
||||
token(chain: $chain, address: $address) {
|
||||
id
|
||||
decimals
|
||||
name
|
||||
@@ -914,31 +1027,39 @@ export const TokenDocument = gql`
|
||||
address
|
||||
symbol
|
||||
market(currency: USD) {
|
||||
id
|
||||
totalValueLocked {
|
||||
id
|
||||
value
|
||||
currency
|
||||
}
|
||||
price {
|
||||
id
|
||||
value
|
||||
currency
|
||||
}
|
||||
volume24H: volume(duration: DAY) {
|
||||
id
|
||||
value
|
||||
currency
|
||||
}
|
||||
priceHigh52W: priceHighLow(duration: YEAR, highLow: HIGH) {
|
||||
id
|
||||
value
|
||||
}
|
||||
priceLow52W: priceHighLow(duration: YEAR, highLow: LOW) {
|
||||
id
|
||||
value
|
||||
}
|
||||
}
|
||||
project {
|
||||
id
|
||||
description
|
||||
homepageUrl
|
||||
twitterName
|
||||
logoUrl
|
||||
tokens {
|
||||
id
|
||||
chain
|
||||
address
|
||||
}
|
||||
@@ -959,7 +1080,8 @@ export const TokenDocument = gql`
|
||||
* @example
|
||||
* const { data, loading, error } = useTokenQuery({
|
||||
* variables: {
|
||||
* contract: // value for 'contract'
|
||||
* chain: // value for 'chain'
|
||||
* address: // value for 'address'
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
@@ -975,13 +1097,19 @@ export type TokenQueryHookResult = ReturnType<typeof useTokenQuery>;
|
||||
export type TokenLazyQueryHookResult = ReturnType<typeof useTokenLazyQuery>;
|
||||
export type TokenQueryResult = Apollo.QueryResult<TokenQuery, TokenQueryVariables>;
|
||||
export const TokenPriceDocument = gql`
|
||||
query TokenPrice($contract: ContractInput!, $duration: HistoryDuration!) {
|
||||
tokens(contracts: [$contract]) {
|
||||
query TokenPrice($chain: Chain!, $address: String = null, $duration: HistoryDuration!) {
|
||||
token(chain: $chain, address: $address) {
|
||||
id
|
||||
address
|
||||
chain
|
||||
market(currency: USD) {
|
||||
id
|
||||
price {
|
||||
id
|
||||
value
|
||||
}
|
||||
priceHistory(duration: $duration) {
|
||||
id
|
||||
timestamp
|
||||
value
|
||||
}
|
||||
@@ -1002,7 +1130,8 @@ export const TokenPriceDocument = gql`
|
||||
* @example
|
||||
* const { data, loading, error } = useTokenPriceQuery({
|
||||
* variables: {
|
||||
* contract: // value for 'contract'
|
||||
* chain: // value for 'chain'
|
||||
* address: // value for 'address'
|
||||
* duration: // value for 'duration'
|
||||
* },
|
||||
* });
|
||||
@@ -1027,24 +1156,30 @@ export const TopTokens100Document = gql`
|
||||
address
|
||||
symbol
|
||||
market(currency: USD) {
|
||||
id
|
||||
totalValueLocked {
|
||||
id
|
||||
value
|
||||
currency
|
||||
}
|
||||
price {
|
||||
id
|
||||
value
|
||||
currency
|
||||
}
|
||||
pricePercentChange(duration: $duration) {
|
||||
id
|
||||
currency
|
||||
value
|
||||
}
|
||||
volume(duration: $duration) {
|
||||
id
|
||||
value
|
||||
currency
|
||||
}
|
||||
}
|
||||
project {
|
||||
id
|
||||
logoUrl
|
||||
}
|
||||
}
|
||||
@@ -1082,9 +1217,13 @@ export type TopTokens100QueryResult = Apollo.QueryResult<TopTokens100Query, TopT
|
||||
export const TopTokensSparklineDocument = gql`
|
||||
query TopTokensSparkline($duration: HistoryDuration!, $chain: Chain!) {
|
||||
topTokens(pageSize: 100, page: 1, chain: $chain) {
|
||||
id
|
||||
address
|
||||
chain
|
||||
market(currency: USD) {
|
||||
id
|
||||
priceHistory(duration: $duration) {
|
||||
id
|
||||
timestamp
|
||||
value
|
||||
}
|
||||
@@ -1493,6 +1632,7 @@ export const NftBalanceDocument = gql`
|
||||
url
|
||||
}
|
||||
name
|
||||
twitterName
|
||||
nftContracts {
|
||||
address
|
||||
chain
|
||||
@@ -1592,4 +1732,68 @@ export function useNftBalanceLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions
|
||||
}
|
||||
export type NftBalanceQueryHookResult = ReturnType<typeof useNftBalanceQuery>;
|
||||
export type NftBalanceLazyQueryHookResult = ReturnType<typeof useNftBalanceLazyQuery>;
|
||||
export type NftBalanceQueryResult = Apollo.QueryResult<NftBalanceQuery, NftBalanceQueryVariables>;
|
||||
export type NftBalanceQueryResult = Apollo.QueryResult<NftBalanceQuery, NftBalanceQueryVariables>;
|
||||
export const NftRouteDocument = gql`
|
||||
query NftRoute($chain: Chain = ETHEREUM, $senderAddress: String!, $nftTrades: [NftTradeInput!]!, $tokenTrades: [TokenTradeInput!]) {
|
||||
nftRoute(
|
||||
chain: $chain
|
||||
senderAddress: $senderAddress
|
||||
nftTrades: $nftTrades
|
||||
tokenTrades: $tokenTrades
|
||||
) {
|
||||
calldata
|
||||
route {
|
||||
amount
|
||||
contractAddress
|
||||
id
|
||||
marketplace
|
||||
price {
|
||||
currency
|
||||
value
|
||||
}
|
||||
quotePrice {
|
||||
currency
|
||||
value
|
||||
}
|
||||
tokenId
|
||||
tokenType
|
||||
}
|
||||
sendAmount {
|
||||
currency
|
||||
value
|
||||
}
|
||||
toAddress
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
/**
|
||||
* __useNftRouteQuery__
|
||||
*
|
||||
* To run a query within a React component, call `useNftRouteQuery` and pass it any options that fit your needs.
|
||||
* When your component renders, `useNftRouteQuery` returns an object from Apollo Client that contains loading, error, and data properties
|
||||
* you can use to render your UI.
|
||||
*
|
||||
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
|
||||
*
|
||||
* @example
|
||||
* const { data, loading, error } = useNftRouteQuery({
|
||||
* variables: {
|
||||
* chain: // value for 'chain'
|
||||
* senderAddress: // value for 'senderAddress'
|
||||
* nftTrades: // value for 'nftTrades'
|
||||
* tokenTrades: // value for 'tokenTrades'
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
export function useNftRouteQuery(baseOptions: Apollo.QueryHookOptions<NftRouteQuery, NftRouteQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useQuery<NftRouteQuery, NftRouteQueryVariables>(NftRouteDocument, options);
|
||||
}
|
||||
export function useNftRouteLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<NftRouteQuery, NftRouteQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useLazyQuery<NftRouteQuery, NftRouteQueryVariables>(NftRouteDocument, options);
|
||||
}
|
||||
export type NftRouteQueryHookResult = ReturnType<typeof useNftRouteQuery>;
|
||||
export type NftRouteLazyQueryHookResult = ReturnType<typeof useNftRouteLazyQuery>;
|
||||
export type NftRouteQueryResult = Apollo.QueryResult<NftRouteQuery, NftRouteQueryVariables>;
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ApolloClient, InMemoryCache } from '@apollo/client'
|
||||
import { relayStylePagination } from '@apollo/client/utilities'
|
||||
import { Reference, relayStylePagination } from '@apollo/client/utilities'
|
||||
|
||||
const GRAPHQL_URL = process.env.REACT_APP_AWS_API_ENDPOINT
|
||||
if (!GRAPHQL_URL) {
|
||||
@@ -7,6 +7,7 @@ if (!GRAPHQL_URL) {
|
||||
}
|
||||
|
||||
export const apolloClient = new ApolloClient({
|
||||
connectToDevTools: true,
|
||||
uri: GRAPHQL_URL,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
@@ -18,6 +19,33 @@ export const apolloClient = new ApolloClient({
|
||||
fields: {
|
||||
nftBalances: relayStylePagination(),
|
||||
nftAssets: relayStylePagination(),
|
||||
// tell apollo client how to reference Token items in the cache after being fetched by queries that return Token[]
|
||||
token: {
|
||||
read(_, { args, toReference }): Reference | undefined {
|
||||
return toReference({
|
||||
__typename: 'Token',
|
||||
chain: args?.chain,
|
||||
address: args?.address,
|
||||
})
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Token: {
|
||||
// key by chain, address combination so that Token(chain, address) endpoint can read from cache
|
||||
/**
|
||||
* NOTE: In any query for `token` or `tokens`, you must include the `chain` and `address` fields
|
||||
* in order for result to normalize properly in the cache.
|
||||
*/
|
||||
keyFields: ['chain', 'address'],
|
||||
fields: {
|
||||
address: {
|
||||
read(address: string | null): string | null {
|
||||
// backend endpoint sometimes returns checksummed, sometimes lowercased addresses
|
||||
// always use lowercased addresses in our app for consistency
|
||||
return address?.toLowerCase() ?? null
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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])
|
||||
}
|
||||
|
||||
@@ -34,6 +34,7 @@ gql`
|
||||
url
|
||||
}
|
||||
name
|
||||
twitterName
|
||||
nftContracts {
|
||||
address
|
||||
chain
|
||||
@@ -166,7 +167,12 @@ export function useNftBalance(
|
||||
image_url: asset?.collection?.image?.url,
|
||||
payout_address: queryAsset?.node?.listingFees?.[0]?.payoutAddress,
|
||||
},
|
||||
collection: asset?.collection as unknown as GenieCollection,
|
||||
collection: {
|
||||
name: asset.collection?.name,
|
||||
isVerified: asset.collection?.isVerified,
|
||||
imageUrl: asset.collection?.image?.url,
|
||||
twitterUrl: asset.collection?.twitterName ? `@${asset.collection?.twitterName}` : undefined,
|
||||
} as GenieCollection,
|
||||
collectionIsVerified: asset?.collection?.isVerified,
|
||||
lastPrice: queryAsset.node.lastPrice?.value,
|
||||
floorPrice: asset?.collection?.markets?.[0]?.floorPrice?.value,
|
||||
|
||||
47
src/graphql/data/nft/Routing.ts
Normal file
47
src/graphql/data/nft/Routing.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import gql from 'graphql-tag'
|
||||
|
||||
import { NftTradeInput, TokenTradeInput, useNftRouteQuery } from '../__generated__/types-and-hooks'
|
||||
|
||||
gql`
|
||||
query NftRoute(
|
||||
$chain: Chain = ETHEREUM
|
||||
$senderAddress: String!
|
||||
$nftTrades: [NftTradeInput!]!
|
||||
$tokenTrades: [TokenTradeInput!]
|
||||
) {
|
||||
nftRoute(chain: $chain, senderAddress: $senderAddress, nftTrades: $nftTrades, tokenTrades: $tokenTrades) {
|
||||
calldata
|
||||
route {
|
||||
amount
|
||||
contractAddress
|
||||
id
|
||||
marketplace
|
||||
price {
|
||||
currency
|
||||
value
|
||||
}
|
||||
quotePrice {
|
||||
currency
|
||||
value
|
||||
}
|
||||
tokenId
|
||||
tokenType
|
||||
}
|
||||
sendAmount {
|
||||
currency
|
||||
value
|
||||
}
|
||||
toAddress
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export function useNftRoute(senderAddress: string, nftTrades: NftTradeInput[], tokenTrades?: TokenTradeInput[]) {
|
||||
return useNftRouteQuery({
|
||||
variables: {
|
||||
senderAddress,
|
||||
nftTrades,
|
||||
tokenTrades,
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -1,9 +1,31 @@
|
||||
import { QueryResult } from '@apollo/client'
|
||||
import { SupportedChainId } from 'constants/chains'
|
||||
import { ZERO_ADDRESS } from 'constants/misc'
|
||||
import { NATIVE_CHAIN_ID, nativeOnChain, WRAPPED_NATIVE_CURRENCY } from 'constants/tokens'
|
||||
import ms from 'ms.macro'
|
||||
import { useEffect } from 'react'
|
||||
|
||||
import { Chain, HistoryDuration } from './__generated__/types-and-hooks'
|
||||
|
||||
export enum PollingInterval {
|
||||
Slow = ms`5m`,
|
||||
Normal = ms`1m`,
|
||||
Fast = ms`12s`, // 12 seconds, block times for mainnet
|
||||
LightningMcQueen = ms`3s`, // 3 seconds, approx block times for polygon
|
||||
}
|
||||
|
||||
// Polls a query only when the current component is mounted, as useQuery's pollInterval prop will continue to poll after unmount
|
||||
export function usePollQueryWhileMounted<T, K>(queryResult: QueryResult<T, K>, interval: PollingInterval) {
|
||||
const { startPolling, stopPolling } = queryResult
|
||||
|
||||
useEffect(() => {
|
||||
startPolling(interval)
|
||||
return stopPolling
|
||||
}, [interval, startPolling, stopPolling])
|
||||
|
||||
return queryResult
|
||||
}
|
||||
|
||||
export enum TimePeriod {
|
||||
HOUR,
|
||||
DAY,
|
||||
|
||||
@@ -37,6 +37,7 @@ export type Bundle = {
|
||||
export type Bundle_Filter = {
|
||||
/** Filter for the block changed event. */
|
||||
_change_block?: InputMaybe<BlockChangedFilter>;
|
||||
and?: InputMaybe<Array<InputMaybe<Bundle_Filter>>>;
|
||||
ethPriceUSD?: InputMaybe<Scalars['BigDecimal']>;
|
||||
ethPriceUSD_gt?: InputMaybe<Scalars['BigDecimal']>;
|
||||
ethPriceUSD_gte?: InputMaybe<Scalars['BigDecimal']>;
|
||||
@@ -53,6 +54,7 @@ export type Bundle_Filter = {
|
||||
id_lte?: InputMaybe<Scalars['ID']>;
|
||||
id_not?: InputMaybe<Scalars['ID']>;
|
||||
id_not_in?: InputMaybe<Array<Scalars['ID']>>;
|
||||
or?: InputMaybe<Array<InputMaybe<Bundle_Filter>>>;
|
||||
};
|
||||
|
||||
export enum Bundle_OrderBy {
|
||||
@@ -114,6 +116,7 @@ export type Burn_Filter = {
|
||||
amount_lte?: InputMaybe<Scalars['BigInt']>;
|
||||
amount_not?: InputMaybe<Scalars['BigInt']>;
|
||||
amount_not_in?: InputMaybe<Array<Scalars['BigInt']>>;
|
||||
and?: InputMaybe<Array<InputMaybe<Burn_Filter>>>;
|
||||
id?: InputMaybe<Scalars['ID']>;
|
||||
id_gt?: InputMaybe<Scalars['ID']>;
|
||||
id_gte?: InputMaybe<Scalars['ID']>;
|
||||
@@ -130,15 +133,24 @@ export type Burn_Filter = {
|
||||
logIndex_lte?: InputMaybe<Scalars['BigInt']>;
|
||||
logIndex_not?: InputMaybe<Scalars['BigInt']>;
|
||||
logIndex_not_in?: InputMaybe<Array<Scalars['BigInt']>>;
|
||||
or?: InputMaybe<Array<InputMaybe<Burn_Filter>>>;
|
||||
origin?: InputMaybe<Scalars['Bytes']>;
|
||||
origin_contains?: InputMaybe<Scalars['Bytes']>;
|
||||
origin_gt?: InputMaybe<Scalars['Bytes']>;
|
||||
origin_gte?: InputMaybe<Scalars['Bytes']>;
|
||||
origin_in?: InputMaybe<Array<Scalars['Bytes']>>;
|
||||
origin_lt?: InputMaybe<Scalars['Bytes']>;
|
||||
origin_lte?: InputMaybe<Scalars['Bytes']>;
|
||||
origin_not?: InputMaybe<Scalars['Bytes']>;
|
||||
origin_not_contains?: InputMaybe<Scalars['Bytes']>;
|
||||
origin_not_in?: InputMaybe<Array<Scalars['Bytes']>>;
|
||||
owner?: InputMaybe<Scalars['Bytes']>;
|
||||
owner_contains?: InputMaybe<Scalars['Bytes']>;
|
||||
owner_gt?: InputMaybe<Scalars['Bytes']>;
|
||||
owner_gte?: InputMaybe<Scalars['Bytes']>;
|
||||
owner_in?: InputMaybe<Array<Scalars['Bytes']>>;
|
||||
owner_lt?: InputMaybe<Scalars['Bytes']>;
|
||||
owner_lte?: InputMaybe<Scalars['Bytes']>;
|
||||
owner_not?: InputMaybe<Scalars['Bytes']>;
|
||||
owner_not_contains?: InputMaybe<Scalars['Bytes']>;
|
||||
owner_not_in?: InputMaybe<Array<Scalars['Bytes']>>;
|
||||
@@ -312,6 +324,7 @@ export type Collect_Filter = {
|
||||
amountUSD_lte?: InputMaybe<Scalars['BigDecimal']>;
|
||||
amountUSD_not?: InputMaybe<Scalars['BigDecimal']>;
|
||||
amountUSD_not_in?: InputMaybe<Array<Scalars['BigDecimal']>>;
|
||||
and?: InputMaybe<Array<InputMaybe<Collect_Filter>>>;
|
||||
id?: InputMaybe<Scalars['ID']>;
|
||||
id_gt?: InputMaybe<Scalars['ID']>;
|
||||
id_gte?: InputMaybe<Scalars['ID']>;
|
||||
@@ -328,9 +341,14 @@ export type Collect_Filter = {
|
||||
logIndex_lte?: InputMaybe<Scalars['BigInt']>;
|
||||
logIndex_not?: InputMaybe<Scalars['BigInt']>;
|
||||
logIndex_not_in?: InputMaybe<Array<Scalars['BigInt']>>;
|
||||
or?: InputMaybe<Array<InputMaybe<Collect_Filter>>>;
|
||||
owner?: InputMaybe<Scalars['Bytes']>;
|
||||
owner_contains?: InputMaybe<Scalars['Bytes']>;
|
||||
owner_gt?: InputMaybe<Scalars['Bytes']>;
|
||||
owner_gte?: InputMaybe<Scalars['Bytes']>;
|
||||
owner_in?: InputMaybe<Array<Scalars['Bytes']>>;
|
||||
owner_lt?: InputMaybe<Scalars['Bytes']>;
|
||||
owner_lte?: InputMaybe<Scalars['Bytes']>;
|
||||
owner_not?: InputMaybe<Scalars['Bytes']>;
|
||||
owner_not_contains?: InputMaybe<Scalars['Bytes']>;
|
||||
owner_not_in?: InputMaybe<Array<Scalars['Bytes']>>;
|
||||
@@ -436,6 +454,7 @@ export type Factory = {
|
||||
export type Factory_Filter = {
|
||||
/** Filter for the block changed event. */
|
||||
_change_block?: InputMaybe<BlockChangedFilter>;
|
||||
and?: InputMaybe<Array<InputMaybe<Factory_Filter>>>;
|
||||
id?: InputMaybe<Scalars['ID']>;
|
||||
id_gt?: InputMaybe<Scalars['ID']>;
|
||||
id_gte?: InputMaybe<Scalars['ID']>;
|
||||
@@ -444,6 +463,7 @@ export type Factory_Filter = {
|
||||
id_lte?: InputMaybe<Scalars['ID']>;
|
||||
id_not?: InputMaybe<Scalars['ID']>;
|
||||
id_not_in?: InputMaybe<Array<Scalars['ID']>>;
|
||||
or?: InputMaybe<Array<InputMaybe<Factory_Filter>>>;
|
||||
owner?: InputMaybe<Scalars['ID']>;
|
||||
owner_gt?: InputMaybe<Scalars['ID']>;
|
||||
owner_gte?: InputMaybe<Scalars['ID']>;
|
||||
@@ -617,6 +637,7 @@ export type Flash_Filter = {
|
||||
amountUSD_lte?: InputMaybe<Scalars['BigDecimal']>;
|
||||
amountUSD_not?: InputMaybe<Scalars['BigDecimal']>;
|
||||
amountUSD_not_in?: InputMaybe<Array<Scalars['BigDecimal']>>;
|
||||
and?: InputMaybe<Array<InputMaybe<Flash_Filter>>>;
|
||||
id?: InputMaybe<Scalars['ID']>;
|
||||
id_gt?: InputMaybe<Scalars['ID']>;
|
||||
id_gte?: InputMaybe<Scalars['ID']>;
|
||||
@@ -633,6 +654,7 @@ export type Flash_Filter = {
|
||||
logIndex_lte?: InputMaybe<Scalars['BigInt']>;
|
||||
logIndex_not?: InputMaybe<Scalars['BigInt']>;
|
||||
logIndex_not_in?: InputMaybe<Array<Scalars['BigInt']>>;
|
||||
or?: InputMaybe<Array<InputMaybe<Flash_Filter>>>;
|
||||
pool?: InputMaybe<Scalars['String']>;
|
||||
pool_?: InputMaybe<Pool_Filter>;
|
||||
pool_contains?: InputMaybe<Scalars['String']>;
|
||||
@@ -656,13 +678,21 @@ export type Flash_Filter = {
|
||||
pool_starts_with_nocase?: InputMaybe<Scalars['String']>;
|
||||
recipient?: InputMaybe<Scalars['Bytes']>;
|
||||
recipient_contains?: InputMaybe<Scalars['Bytes']>;
|
||||
recipient_gt?: InputMaybe<Scalars['Bytes']>;
|
||||
recipient_gte?: InputMaybe<Scalars['Bytes']>;
|
||||
recipient_in?: InputMaybe<Array<Scalars['Bytes']>>;
|
||||
recipient_lt?: InputMaybe<Scalars['Bytes']>;
|
||||
recipient_lte?: InputMaybe<Scalars['Bytes']>;
|
||||
recipient_not?: InputMaybe<Scalars['Bytes']>;
|
||||
recipient_not_contains?: InputMaybe<Scalars['Bytes']>;
|
||||
recipient_not_in?: InputMaybe<Array<Scalars['Bytes']>>;
|
||||
sender?: InputMaybe<Scalars['Bytes']>;
|
||||
sender_contains?: InputMaybe<Scalars['Bytes']>;
|
||||
sender_gt?: InputMaybe<Scalars['Bytes']>;
|
||||
sender_gte?: InputMaybe<Scalars['Bytes']>;
|
||||
sender_in?: InputMaybe<Array<Scalars['Bytes']>>;
|
||||
sender_lt?: InputMaybe<Scalars['Bytes']>;
|
||||
sender_lte?: InputMaybe<Scalars['Bytes']>;
|
||||
sender_not?: InputMaybe<Scalars['Bytes']>;
|
||||
sender_not_contains?: InputMaybe<Scalars['Bytes']>;
|
||||
sender_not_in?: InputMaybe<Array<Scalars['Bytes']>>;
|
||||
@@ -767,6 +797,7 @@ export type Mint_Filter = {
|
||||
amount_lte?: InputMaybe<Scalars['BigInt']>;
|
||||
amount_not?: InputMaybe<Scalars['BigInt']>;
|
||||
amount_not_in?: InputMaybe<Array<Scalars['BigInt']>>;
|
||||
and?: InputMaybe<Array<InputMaybe<Mint_Filter>>>;
|
||||
id?: InputMaybe<Scalars['ID']>;
|
||||
id_gt?: InputMaybe<Scalars['ID']>;
|
||||
id_gte?: InputMaybe<Scalars['ID']>;
|
||||
@@ -783,15 +814,24 @@ export type Mint_Filter = {
|
||||
logIndex_lte?: InputMaybe<Scalars['BigInt']>;
|
||||
logIndex_not?: InputMaybe<Scalars['BigInt']>;
|
||||
logIndex_not_in?: InputMaybe<Array<Scalars['BigInt']>>;
|
||||
or?: InputMaybe<Array<InputMaybe<Mint_Filter>>>;
|
||||
origin?: InputMaybe<Scalars['Bytes']>;
|
||||
origin_contains?: InputMaybe<Scalars['Bytes']>;
|
||||
origin_gt?: InputMaybe<Scalars['Bytes']>;
|
||||
origin_gte?: InputMaybe<Scalars['Bytes']>;
|
||||
origin_in?: InputMaybe<Array<Scalars['Bytes']>>;
|
||||
origin_lt?: InputMaybe<Scalars['Bytes']>;
|
||||
origin_lte?: InputMaybe<Scalars['Bytes']>;
|
||||
origin_not?: InputMaybe<Scalars['Bytes']>;
|
||||
origin_not_contains?: InputMaybe<Scalars['Bytes']>;
|
||||
origin_not_in?: InputMaybe<Array<Scalars['Bytes']>>;
|
||||
owner?: InputMaybe<Scalars['Bytes']>;
|
||||
owner_contains?: InputMaybe<Scalars['Bytes']>;
|
||||
owner_gt?: InputMaybe<Scalars['Bytes']>;
|
||||
owner_gte?: InputMaybe<Scalars['Bytes']>;
|
||||
owner_in?: InputMaybe<Array<Scalars['Bytes']>>;
|
||||
owner_lt?: InputMaybe<Scalars['Bytes']>;
|
||||
owner_lte?: InputMaybe<Scalars['Bytes']>;
|
||||
owner_not?: InputMaybe<Scalars['Bytes']>;
|
||||
owner_not_contains?: InputMaybe<Scalars['Bytes']>;
|
||||
owner_not_in?: InputMaybe<Array<Scalars['Bytes']>>;
|
||||
@@ -818,7 +858,11 @@ export type Mint_Filter = {
|
||||
pool_starts_with_nocase?: InputMaybe<Scalars['String']>;
|
||||
sender?: InputMaybe<Scalars['Bytes']>;
|
||||
sender_contains?: InputMaybe<Scalars['Bytes']>;
|
||||
sender_gt?: InputMaybe<Scalars['Bytes']>;
|
||||
sender_gte?: InputMaybe<Scalars['Bytes']>;
|
||||
sender_in?: InputMaybe<Array<Scalars['Bytes']>>;
|
||||
sender_lt?: InputMaybe<Scalars['Bytes']>;
|
||||
sender_lte?: InputMaybe<Scalars['Bytes']>;
|
||||
sender_not?: InputMaybe<Scalars['Bytes']>;
|
||||
sender_not_contains?: InputMaybe<Scalars['Bytes']>;
|
||||
sender_not_in?: InputMaybe<Array<Scalars['Bytes']>>;
|
||||
@@ -1066,6 +1110,7 @@ export type PoolDayData = {
|
||||
export type PoolDayData_Filter = {
|
||||
/** Filter for the block changed event. */
|
||||
_change_block?: InputMaybe<BlockChangedFilter>;
|
||||
and?: InputMaybe<Array<InputMaybe<PoolDayData_Filter>>>;
|
||||
close?: InputMaybe<Scalars['BigDecimal']>;
|
||||
close_gt?: InputMaybe<Scalars['BigDecimal']>;
|
||||
close_gte?: InputMaybe<Scalars['BigDecimal']>;
|
||||
@@ -1146,6 +1191,7 @@ export type PoolDayData_Filter = {
|
||||
open_lte?: InputMaybe<Scalars['BigDecimal']>;
|
||||
open_not?: InputMaybe<Scalars['BigDecimal']>;
|
||||
open_not_in?: InputMaybe<Array<Scalars['BigDecimal']>>;
|
||||
or?: InputMaybe<Array<InputMaybe<PoolDayData_Filter>>>;
|
||||
pool?: InputMaybe<Scalars['String']>;
|
||||
pool_?: InputMaybe<Pool_Filter>;
|
||||
pool_contains?: InputMaybe<Scalars['String']>;
|
||||
@@ -1291,6 +1337,7 @@ export type PoolHourData = {
|
||||
export type PoolHourData_Filter = {
|
||||
/** Filter for the block changed event. */
|
||||
_change_block?: InputMaybe<BlockChangedFilter>;
|
||||
and?: InputMaybe<Array<InputMaybe<PoolHourData_Filter>>>;
|
||||
close?: InputMaybe<Scalars['BigDecimal']>;
|
||||
close_gt?: InputMaybe<Scalars['BigDecimal']>;
|
||||
close_gte?: InputMaybe<Scalars['BigDecimal']>;
|
||||
@@ -1363,6 +1410,7 @@ export type PoolHourData_Filter = {
|
||||
open_lte?: InputMaybe<Scalars['BigDecimal']>;
|
||||
open_not?: InputMaybe<Scalars['BigDecimal']>;
|
||||
open_not_in?: InputMaybe<Array<Scalars['BigDecimal']>>;
|
||||
or?: InputMaybe<Array<InputMaybe<PoolHourData_Filter>>>;
|
||||
periodStartUnix?: InputMaybe<Scalars['Int']>;
|
||||
periodStartUnix_gt?: InputMaybe<Scalars['Int']>;
|
||||
periodStartUnix_gte?: InputMaybe<Scalars['Int']>;
|
||||
@@ -1492,6 +1540,7 @@ export enum PoolHourData_OrderBy {
|
||||
export type Pool_Filter = {
|
||||
/** Filter for the block changed event. */
|
||||
_change_block?: InputMaybe<BlockChangedFilter>;
|
||||
and?: InputMaybe<Array<InputMaybe<Pool_Filter>>>;
|
||||
burns_?: InputMaybe<Burn_Filter>;
|
||||
collectedFeesToken0?: InputMaybe<Scalars['BigDecimal']>;
|
||||
collectedFeesToken0_gt?: InputMaybe<Scalars['BigDecimal']>;
|
||||
@@ -1599,6 +1648,7 @@ export type Pool_Filter = {
|
||||
observationIndex_lte?: InputMaybe<Scalars['BigInt']>;
|
||||
observationIndex_not?: InputMaybe<Scalars['BigInt']>;
|
||||
observationIndex_not_in?: InputMaybe<Array<Scalars['BigInt']>>;
|
||||
or?: InputMaybe<Array<InputMaybe<Pool_Filter>>>;
|
||||
poolDayData_?: InputMaybe<PoolDayData_Filter>;
|
||||
poolHourData_?: InputMaybe<PoolHourData_Filter>;
|
||||
sqrtPrice?: InputMaybe<Scalars['BigInt']>;
|
||||
@@ -1842,6 +1892,7 @@ export type PositionSnapshot = {
|
||||
export type PositionSnapshot_Filter = {
|
||||
/** Filter for the block changed event. */
|
||||
_change_block?: InputMaybe<BlockChangedFilter>;
|
||||
and?: InputMaybe<Array<InputMaybe<PositionSnapshot_Filter>>>;
|
||||
blockNumber?: InputMaybe<Scalars['BigInt']>;
|
||||
blockNumber_gt?: InputMaybe<Scalars['BigInt']>;
|
||||
blockNumber_gte?: InputMaybe<Scalars['BigInt']>;
|
||||
@@ -1914,9 +1965,14 @@ export type PositionSnapshot_Filter = {
|
||||
liquidity_lte?: InputMaybe<Scalars['BigInt']>;
|
||||
liquidity_not?: InputMaybe<Scalars['BigInt']>;
|
||||
liquidity_not_in?: InputMaybe<Array<Scalars['BigInt']>>;
|
||||
or?: InputMaybe<Array<InputMaybe<PositionSnapshot_Filter>>>;
|
||||
owner?: InputMaybe<Scalars['Bytes']>;
|
||||
owner_contains?: InputMaybe<Scalars['Bytes']>;
|
||||
owner_gt?: InputMaybe<Scalars['Bytes']>;
|
||||
owner_gte?: InputMaybe<Scalars['Bytes']>;
|
||||
owner_in?: InputMaybe<Array<Scalars['Bytes']>>;
|
||||
owner_lt?: InputMaybe<Scalars['Bytes']>;
|
||||
owner_lte?: InputMaybe<Scalars['Bytes']>;
|
||||
owner_not?: InputMaybe<Scalars['Bytes']>;
|
||||
owner_not_contains?: InputMaybe<Scalars['Bytes']>;
|
||||
owner_not_in?: InputMaybe<Array<Scalars['Bytes']>>;
|
||||
@@ -2031,6 +2087,7 @@ export enum PositionSnapshot_OrderBy {
|
||||
export type Position_Filter = {
|
||||
/** Filter for the block changed event. */
|
||||
_change_block?: InputMaybe<BlockChangedFilter>;
|
||||
and?: InputMaybe<Array<InputMaybe<Position_Filter>>>;
|
||||
collectedFeesToken0?: InputMaybe<Scalars['BigDecimal']>;
|
||||
collectedFeesToken0_gt?: InputMaybe<Scalars['BigDecimal']>;
|
||||
collectedFeesToken0_gte?: InputMaybe<Scalars['BigDecimal']>;
|
||||
@@ -2095,9 +2152,14 @@ export type Position_Filter = {
|
||||
liquidity_lte?: InputMaybe<Scalars['BigInt']>;
|
||||
liquidity_not?: InputMaybe<Scalars['BigInt']>;
|
||||
liquidity_not_in?: InputMaybe<Array<Scalars['BigInt']>>;
|
||||
or?: InputMaybe<Array<InputMaybe<Position_Filter>>>;
|
||||
owner?: InputMaybe<Scalars['Bytes']>;
|
||||
owner_contains?: InputMaybe<Scalars['Bytes']>;
|
||||
owner_gt?: InputMaybe<Scalars['Bytes']>;
|
||||
owner_gte?: InputMaybe<Scalars['Bytes']>;
|
||||
owner_in?: InputMaybe<Array<Scalars['Bytes']>>;
|
||||
owner_lt?: InputMaybe<Scalars['Bytes']>;
|
||||
owner_lte?: InputMaybe<Scalars['Bytes']>;
|
||||
owner_not?: InputMaybe<Scalars['Bytes']>;
|
||||
owner_not_contains?: InputMaybe<Scalars['Bytes']>;
|
||||
owner_not_in?: InputMaybe<Array<Scalars['Bytes']>>;
|
||||
@@ -3133,6 +3195,7 @@ export type Swap_Filter = {
|
||||
amountUSD_lte?: InputMaybe<Scalars['BigDecimal']>;
|
||||
amountUSD_not?: InputMaybe<Scalars['BigDecimal']>;
|
||||
amountUSD_not_in?: InputMaybe<Array<Scalars['BigDecimal']>>;
|
||||
and?: InputMaybe<Array<InputMaybe<Swap_Filter>>>;
|
||||
id?: InputMaybe<Scalars['ID']>;
|
||||
id_gt?: InputMaybe<Scalars['ID']>;
|
||||
id_gte?: InputMaybe<Scalars['ID']>;
|
||||
@@ -3149,9 +3212,14 @@ export type Swap_Filter = {
|
||||
logIndex_lte?: InputMaybe<Scalars['BigInt']>;
|
||||
logIndex_not?: InputMaybe<Scalars['BigInt']>;
|
||||
logIndex_not_in?: InputMaybe<Array<Scalars['BigInt']>>;
|
||||
or?: InputMaybe<Array<InputMaybe<Swap_Filter>>>;
|
||||
origin?: InputMaybe<Scalars['Bytes']>;
|
||||
origin_contains?: InputMaybe<Scalars['Bytes']>;
|
||||
origin_gt?: InputMaybe<Scalars['Bytes']>;
|
||||
origin_gte?: InputMaybe<Scalars['Bytes']>;
|
||||
origin_in?: InputMaybe<Array<Scalars['Bytes']>>;
|
||||
origin_lt?: InputMaybe<Scalars['Bytes']>;
|
||||
origin_lte?: InputMaybe<Scalars['Bytes']>;
|
||||
origin_not?: InputMaybe<Scalars['Bytes']>;
|
||||
origin_not_contains?: InputMaybe<Scalars['Bytes']>;
|
||||
origin_not_in?: InputMaybe<Array<Scalars['Bytes']>>;
|
||||
@@ -3178,13 +3246,21 @@ export type Swap_Filter = {
|
||||
pool_starts_with_nocase?: InputMaybe<Scalars['String']>;
|
||||
recipient?: InputMaybe<Scalars['Bytes']>;
|
||||
recipient_contains?: InputMaybe<Scalars['Bytes']>;
|
||||
recipient_gt?: InputMaybe<Scalars['Bytes']>;
|
||||
recipient_gte?: InputMaybe<Scalars['Bytes']>;
|
||||
recipient_in?: InputMaybe<Array<Scalars['Bytes']>>;
|
||||
recipient_lt?: InputMaybe<Scalars['Bytes']>;
|
||||
recipient_lte?: InputMaybe<Scalars['Bytes']>;
|
||||
recipient_not?: InputMaybe<Scalars['Bytes']>;
|
||||
recipient_not_contains?: InputMaybe<Scalars['Bytes']>;
|
||||
recipient_not_in?: InputMaybe<Array<Scalars['Bytes']>>;
|
||||
sender?: InputMaybe<Scalars['Bytes']>;
|
||||
sender_contains?: InputMaybe<Scalars['Bytes']>;
|
||||
sender_gt?: InputMaybe<Scalars['Bytes']>;
|
||||
sender_gte?: InputMaybe<Scalars['Bytes']>;
|
||||
sender_in?: InputMaybe<Array<Scalars['Bytes']>>;
|
||||
sender_lt?: InputMaybe<Scalars['Bytes']>;
|
||||
sender_lte?: InputMaybe<Scalars['Bytes']>;
|
||||
sender_not?: InputMaybe<Scalars['Bytes']>;
|
||||
sender_not_contains?: InputMaybe<Scalars['Bytes']>;
|
||||
sender_not_in?: InputMaybe<Array<Scalars['Bytes']>>;
|
||||
@@ -3339,6 +3415,7 @@ export type TickDayData = {
|
||||
export type TickDayData_Filter = {
|
||||
/** Filter for the block changed event. */
|
||||
_change_block?: InputMaybe<BlockChangedFilter>;
|
||||
and?: InputMaybe<Array<InputMaybe<TickDayData_Filter>>>;
|
||||
date?: InputMaybe<Scalars['Int']>;
|
||||
date_gt?: InputMaybe<Scalars['Int']>;
|
||||
date_gte?: InputMaybe<Scalars['Int']>;
|
||||
@@ -3395,6 +3472,7 @@ export type TickDayData_Filter = {
|
||||
liquidityNet_lte?: InputMaybe<Scalars['BigInt']>;
|
||||
liquidityNet_not?: InputMaybe<Scalars['BigInt']>;
|
||||
liquidityNet_not_in?: InputMaybe<Array<Scalars['BigInt']>>;
|
||||
or?: InputMaybe<Array<InputMaybe<TickDayData_Filter>>>;
|
||||
pool?: InputMaybe<Scalars['String']>;
|
||||
pool_?: InputMaybe<Pool_Filter>;
|
||||
pool_contains?: InputMaybe<Scalars['String']>;
|
||||
@@ -3495,6 +3573,7 @@ export type TickHourData = {
|
||||
export type TickHourData_Filter = {
|
||||
/** Filter for the block changed event. */
|
||||
_change_block?: InputMaybe<BlockChangedFilter>;
|
||||
and?: InputMaybe<Array<InputMaybe<TickHourData_Filter>>>;
|
||||
feesUSD?: InputMaybe<Scalars['BigDecimal']>;
|
||||
feesUSD_gt?: InputMaybe<Scalars['BigDecimal']>;
|
||||
feesUSD_gte?: InputMaybe<Scalars['BigDecimal']>;
|
||||
@@ -3527,6 +3606,7 @@ export type TickHourData_Filter = {
|
||||
liquidityNet_lte?: InputMaybe<Scalars['BigInt']>;
|
||||
liquidityNet_not?: InputMaybe<Scalars['BigInt']>;
|
||||
liquidityNet_not_in?: InputMaybe<Array<Scalars['BigInt']>>;
|
||||
or?: InputMaybe<Array<InputMaybe<TickHourData_Filter>>>;
|
||||
periodStartUnix?: InputMaybe<Scalars['Int']>;
|
||||
periodStartUnix_gt?: InputMaybe<Scalars['Int']>;
|
||||
periodStartUnix_gte?: InputMaybe<Scalars['Int']>;
|
||||
@@ -3619,6 +3699,7 @@ export enum TickHourData_OrderBy {
|
||||
export type Tick_Filter = {
|
||||
/** Filter for the block changed event. */
|
||||
_change_block?: InputMaybe<BlockChangedFilter>;
|
||||
and?: InputMaybe<Array<InputMaybe<Tick_Filter>>>;
|
||||
collectedFeesToken0?: InputMaybe<Scalars['BigDecimal']>;
|
||||
collectedFeesToken0_gt?: InputMaybe<Scalars['BigDecimal']>;
|
||||
collectedFeesToken0_gte?: InputMaybe<Scalars['BigDecimal']>;
|
||||
@@ -3715,6 +3796,7 @@ export type Tick_Filter = {
|
||||
liquidityProviderCount_lte?: InputMaybe<Scalars['BigInt']>;
|
||||
liquidityProviderCount_not?: InputMaybe<Scalars['BigInt']>;
|
||||
liquidityProviderCount_not_in?: InputMaybe<Array<Scalars['BigInt']>>;
|
||||
or?: InputMaybe<Array<InputMaybe<Tick_Filter>>>;
|
||||
pool?: InputMaybe<Scalars['String']>;
|
||||
poolAddress?: InputMaybe<Scalars['String']>;
|
||||
poolAddress_contains?: InputMaybe<Scalars['String']>;
|
||||
@@ -3898,6 +3980,7 @@ export type TokenDayData = {
|
||||
export type TokenDayData_Filter = {
|
||||
/** Filter for the block changed event. */
|
||||
_change_block?: InputMaybe<BlockChangedFilter>;
|
||||
and?: InputMaybe<Array<InputMaybe<TokenDayData_Filter>>>;
|
||||
close?: InputMaybe<Scalars['BigDecimal']>;
|
||||
close_gt?: InputMaybe<Scalars['BigDecimal']>;
|
||||
close_gte?: InputMaybe<Scalars['BigDecimal']>;
|
||||
@@ -3954,6 +4037,7 @@ export type TokenDayData_Filter = {
|
||||
open_lte?: InputMaybe<Scalars['BigDecimal']>;
|
||||
open_not?: InputMaybe<Scalars['BigDecimal']>;
|
||||
open_not_in?: InputMaybe<Array<Scalars['BigDecimal']>>;
|
||||
or?: InputMaybe<Array<InputMaybe<TokenDayData_Filter>>>;
|
||||
priceUSD?: InputMaybe<Scalars['BigDecimal']>;
|
||||
priceUSD_gt?: InputMaybe<Scalars['BigDecimal']>;
|
||||
priceUSD_gte?: InputMaybe<Scalars['BigDecimal']>;
|
||||
@@ -4063,6 +4147,7 @@ export type TokenHourData = {
|
||||
export type TokenHourData_Filter = {
|
||||
/** Filter for the block changed event. */
|
||||
_change_block?: InputMaybe<BlockChangedFilter>;
|
||||
and?: InputMaybe<Array<InputMaybe<TokenHourData_Filter>>>;
|
||||
close?: InputMaybe<Scalars['BigDecimal']>;
|
||||
close_gt?: InputMaybe<Scalars['BigDecimal']>;
|
||||
close_gte?: InputMaybe<Scalars['BigDecimal']>;
|
||||
@@ -4111,6 +4196,7 @@ export type TokenHourData_Filter = {
|
||||
open_lte?: InputMaybe<Scalars['BigDecimal']>;
|
||||
open_not?: InputMaybe<Scalars['BigDecimal']>;
|
||||
open_not_in?: InputMaybe<Array<Scalars['BigDecimal']>>;
|
||||
or?: InputMaybe<Array<InputMaybe<TokenHourData_Filter>>>;
|
||||
periodStartUnix?: InputMaybe<Scalars['Int']>;
|
||||
periodStartUnix_gt?: InputMaybe<Scalars['Int']>;
|
||||
periodStartUnix_gte?: InputMaybe<Scalars['Int']>;
|
||||
@@ -4210,6 +4296,7 @@ export enum TokenHourData_OrderBy {
|
||||
export type Token_Filter = {
|
||||
/** Filter for the block changed event. */
|
||||
_change_block?: InputMaybe<BlockChangedFilter>;
|
||||
and?: InputMaybe<Array<InputMaybe<Token_Filter>>>;
|
||||
decimals?: InputMaybe<Scalars['BigInt']>;
|
||||
decimals_gt?: InputMaybe<Scalars['BigInt']>;
|
||||
decimals_gte?: InputMaybe<Scalars['BigInt']>;
|
||||
@@ -4262,6 +4349,7 @@ export type Token_Filter = {
|
||||
name_not_starts_with_nocase?: InputMaybe<Scalars['String']>;
|
||||
name_starts_with?: InputMaybe<Scalars['String']>;
|
||||
name_starts_with_nocase?: InputMaybe<Scalars['String']>;
|
||||
or?: InputMaybe<Array<InputMaybe<Token_Filter>>>;
|
||||
poolCount?: InputMaybe<Scalars['BigInt']>;
|
||||
poolCount_gt?: InputMaybe<Scalars['BigInt']>;
|
||||
poolCount_gte?: InputMaybe<Scalars['BigInt']>;
|
||||
@@ -4446,6 +4534,7 @@ export type TransactionSwapsArgs = {
|
||||
export type Transaction_Filter = {
|
||||
/** Filter for the block changed event. */
|
||||
_change_block?: InputMaybe<BlockChangedFilter>;
|
||||
and?: InputMaybe<Array<InputMaybe<Transaction_Filter>>>;
|
||||
blockNumber?: InputMaybe<Scalars['BigInt']>;
|
||||
blockNumber_gt?: InputMaybe<Scalars['BigInt']>;
|
||||
blockNumber_gte?: InputMaybe<Scalars['BigInt']>;
|
||||
@@ -4482,6 +4571,7 @@ export type Transaction_Filter = {
|
||||
id_not?: InputMaybe<Scalars['ID']>;
|
||||
id_not_in?: InputMaybe<Array<Scalars['ID']>>;
|
||||
mints_?: InputMaybe<Mint_Filter>;
|
||||
or?: InputMaybe<Array<InputMaybe<Transaction_Filter>>>;
|
||||
swaps_?: InputMaybe<Swap_Filter>;
|
||||
timestamp?: InputMaybe<Scalars['BigInt']>;
|
||||
timestamp_gt?: InputMaybe<Scalars['BigInt']>;
|
||||
@@ -4521,6 +4611,7 @@ export type UniswapDayData = {
|
||||
export type UniswapDayData_Filter = {
|
||||
/** Filter for the block changed event. */
|
||||
_change_block?: InputMaybe<BlockChangedFilter>;
|
||||
and?: InputMaybe<Array<InputMaybe<UniswapDayData_Filter>>>;
|
||||
date?: InputMaybe<Scalars['Int']>;
|
||||
date_gt?: InputMaybe<Scalars['Int']>;
|
||||
date_gte?: InputMaybe<Scalars['Int']>;
|
||||
@@ -4545,6 +4636,7 @@ export type UniswapDayData_Filter = {
|
||||
id_lte?: InputMaybe<Scalars['ID']>;
|
||||
id_not?: InputMaybe<Scalars['ID']>;
|
||||
id_not_in?: InputMaybe<Array<Scalars['ID']>>;
|
||||
or?: InputMaybe<Array<InputMaybe<UniswapDayData_Filter>>>;
|
||||
tvlUSD?: InputMaybe<Scalars['BigDecimal']>;
|
||||
tvlUSD_gt?: InputMaybe<Scalars['BigDecimal']>;
|
||||
tvlUSD_gte?: InputMaybe<Scalars['BigDecimal']>;
|
||||
|
||||
@@ -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,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,11 @@
|
||||
import {
|
||||
AllowanceData,
|
||||
AllowanceProvider,
|
||||
AllowanceTransfer,
|
||||
MaxAllowanceTransferAmount,
|
||||
PERMIT2_ADDRESS,
|
||||
PermitSingle,
|
||||
} from '@uniswap/permit2-sdk'
|
||||
import { Token } from '@uniswap/sdk-core'
|
||||
import { signTypedData } from '@uniswap/conedison/provider'
|
||||
import { AllowanceTransfer, MaxAllowanceTransferAmount, PERMIT2_ADDRESS, PermitSingle } from '@uniswap/permit2-sdk'
|
||||
import { CurrencyAmount, Token } from '@uniswap/sdk-core'
|
||||
import { useWeb3React } from '@web3-react/core'
|
||||
import useBlockNumber from 'lib/hooks/useBlockNumber'
|
||||
import PERMIT2_ABI from 'abis/permit2.json'
|
||||
import { Permit2 } from 'abis/types'
|
||||
import { useContract } from 'hooks/useContract'
|
||||
import { useSingleCallResult } from 'lib/hooks/multicall'
|
||||
import ms from 'ms.macro'
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
|
||||
@@ -19,35 +16,28 @@ function toDeadline(expiration: number): number {
|
||||
return Math.floor((Date.now() + expiration) / 1000)
|
||||
}
|
||||
|
||||
export function usePermitAllowance(token?: Token, spender?: string) {
|
||||
const { account, provider } = useWeb3React()
|
||||
const allowanceProvider = useMemo(() => provider && new AllowanceProvider(provider, PERMIT2_ADDRESS), [provider])
|
||||
const [allowanceData, setAllowanceData] = useState<AllowanceData>()
|
||||
export function usePermitAllowance(token?: Token, owner?: string, spender?: string) {
|
||||
const contract = useContract<Permit2>(PERMIT2_ADDRESS, PERMIT2_ABI)
|
||||
const inputs = useMemo(() => [owner, token?.address, spender], [owner, spender, token?.address])
|
||||
|
||||
// If there is no allowanceData, recheck every block so a submitted allowance is immediately observed.
|
||||
const blockNumber = useBlockNumber()
|
||||
const shouldUpdate = allowanceData ? false : blockNumber
|
||||
// If there is no allowance yet, re-check next observed block.
|
||||
// This guarantees that the permitAllowance is synced upon submission and updated upon being synced.
|
||||
const [blocksPerFetch, setBlocksPerFetch] = useState<1>()
|
||||
const result = useSingleCallResult(contract, 'allowance', inputs, {
|
||||
blocksPerFetch,
|
||||
}).result as Awaited<ReturnType<Permit2['allowance']>> | undefined
|
||||
|
||||
useEffect(() => {
|
||||
if (!account || !token || !spender) return
|
||||
const rawAmount = result?.amount.toString() // convert to a string before using in a hook, to avoid spurious rerenders
|
||||
const allowance = useMemo(
|
||||
() => (token && rawAmount ? CurrencyAmount.fromRawAmount(token, rawAmount) : undefined),
|
||||
[token, rawAmount]
|
||||
)
|
||||
useEffect(() => setBlocksPerFetch(allowance?.equalTo(0) ? 1 : undefined), [allowance])
|
||||
|
||||
allowanceProvider
|
||||
?.getAllowanceData(token.address, account, spender)
|
||||
.then((data) => {
|
||||
if (stale) return
|
||||
setAllowanceData(data)
|
||||
})
|
||||
.catch((e) => {
|
||||
console.warn(`Failed to fetch allowance data: ${e}`)
|
||||
})
|
||||
|
||||
let stale = false
|
||||
return () => {
|
||||
stale = true
|
||||
}
|
||||
}, [account, allowanceProvider, shouldUpdate, spender, token])
|
||||
|
||||
return allowanceData
|
||||
return useMemo(
|
||||
() => ({ permitAllowance: allowance, expiration: result?.expiration, nonce: result?.nonce }),
|
||||
[allowance, result?.expiration, result?.nonce]
|
||||
)
|
||||
}
|
||||
|
||||
interface Permit extends PermitSingle {
|
||||
@@ -86,12 +76,13 @@ export function useUpdatePermitAllowance(
|
||||
}
|
||||
|
||||
const { domain, types, values } = AllowanceTransfer.getPermitData(permit, PERMIT2_ADDRESS, chainId)
|
||||
const signature = await provider.getSigner(account)._signTypedData(domain, types, values)
|
||||
// Use conedison's signTypedData for better x-wallet compatibility.
|
||||
const signature = await signTypedData(provider.getSigner(account), domain, types, values)
|
||||
onPermitSignature?.({ ...permit, signature })
|
||||
return
|
||||
} catch (e: unknown) {
|
||||
const symbol = token?.symbol ?? 'Token'
|
||||
throw new Error(`${symbol} permit failed: ${e instanceof Error ? e.message : e}`)
|
||||
throw new Error(`${symbol} permit allowance failed: ${e instanceof Error ? e.message : e}`)
|
||||
}
|
||||
}, [account, chainId, nonce, onPermitSignature, provider, spender, token])
|
||||
}
|
||||
|
||||
@@ -48,7 +48,12 @@ export default function useStablecoinPrice(currency?: Currency): Price<Currency,
|
||||
}, [currency, stablecoin, trade])
|
||||
|
||||
const lastPrice = useRef(price)
|
||||
if (!price || !lastPrice.current || !price.equalTo(lastPrice.current)) {
|
||||
if (
|
||||
!price ||
|
||||
!lastPrice.current ||
|
||||
!price.equalTo(lastPrice.current) ||
|
||||
!price.baseCurrency.equals(lastPrice.current.baseCurrency)
|
||||
) {
|
||||
lastPrice.current = price
|
||||
}
|
||||
return lastPrice.current
|
||||
|
||||
@@ -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
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user