Compare commits
131 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6efe8f3260 | ||
|
|
9ac28a4571 | ||
|
|
bde1421ffb | ||
|
|
3dceb45d9e | ||
|
|
7b589561bc | ||
|
|
c9d3dc36b8 | ||
|
|
ef4d8fc269 | ||
|
|
ff9cc5cb69 | ||
|
|
719fd524ed | ||
|
|
f15dd1e61e | ||
|
|
8c372c6142 | ||
|
|
8c0998bd59 | ||
|
|
ad3a4ea808 | ||
|
|
d30c5173f5 | ||
|
|
689100afa2 | ||
|
|
d546ffec1c | ||
|
|
4a72d8835c | ||
|
|
19c6023601 | ||
|
|
a4a954c8af | ||
|
|
82dcdcec55 | ||
|
|
27e20d7230 | ||
|
|
95eafbab7d | ||
|
|
639fe2f73b | ||
|
|
72cd27f045 | ||
|
|
4e64c0e88f | ||
|
|
fda28d9be3 | ||
|
|
bc92af6c15 | ||
|
|
9a257e0ca8 | ||
|
|
82646b77dd | ||
|
|
1992c5de06 | ||
|
|
0208ccd7d2 | ||
|
|
12df4b3981 | ||
|
|
3eaeb65b07 | ||
|
|
6df2f3677e | ||
|
|
80edf5a0d6 | ||
|
|
96f6929127 | ||
|
|
4ec95d0927 | ||
|
|
fba6cc9e02 | ||
|
|
bc2f68565b | ||
|
|
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 |
1
.env
1
.env
@@ -10,3 +10,4 @@ REACT_APP_INFURA_KEY="4bf032f2d38a4ed6bb975b80d6340847"
|
||||
REACT_APP_MOONPAY_API="https://api.moonpay.com"
|
||||
REACT_APP_MOONPAY_LINK="https://us-central1-uniswap-mobile.cloudfunctions.net/signMoonpayLinkStaging?platform=web"
|
||||
REACT_APP_MOONPAY_PUBLISHABLE_KEY="pk_test_DycfESRid31UaSxhI5yWKe1r5E5kKSz"
|
||||
REACT_APP_STATSIG_API_KEY="client-1rY92WZGidd2hgW4x1lsZ7afqm1Qfr3sJfH3A5b8eJa"
|
||||
|
||||
@@ -9,3 +9,4 @@ REACT_APP_MOONPAY_PUBLISHABLE_KEY="pk_live_uQG4BJC4w3cxnqpcSqAfohdBFDTsY6E"
|
||||
REACT_APP_FIREBASE_KEY="AIzaSyBcZWwTcTJHj_R6ipZcrJkXdq05PuX0Rs0"
|
||||
THE_GRAPH_SCHEMA_ENDPOINT="https://api.thegraph.com/subgraphs/name/uniswap/uniswap-v3"
|
||||
REACT_APP_SENTRY_ENABLED=false
|
||||
REACT_APP_STATSIG_API_KEY="client-1rY92WZGidd2hgW4x1lsZ7afqm1Qfr3sJfH3A5b8eJa"
|
||||
|
||||
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
|
||||
|
||||
|
||||
@@ -12,9 +12,10 @@ module.exports = {
|
||||
jest: {
|
||||
configure(jestConfig) {
|
||||
return Object.assign({}, jestConfig, {
|
||||
transformIgnorePatterns: ['@uniswap/conedison/format'],
|
||||
transformIgnorePatterns: ['@uniswap/conedison/format', '@uniswap/conedison/provider'],
|
||||
moduleNameMapper: {
|
||||
'@uniswap/conedison/format': '@uniswap/conedison/dist/format',
|
||||
'@uniswap/conedison/provider': '@uniswap/conedison/dist/provider',
|
||||
},
|
||||
})
|
||||
},
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import { getTestSelector } from '../utils'
|
||||
|
||||
const COLLECTION_ADDRESS = '0xbd3531da5cf5857e7cfaa92426877b022e612cf8'
|
||||
const PUDGY_COLLECTION_ADDRESS = '0xbd3531da5cf5857e7cfaa92426877b022e612cf8'
|
||||
const BONSAI_COLLECTION_ADDRESS = '0xec9c519d49856fd2f8133a0741b4dbe002ce211b'
|
||||
|
||||
describe('Testing nfts', () => {
|
||||
beforeEach(() => {
|
||||
cy.visit('/')
|
||||
cy.get(getTestSelector('FiatOnrampAnnouncement-close')).first().click()
|
||||
cy.visit('/').then(() => {
|
||||
cy.get(getTestSelector('FiatOnrampAnnouncement-close')).first().click()
|
||||
})
|
||||
})
|
||||
|
||||
it('should load nft leaderboard', () => {
|
||||
@@ -16,7 +18,7 @@ describe('Testing nfts', () => {
|
||||
})
|
||||
|
||||
it('should load pudgy penguin collection page', () => {
|
||||
cy.visit(`/#/nfts/collection/${COLLECTION_ADDRESS}`)
|
||||
cy.visit(`/#/nfts/collection/${PUDGY_COLLECTION_ADDRESS}`)
|
||||
cy.get(getTestSelector('nft-collection-asset')).should('exist')
|
||||
cy.get(getTestSelector('nft-collection-filter-buy-now')).should('not.exist')
|
||||
cy.get(getTestSelector('nft-filter')).first().click()
|
||||
@@ -24,13 +26,13 @@ describe('Testing nfts', () => {
|
||||
})
|
||||
|
||||
it('should be able to navigate to activity', () => {
|
||||
cy.visit(`/#/nfts/collection/${COLLECTION_ADDRESS}`)
|
||||
cy.visit(`/#/nfts/collection/${PUDGY_COLLECTION_ADDRESS}`)
|
||||
cy.get(getTestSelector('nft-activity')).first().click()
|
||||
cy.get(getTestSelector('nft-activity-row')).should('exist')
|
||||
})
|
||||
|
||||
it('should go to the details page', () => {
|
||||
cy.visit(`/#/nfts/collection/${COLLECTION_ADDRESS}`)
|
||||
cy.visit(`/#/nfts/collection/${PUDGY_COLLECTION_ADDRESS}`)
|
||||
cy.get(getTestSelector('nft-filter')).first().click()
|
||||
cy.get(getTestSelector('nft-collection-filter-buy-now')).click()
|
||||
cy.get(getTestSelector('nft-details-link')).first().click()
|
||||
@@ -41,7 +43,7 @@ describe('Testing nfts', () => {
|
||||
})
|
||||
|
||||
it('should toggle buy now on details page', () => {
|
||||
cy.visit(`#/nfts/asset/${COLLECTION_ADDRESS}/8565`)
|
||||
cy.visit(`#/nfts/asset/${BONSAI_COLLECTION_ADDRESS}/7580`)
|
||||
cy.get(getTestSelector('nft-details-description-text')).should('exist')
|
||||
cy.get(getTestSelector('nft-details-description')).click()
|
||||
cy.get(getTestSelector('nft-details-description-text')).should('not.exist')
|
||||
|
||||
@@ -1,11 +1,25 @@
|
||||
import { getTestSelector } from '../utils'
|
||||
|
||||
describe('Pool', () => {
|
||||
beforeEach(() => cy.visit('/pool'))
|
||||
beforeEach(() => {
|
||||
cy.visit('/pool').then(() => {
|
||||
cy.wait('@eth_blockNumber')
|
||||
})
|
||||
})
|
||||
|
||||
it('add liquidity links to /add/ETH', () => {
|
||||
cy.get(getTestSelector('FiatOnrampAnnouncement-close')).first().click()
|
||||
cy.get('#join-pool-button').click()
|
||||
cy.url().should('contain', '/add/ETH')
|
||||
cy.get('body')
|
||||
.then((body) => {
|
||||
if (body.find(getTestSelector('FiatOnrampAnnouncement-close')).length > 0) {
|
||||
cy.get(getTestSelector('FiatOnrampAnnouncement-close')).click()
|
||||
}
|
||||
})
|
||||
.then(() => {
|
||||
cy.get('#join-pool-button')
|
||||
.click()
|
||||
.then(() => {
|
||||
cy.url().should('contain', '/add/ETH')
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
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')
|
||||
})
|
||||
})
|
||||
64
cypress/e2e/universal-search.test.ts
Normal file
64
cypress/e2e/universal-search.test.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
import { getTestSelector } from '../utils'
|
||||
|
||||
describe('Universal search bar', () => {
|
||||
before(() => {
|
||||
cy.visit('/')
|
||||
cy.get('[data-cy="magnifying-icon"]')
|
||||
.parent()
|
||||
.then(($navIcon) => {
|
||||
$navIcon.click()
|
||||
})
|
||||
})
|
||||
|
||||
it('should yield clickable result for regular token or nft collection search term', () => {
|
||||
// Search for uni token by name.
|
||||
cy.get('[data-cy="search-bar-input"]').last().clear().type('uni')
|
||||
cy.get('[data-cy="searchbar-token-row-UNI"]')
|
||||
.should('contain.text', 'Uniswap')
|
||||
.and('contain.text', 'UNI')
|
||||
.and('contain.text', '$')
|
||||
.and('contain.text', '%')
|
||||
cy.get('[data-cy="searchbar-token-row-UNI"]').click()
|
||||
|
||||
cy.get('div').contains('Uniswap').should('exist')
|
||||
// Stats should have: TVL, 24H Volume, 52W low, 52W high.
|
||||
cy.get(getTestSelector('token-details-stats')).should('exist')
|
||||
cy.get(getTestSelector('token-details-stats')).within(() => {
|
||||
cy.get('[data-cy="tvl"]').should('include.text', '$')
|
||||
cy.get('[data-cy="volume-24h"]').should('include.text', '$')
|
||||
cy.get('[data-cy="52w-low"]').should('include.text', '$')
|
||||
cy.get('[data-cy="52w-high"]').should('include.text', '$')
|
||||
})
|
||||
|
||||
// About section should have description of token.
|
||||
cy.get(getTestSelector('token-details-about-section')).should('exist')
|
||||
cy.contains('UNI is the governance token for Uniswap').should('exist')
|
||||
})
|
||||
|
||||
it('should show recent tokens and popular tokens with empty search term', () => {
|
||||
cy.get('[data-cy="magnifying-icon"]')
|
||||
.parent()
|
||||
.then(($navIcon) => {
|
||||
$navIcon.click()
|
||||
})
|
||||
// Recently searched UNI token should exist.
|
||||
cy.get('[data-cy="search-bar-input"]').last().clear()
|
||||
cy.get('[data-cy="searchbar-dropdown"]')
|
||||
.contains('[data-cy="searchbar-dropdown"]', 'Recent searches')
|
||||
.find('[data-cy="searchbar-token-row-UNI"]')
|
||||
.should('exist')
|
||||
|
||||
// Most popular 3 tokens should be shown.
|
||||
cy.get('[data-cy="searchbar-dropdown"]')
|
||||
.contains('[data-cy="searchbar-dropdown"]', 'Popular tokens')
|
||||
.find('[data-cy^="searchbar-token-row"]')
|
||||
.its('length')
|
||||
.should('be.eq', 3)
|
||||
})
|
||||
|
||||
it.skip('should show blocked badge when blocked token is searched for', () => {
|
||||
// Search for mTSLA, which is a blocked token.
|
||||
cy.get('[data-cy="search-bar-input"]').last().clear().type('mtsla')
|
||||
cy.get('[data-cy="searchbar-token-row-mTSLA"]').find('[data-cy="blocked-icon"]').should('exist')
|
||||
})
|
||||
})
|
||||
@@ -13,8 +13,8 @@ describe('Wallet Dropdown', () => {
|
||||
|
||||
it('should select a language', () => {
|
||||
cy.get(getTestSelector('wallet-select-language')).click()
|
||||
cy.get(getTestSelector('wallet-language-item')).contains('Afrikaans').click({ force: true })
|
||||
cy.get(getTestSelector('wallet-header')).should('contain', 'Taal')
|
||||
cy.get(getTestSelector('wallet-language-item')).contains('Deutsch').click({ force: true })
|
||||
cy.get(getTestSelector('wallet-header')).should('contain', 'Sprache')
|
||||
cy.get(getTestSelector('wallet-language-item')).contains('English').click({ force: true })
|
||||
cy.get(getTestSelector('wallet-header')).should('contain', 'Language')
|
||||
cy.get(getTestSelector('wallet-back')).click()
|
||||
@@ -36,8 +36,8 @@ describe('Wallet Dropdown', () => {
|
||||
|
||||
it('should select a language when not connected', () => {
|
||||
cy.get(getTestSelector('wallet-select-language')).click()
|
||||
cy.get(getTestSelector('wallet-language-item')).contains('Afrikaans').click({ force: true })
|
||||
cy.get(getTestSelector('wallet-header')).should('contain', 'Taal')
|
||||
cy.get(getTestSelector('wallet-language-item')).contains('Deutsch').click({ force: true })
|
||||
cy.get(getTestSelector('wallet-header')).should('contain', 'Sprache')
|
||||
cy.get(getTestSelector('wallet-language-item')).contains('English').click({ force: true })
|
||||
cy.get(getTestSelector('wallet-header')).should('contain', 'Language')
|
||||
cy.get(getTestSelector('wallet-back')).click()
|
||||
|
||||
@@ -70,6 +70,7 @@ beforeEach(() => {
|
||||
// These are stripped by cypress because chromeWebSecurity === false; this adds them back in.
|
||||
cy.intercept(/infura.io/, (res) => {
|
||||
res.headers['origin'] = 'http://localhost:3000'
|
||||
res.alias = res.body.method
|
||||
res.continue()
|
||||
})
|
||||
|
||||
|
||||
20
package.json
20
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",
|
||||
@@ -132,9 +133,9 @@
|
||||
"@reduxjs/toolkit": "^1.6.1",
|
||||
"@sentry/react": "^7.29.0",
|
||||
"@types/react-window-infinite-loader": "^1.0.6",
|
||||
"@uniswap/analytics": "1.2.0",
|
||||
"@uniswap/analytics-events": "^2.1.0",
|
||||
"@uniswap/conedison": "^1.1.1",
|
||||
"@uniswap/analytics": "^1.3.0",
|
||||
"@uniswap/analytics-events": "^2.3.0",
|
||||
"@uniswap/conedison": "^1.3.0",
|
||||
"@uniswap/governance": "^1.0.2",
|
||||
"@uniswap/liquidity-staker": "^1.0.2",
|
||||
"@uniswap/merkle-distributor": "1.0.1",
|
||||
@@ -144,14 +145,14 @@
|
||||
"@uniswap/sdk-core": "^3.0.1",
|
||||
"@uniswap/smart-order-router": "^2.10.0",
|
||||
"@uniswap/token-lists": "^1.0.0-beta.30",
|
||||
"@uniswap/universal-router-sdk": "1.3.0",
|
||||
"@uniswap/universal-router-sdk": "^1.3.6",
|
||||
"@uniswap/v2-core": "1.0.0",
|
||||
"@uniswap/v2-periphery": "^1.1.0-beta.0",
|
||||
"@uniswap/v2-sdk": "^3.0.1",
|
||||
"@uniswap/v3-core": "1.0.0",
|
||||
"@uniswap/v3-periphery": "^1.1.1",
|
||||
"@uniswap/v3-sdk": "^3.9.0",
|
||||
"@uniswap/widgets": "2.22.11",
|
||||
"@uniswap/widgets": "^2.29.3",
|
||||
"@vanilla-extract/css": "^1.7.2",
|
||||
"@vanilla-extract/css-utils": "^0.1.2",
|
||||
"@vanilla-extract/dynamic": "^2.0.2",
|
||||
@@ -164,16 +165,16 @@
|
||||
"@visx/responsive": "^2.10.0",
|
||||
"@visx/shape": "^2.11.1",
|
||||
"@walletconnect/ethereum-provider": "^1.8.0",
|
||||
"@web3-react/coinbase-wallet": "8.0.34-beta.0",
|
||||
"@web3-react/coinbase-wallet": "8.0.35-beta.0",
|
||||
"@web3-react/core": "8.0.35-beta.0",
|
||||
"@web3-react/eip1193": "8.0.26-beta.0",
|
||||
"@web3-react/eip1193": "8.0.27-beta.0",
|
||||
"@web3-react/empty": "8.0.20-beta.0",
|
||||
"@web3-react/gnosis-safe": "8.0.7-beta.0",
|
||||
"@web3-react/metamask": "8.0.28-beta.0",
|
||||
"@web3-react/metamask": "8.0.30-beta.0",
|
||||
"@web3-react/network": "8.0.27-beta.0",
|
||||
"@web3-react/types": "8.0.20-beta.0",
|
||||
"@web3-react/url": "8.0.25-beta.0",
|
||||
"@web3-react/walletconnect": "8.0.36-beta.0",
|
||||
"@web3-react/walletconnect": "8.0.37-beta.0",
|
||||
"array.prototype.flat": "^1.2.4",
|
||||
"array.prototype.flatmap": "^1.2.4",
|
||||
"cids": "^1.0.0",
|
||||
@@ -221,6 +222,7 @@
|
||||
"redux": "^4.1.2",
|
||||
"redux-localstorage-simple": "^2.3.1",
|
||||
"setimmediate": "^1.0.5",
|
||||
"statsig-react": "^1.22.0",
|
||||
"styled-components": "^5.3.5",
|
||||
"tiny-invariant": "^1.2.0",
|
||||
"ua-parser-js": "^0.7.28",
|
||||
|
||||
41
src/abis/permit2.json
Normal file
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,11 +1,6 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import { useWeb3React } from '@web3-react/core'
|
||||
import {
|
||||
getConnection,
|
||||
getConnectionName,
|
||||
getHasCoinbaseExtensionInstalled,
|
||||
getHasMetaMaskExtensionInstalled,
|
||||
} from 'connection/utils'
|
||||
import { getConnection, getConnectionName, getIsCoinbaseWallet, getIsMetaMaskWallet } from 'connection/utils'
|
||||
import { useCallback } from 'react'
|
||||
import { ExternalLink as LinkIcon } from 'react-feather'
|
||||
import { useAppDispatch } from 'state/hooks'
|
||||
@@ -215,8 +210,8 @@ export default function AccountDetails({
|
||||
const theme = useTheme()
|
||||
const dispatch = useAppDispatch()
|
||||
|
||||
const hasMetaMaskExtension = getHasMetaMaskExtensionInstalled()
|
||||
const hasCoinbaseExtension = getHasCoinbaseExtensionInstalled()
|
||||
const hasMetaMaskExtension = getIsMetaMaskWallet()
|
||||
const hasCoinbaseExtension = getIsCoinbaseWallet()
|
||||
const isInjectedMobileBrowser = (hasMetaMaskExtension || hasCoinbaseExtension) && isMobile
|
||||
|
||||
function formatConnectorName() {
|
||||
|
||||
@@ -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,7 +1,9 @@
|
||||
import { BaseVariant, FeatureFlag, featureFlagSettings, useUpdateFlag } from 'featureFlags'
|
||||
import { useFiatOnrampFlag } from 'featureFlags/flags/fiatOnramp'
|
||||
import { GqlRoutingVariant, useGqlRoutingFlag } from 'featureFlags/flags/gqlRouting'
|
||||
import { NftListV2Variant, useNftListV2Flag } from 'featureFlags/flags/nftListV2'
|
||||
import { PayWithAnyTokenVariant, usePayWithAnyTokenFlag } from 'featureFlags/flags/payWithAnyToken'
|
||||
import { Permit2Variant, usePermit2Flag } from 'featureFlags/flags/permit2'
|
||||
import { SwapWidgetVariant, useSwapWidgetFlag } from 'featureFlags/flags/swapWidget'
|
||||
import { TraceJsonRpcVariant, useTraceJsonRpcFlag } from 'featureFlags/flags/traceJsonRpc'
|
||||
import { useAtomValue, useUpdateAtom } from 'jotai/utils'
|
||||
import { Children, PropsWithChildren, ReactElement, ReactNode, useCallback, useState } from 'react'
|
||||
@@ -210,18 +212,30 @@ export default function FeatureFlagModal() {
|
||||
featureFlag={FeatureFlag.permit2}
|
||||
label="Permit 2 / Universal Router"
|
||||
/>
|
||||
<FeatureFlagOption
|
||||
variant={BaseVariant}
|
||||
value={useFiatOnrampFlag()}
|
||||
featureFlag={FeatureFlag.fiatOnramp}
|
||||
label="Fiat on-ramp"
|
||||
/>
|
||||
<FeatureFlagOption
|
||||
variant={NftListV2Variant}
|
||||
value={useNftListV2Flag()}
|
||||
featureFlag={FeatureFlag.nftListV2}
|
||||
label="NFT Listing Page v2"
|
||||
/>
|
||||
<FeatureFlagOption
|
||||
variant={PayWithAnyTokenVariant}
|
||||
value={usePayWithAnyTokenFlag()}
|
||||
featureFlag={FeatureFlag.payWithAnyToken}
|
||||
label="Pay With Any Token"
|
||||
/>
|
||||
<FeatureFlagOption
|
||||
variant={SwapWidgetVariant}
|
||||
value={useSwapWidgetFlag()}
|
||||
featureFlag={FeatureFlag.swapWidget}
|
||||
label="Swap Widget"
|
||||
/>
|
||||
<FeatureFlagOption
|
||||
variant={GqlRoutingVariant}
|
||||
value={useGqlRoutingFlag()}
|
||||
featureFlag={FeatureFlag.gqlRouting}
|
||||
label="GraphQL NFT Routing"
|
||||
/>
|
||||
<FeatureFlagGroup name="Debug">
|
||||
<FeatureFlagOption
|
||||
variant={TraceJsonRpcVariant}
|
||||
|
||||
@@ -3,9 +3,7 @@ import { sendAnalyticsEvent } from '@uniswap/analytics'
|
||||
import { InterfaceEventName } from '@uniswap/analytics-events'
|
||||
import { useWeb3React } from '@web3-react/core'
|
||||
import fiatMaskUrl from 'assets/svg/fiat_mask.svg'
|
||||
import { BaseVariant } from 'featureFlags'
|
||||
import { useFiatOnrampFlag } from 'featureFlags/flags/fiatOnramp'
|
||||
import { useCallback, useEffect } from 'react'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { X } from 'react-feather'
|
||||
import { useToggleWalletDropdown } from 'state/application/hooks'
|
||||
import { useAppSelector } from 'state/hooks'
|
||||
@@ -100,6 +98,7 @@ const MAX_RENDER_COUNT = 3
|
||||
export function FiatOnrampAnnouncement() {
|
||||
const { account } = useWeb3React()
|
||||
const [acks, acknowledge] = useFiatOnrampAck()
|
||||
const [localClose, setLocalClose] = useState(false)
|
||||
useEffect(() => {
|
||||
if (!sessionStorage.getItem(ANNOUNCEMENT_RENDERED)) {
|
||||
acknowledge({ renderCount: acks?.renderCount + 1 })
|
||||
@@ -108,6 +107,7 @@ export function FiatOnrampAnnouncement() {
|
||||
}, [acknowledge, acks])
|
||||
|
||||
const handleClose = useCallback(() => {
|
||||
setLocalClose(true)
|
||||
localStorage.setItem(ANNOUNCEMENT_DISMISSED, 'true')
|
||||
}, [])
|
||||
|
||||
@@ -118,17 +118,16 @@ export function FiatOnrampAnnouncement() {
|
||||
acknowledge({ user: true })
|
||||
}, [acknowledge, toggleWalletDropdown])
|
||||
|
||||
const fiatOnrampFlag = useFiatOnrampFlag()
|
||||
const openModal = useAppSelector((state) => state.application.openModal)
|
||||
|
||||
if (
|
||||
!account ||
|
||||
acks?.user ||
|
||||
fiatOnrampFlag === BaseVariant.Control ||
|
||||
localStorage.getItem(ANNOUNCEMENT_DISMISSED) ||
|
||||
acks?.renderCount >= MAX_RENDER_COUNT ||
|
||||
isMobile ||
|
||||
openModal !== null
|
||||
openModal !== null ||
|
||||
localClose
|
||||
) {
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { useWeb3React } from '@web3-react/core'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { useCloseModal, useModalIsOpen } from 'state/application/hooks'
|
||||
import { ApplicationModal } from 'state/application/reducer'
|
||||
import { useIsDarkMode } from 'state/user/hooks'
|
||||
import styled, { useTheme } from 'styled-components/macro'
|
||||
import { CustomLightSpinner, ThemedText } from 'theme'
|
||||
|
||||
@@ -67,6 +68,7 @@ const MOONPAY_SUPPORTED_CURRENCY_CODES = [
|
||||
export default function FiatOnrampModal() {
|
||||
const { account } = useWeb3React()
|
||||
const theme = useTheme()
|
||||
const isDarkMode = useIsDarkMode()
|
||||
const closeModal = useCloseModal()
|
||||
const fiatOnrampModalOpen = useModalIsOpen(ApplicationModal.FIAT_ONRAMP)
|
||||
|
||||
@@ -90,6 +92,7 @@ export default function FiatOnrampModal() {
|
||||
},
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
theme: isDarkMode ? 'dark' : 'light',
|
||||
colorCode: theme.accentAction,
|
||||
defaultCurrencyCode: 'eth',
|
||||
redirectUrl: 'https://app.uniswap.org/#/swap',
|
||||
@@ -112,7 +115,7 @@ export default function FiatOnrampModal() {
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}, [account, theme.accentAction])
|
||||
}, [account, isDarkMode, theme.accentAction])
|
||||
|
||||
useEffect(() => {
|
||||
fetchSignedIframeUrl()
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
)
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { NATIVE_CHAIN_ID } from 'constants/tokens'
|
||||
import { TokenStandard } from 'graphql/data/__generated__/types-and-hooks'
|
||||
import { SearchToken } from 'graphql/data/SearchTokens'
|
||||
import { TokenQueryData } from 'graphql/data/Token'
|
||||
import { TopToken } from 'graphql/data/TopTokens'
|
||||
import { CHAIN_NAME_TO_CHAIN_ID } from 'graphql/data/util'
|
||||
@@ -7,14 +9,14 @@ import AssetLogo, { AssetLogoBaseProps } from './AssetLogo'
|
||||
|
||||
export default function QueryTokenLogo(
|
||||
props: AssetLogoBaseProps & {
|
||||
token?: TopToken | TokenQueryData
|
||||
token?: TopToken | TokenQueryData | SearchToken
|
||||
}
|
||||
) {
|
||||
const chainId = props.token?.chain ? CHAIN_NAME_TO_CHAIN_ID[props.token?.chain] : undefined
|
||||
|
||||
return (
|
||||
<AssetLogo
|
||||
isNative={props.token?.address === NATIVE_CHAIN_ID}
|
||||
isNative={props.token?.standard === TokenStandard.Native || props.token?.address === NATIVE_CHAIN_ID}
|
||||
chainId={chainId}
|
||||
address={props.token?.address}
|
||||
symbol={props.token?.symbol}
|
||||
|
||||
@@ -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()
|
||||
|
||||
102
src/components/NavBar/RecentlySearchedAssets.ts
Normal file
102
src/components/NavBar/RecentlySearchedAssets.ts
Normal file
@@ -0,0 +1,102 @@
|
||||
import { SupportedChainId } from 'constants/chains'
|
||||
import { NATIVE_CHAIN_ID, nativeOnChain } from 'constants/tokens'
|
||||
import { Chain, NftCollection, useRecentlySearchedAssetsQuery } from 'graphql/data/__generated__/types-and-hooks'
|
||||
import { SearchToken } from 'graphql/data/SearchTokens'
|
||||
import { CHAIN_NAME_TO_CHAIN_ID } from 'graphql/data/util'
|
||||
import { useAtom } from 'jotai'
|
||||
import { atomWithStorage, useAtomValue } from 'jotai/utils'
|
||||
import { GenieCollection } from 'nft/types'
|
||||
import { useCallback, useMemo } from 'react'
|
||||
import { getNativeTokenDBAddress } from 'utils/nativeTokens'
|
||||
|
||||
type RecentlySearchedAsset = {
|
||||
isNft?: boolean
|
||||
address: string
|
||||
chain: Chain
|
||||
}
|
||||
|
||||
// Temporary measure used until backend supports addressing by "NATIVE"
|
||||
const NATIVE_QUERY_ADDRESS_INPUT = null as unknown as string
|
||||
function getQueryAddress(chain: Chain) {
|
||||
return getNativeTokenDBAddress(chain) ?? NATIVE_QUERY_ADDRESS_INPUT
|
||||
}
|
||||
|
||||
const recentlySearchedAssetsAtom = atomWithStorage<RecentlySearchedAsset[]>('recentlySearchedAssets', [])
|
||||
|
||||
export function useAddRecentlySearchedAsset() {
|
||||
const [searchHistory, updateSearchHistory] = useAtom(recentlySearchedAssetsAtom)
|
||||
|
||||
return useCallback(
|
||||
(asset: RecentlySearchedAsset) => {
|
||||
// Removes the new asset if it was already in the array
|
||||
const newHistory = searchHistory.filter(
|
||||
(oldAsset) => !(oldAsset.address === asset.address && oldAsset.chain === asset.chain)
|
||||
)
|
||||
newHistory.unshift(asset)
|
||||
updateSearchHistory(newHistory)
|
||||
},
|
||||
[searchHistory, updateSearchHistory]
|
||||
)
|
||||
}
|
||||
|
||||
export function useRecentlySearchedAssets() {
|
||||
const history = useAtomValue(recentlySearchedAssetsAtom)
|
||||
const shortenedHistory = useMemo(() => history.slice(0, 4), [history])
|
||||
|
||||
const { data: queryData, loading } = useRecentlySearchedAssetsQuery({
|
||||
variables: {
|
||||
collectionAddresses: shortenedHistory.filter((asset) => asset.isNft).map((asset) => asset.address),
|
||||
contracts: shortenedHistory
|
||||
.filter((asset) => !asset.isNft)
|
||||
.map((token) => ({
|
||||
address: token.address === NATIVE_CHAIN_ID ? getQueryAddress(token.chain) : token.address,
|
||||
chain: token.chain,
|
||||
})),
|
||||
},
|
||||
})
|
||||
|
||||
const data = useMemo(() => {
|
||||
if (shortenedHistory.length === 0) return []
|
||||
else if (!queryData) return undefined
|
||||
// Collects both tokens and collections in a map, so they can later be returned in original order
|
||||
const resultsMap: { [key: string]: GenieCollection | SearchToken } = {}
|
||||
|
||||
const queryCollections = queryData?.nftCollections?.edges.map((edge) => edge.node as NonNullable<NftCollection>)
|
||||
const collections = queryCollections?.map(
|
||||
(queryCollection): GenieCollection => {
|
||||
return {
|
||||
address: queryCollection.nftContracts?.[0]?.address ?? '',
|
||||
isVerified: queryCollection?.isVerified,
|
||||
name: queryCollection?.name,
|
||||
stats: {
|
||||
floor_price: queryCollection?.markets?.[0]?.floorPrice?.value,
|
||||
total_supply: queryCollection?.numAssets,
|
||||
},
|
||||
imageUrl: queryCollection?.image?.url ?? '',
|
||||
}
|
||||
},
|
||||
[queryCollections]
|
||||
)
|
||||
collections?.forEach((collection) => (resultsMap[collection.address] = collection))
|
||||
queryData.tokens?.filter(Boolean).forEach((token) => {
|
||||
resultsMap[token.address ?? `NATIVE-${token.chain}`] = token
|
||||
})
|
||||
|
||||
const data: (SearchToken | GenieCollection)[] = []
|
||||
shortenedHistory.forEach((asset) => {
|
||||
if (asset.address === 'NATIVE') {
|
||||
// Handles special case where wMATIC data needs to be used for MATIC
|
||||
const native = nativeOnChain(CHAIN_NAME_TO_CHAIN_ID[asset.chain] ?? SupportedChainId.MAINNET)
|
||||
const queryAddress = getQueryAddress(asset.chain)?.toLowerCase() ?? `NATIVE-${asset.chain}`
|
||||
const result = resultsMap[queryAddress]
|
||||
if (result) data.push({ ...result, address: 'NATIVE', ...native })
|
||||
} else {
|
||||
const result = resultsMap[asset.address]
|
||||
if (result) data.push(result)
|
||||
}
|
||||
})
|
||||
return data
|
||||
}, [queryData, shortenedHistory])
|
||||
|
||||
return { data, loading }
|
||||
}
|
||||
@@ -2,7 +2,9 @@
|
||||
import { t, Trans } from '@lingui/macro'
|
||||
import { sendAnalyticsEvent, Trace, TraceEvent, useTrace } from '@uniswap/analytics'
|
||||
import { BrowserEvent, InterfaceElementName, InterfaceEventName, InterfaceSectionName } from '@uniswap/analytics-events'
|
||||
import { useWeb3React } from '@web3-react/core'
|
||||
import clsx from 'clsx'
|
||||
import { useSearchTokens } from 'graphql/data/SearchTokens'
|
||||
import useDebounce from 'hooks/useDebounce'
|
||||
import { useIsNftPage } from 'hooks/useIsNftPage'
|
||||
import { useOnClickOutside } from 'hooks/useOnClickOutside'
|
||||
@@ -12,7 +14,6 @@ import { Row } from 'nft/components/Flex'
|
||||
import { magicalGradientOnHover } from 'nft/css/common.css'
|
||||
import { useIsMobile, useIsTablet } from 'nft/hooks'
|
||||
import { fetchSearchCollections } from 'nft/queries'
|
||||
import { fetchSearchTokens } from 'nft/queries/genie/SearchTokensFetcher'
|
||||
import { ChangeEvent, useCallback, useEffect, useMemo, useReducer, useRef, useState } from 'react'
|
||||
import { useQuery } from 'react-query'
|
||||
import { useLocation } from 'react-router-dom'
|
||||
@@ -64,16 +65,8 @@ export const SearchBar = () => {
|
||||
}
|
||||
)
|
||||
|
||||
const { data: tokens, isLoading: tokensAreLoading } = useQuery(
|
||||
['searchTokens', debouncedSearchValue],
|
||||
() => fetchSearchTokens(debouncedSearchValue),
|
||||
{
|
||||
refetchOnWindowFocus: false,
|
||||
refetchOnMount: false,
|
||||
refetchOnReconnect: false,
|
||||
enabled: !!debouncedSearchValue.length,
|
||||
}
|
||||
)
|
||||
const { chainId } = useWeb3React()
|
||||
const { data: tokens, loading: tokensAreLoading } = useSearchTokens(debouncedSearchValue, chainId ?? 1)
|
||||
|
||||
const isNFTPage = useIsNftPage()
|
||||
|
||||
@@ -148,6 +141,7 @@ export const SearchBar = () => {
|
||||
return (
|
||||
<Trace section={InterfaceSectionName.NAVBAR_SEARCH}>
|
||||
<Box
|
||||
data-cy="search-bar"
|
||||
position={{ sm: 'fixed', md: 'absolute', xl: 'relative' }}
|
||||
width={{ sm: isOpen ? 'viewWidth' : 'auto', md: 'auto' }}
|
||||
ref={searchRef}
|
||||
@@ -187,6 +181,7 @@ export const SearchBar = () => {
|
||||
render={({ translation }) => (
|
||||
<Box
|
||||
as="input"
|
||||
data-cy="search-bar-input"
|
||||
placeholder={translation as string}
|
||||
onChange={(event: ChangeEvent<HTMLInputElement>) => {
|
||||
!isOpen && toggleOpen()
|
||||
|
||||
@@ -1,30 +1,33 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import { useTrace } from '@uniswap/analytics'
|
||||
import { InterfaceSectionName, NavBarSearchTypes } from '@uniswap/analytics-events'
|
||||
import { useWeb3React } from '@web3-react/core'
|
||||
import { SafetyLevel } from 'graphql/data/__generated__/types-and-hooks'
|
||||
import { SearchToken } from 'graphql/data/SearchTokens'
|
||||
import useTrendingTokens from 'graphql/data/TrendingTokens'
|
||||
import { useIsNftPage } from 'hooks/useIsNftPage'
|
||||
import { Box } from 'nft/components/Box'
|
||||
import { Column, Row } from 'nft/components/Flex'
|
||||
import { subheadSmall } from 'nft/css/common.css'
|
||||
import { useSearchHistory } from 'nft/hooks'
|
||||
import { fetchTrendingCollections } from 'nft/queries'
|
||||
import { fetchTrendingTokens } from 'nft/queries/genie/TrendingTokensFetcher'
|
||||
import { FungibleToken, GenieCollection, TimePeriod, TrendingCollection } from 'nft/types'
|
||||
import { GenieCollection, TimePeriod, TrendingCollection } from 'nft/types'
|
||||
import { formatEthPrice } from 'nft/utils/currency'
|
||||
import { ReactNode, useEffect, useMemo, useState } from 'react'
|
||||
import { useQuery } from 'react-query'
|
||||
import { useLocation } from 'react-router-dom'
|
||||
|
||||
import { ClockIcon, TrendingArrow } from '../../nft/components/icons'
|
||||
import { useRecentlySearchedAssets } from './RecentlySearchedAssets'
|
||||
import * as styles from './SearchBar.css'
|
||||
import { CollectionRow, SkeletonRow, TokenRow } from './SuggestionRow'
|
||||
|
||||
function isCollection(suggestion: GenieCollection | FungibleToken | TrendingCollection) {
|
||||
return (suggestion as FungibleToken).decimals === undefined
|
||||
function isCollection(suggestion: GenieCollection | SearchToken | TrendingCollection) {
|
||||
return (suggestion as SearchToken).decimals === undefined
|
||||
}
|
||||
|
||||
interface SearchBarDropdownSectionProps {
|
||||
toggleOpen: () => void
|
||||
suggestions: (GenieCollection | FungibleToken)[]
|
||||
suggestions: (GenieCollection | SearchToken)[]
|
||||
header: JSX.Element
|
||||
headerIcon?: JSX.Element
|
||||
hoveredIndex: number | undefined
|
||||
@@ -46,7 +49,7 @@ const SearchBarDropdownSection = ({
|
||||
eventProperties,
|
||||
}: SearchBarDropdownSectionProps) => {
|
||||
return (
|
||||
<Column gap="12">
|
||||
<Column gap="12" data-cy="searchbar-dropdown">
|
||||
<Row paddingX="16" paddingY="4" gap="8" color="gray300" className={subheadSmall} style={{ lineHeight: '20px' }}>
|
||||
{headerIcon ? headerIcon : null}
|
||||
<Box>{header}</Box>
|
||||
@@ -73,7 +76,7 @@ const SearchBarDropdownSection = ({
|
||||
) : (
|
||||
<TokenRow
|
||||
key={suggestion.address}
|
||||
token={suggestion as FungibleToken}
|
||||
token={suggestion as SearchToken}
|
||||
isHovered={hoveredIndex === index + startingIndex}
|
||||
setHoveredIndex={setHoveredIndex}
|
||||
toggleOpen={toggleOpen}
|
||||
@@ -92,9 +95,13 @@ const SearchBarDropdownSection = ({
|
||||
)
|
||||
}
|
||||
|
||||
function isKnownToken(token: SearchToken) {
|
||||
return token.project?.safetyLevel == SafetyLevel.Verified || token.project?.safetyLevel == SafetyLevel.MediumWarning
|
||||
}
|
||||
|
||||
interface SearchBarDropdownProps {
|
||||
toggleOpen: () => void
|
||||
tokens: FungibleToken[]
|
||||
tokens: SearchToken[]
|
||||
collections: GenieCollection[]
|
||||
queryText: string
|
||||
hasInput: boolean
|
||||
@@ -110,8 +117,10 @@ export const SearchBarDropdown = ({
|
||||
isLoading,
|
||||
}: SearchBarDropdownProps) => {
|
||||
const [hoveredIndex, setHoveredIndex] = useState<number | undefined>(0)
|
||||
const { history: searchHistory, updateItem: updateSearchHistory } = useSearchHistory()
|
||||
const shortenedHistory = useMemo(() => searchHistory.slice(0, 2), [searchHistory])
|
||||
|
||||
const { data: searchHistory } = useRecentlySearchedAssets()
|
||||
const shortenedHistory = useMemo(() => searchHistory?.slice(0, 2) ?? [...Array<SearchToken>(2)], [searchHistory])
|
||||
|
||||
const { pathname } = useLocation()
|
||||
const isNFTPage = useIsNftPage()
|
||||
const isTokenPage = pathname.includes('/tokens')
|
||||
@@ -141,26 +150,12 @@ export const SearchBarDropdown = ({
|
||||
[isNFTPage, trendingCollectionResults]
|
||||
)
|
||||
|
||||
const { data: trendingTokenResults, isLoading: trendingTokensAreLoading } = useQuery(
|
||||
['trendingTokens'],
|
||||
() => fetchTrendingTokens(4),
|
||||
{
|
||||
refetchOnWindowFocus: false,
|
||||
refetchOnMount: false,
|
||||
refetchOnReconnect: false,
|
||||
}
|
||||
)
|
||||
useEffect(() => {
|
||||
trendingTokenResults?.forEach(updateSearchHistory)
|
||||
}, [trendingTokenResults, updateSearchHistory])
|
||||
const { data: trendingTokenData } = useTrendingTokens(useWeb3React().chainId)
|
||||
|
||||
const trendingTokensLength = isTokenPage ? 3 : 2
|
||||
const trendingTokens = useMemo(
|
||||
() =>
|
||||
trendingTokenResults
|
||||
? trendingTokenResults.slice(0, trendingTokensLength)
|
||||
: [...Array<FungibleToken>(trendingTokensLength)],
|
||||
[trendingTokenResults, trendingTokensLength]
|
||||
() => trendingTokenData?.slice(0, trendingTokensLength) ?? [...Array<SearchToken>(trendingTokensLength)],
|
||||
[trendingTokenData, trendingTokensLength]
|
||||
)
|
||||
|
||||
const totalSuggestions = hasInput
|
||||
@@ -197,10 +192,9 @@ export const SearchBarDropdown = ({
|
||||
}, [toggleOpen, hoveredIndex, totalSuggestions])
|
||||
|
||||
const hasVerifiedCollection = collections.some((collection) => collection.isVerified)
|
||||
const hasVerifiedToken = tokens.some((token) => token.onDefaultList)
|
||||
const hasKnownToken = tokens.some(isKnownToken)
|
||||
const showCollectionsFirst =
|
||||
(isNFTPage && (hasVerifiedCollection || !hasVerifiedToken)) ||
|
||||
(!isNFTPage && !hasVerifiedToken && hasVerifiedCollection)
|
||||
(isNFTPage && (hasVerifiedCollection || !hasKnownToken)) || (!isNFTPage && !hasKnownToken && hasVerifiedCollection)
|
||||
|
||||
const trace = JSON.stringify(useTrace({ section: InterfaceSectionName.NAVBAR_SEARCH }))
|
||||
|
||||
@@ -277,6 +271,7 @@ export const SearchBarDropdown = ({
|
||||
}}
|
||||
header={<Trans>Recent searches</Trans>}
|
||||
headerIcon={<ClockIcon />}
|
||||
isLoading={!searchHistory}
|
||||
/>
|
||||
)}
|
||||
{!isNFTPage && (
|
||||
@@ -292,7 +287,7 @@ export const SearchBarDropdown = ({
|
||||
}}
|
||||
header={<Trans>Popular tokens</Trans>}
|
||||
headerIcon={<TrendingArrow />}
|
||||
isLoading={trendingTokensAreLoading}
|
||||
isLoading={!trendingTokenData}
|
||||
/>
|
||||
)}
|
||||
{!isTokenPage && (
|
||||
@@ -323,7 +318,7 @@ export const SearchBarDropdown = ({
|
||||
trendingCollections,
|
||||
trendingCollectionsAreLoading,
|
||||
trendingTokens,
|
||||
trendingTokensAreLoading,
|
||||
trendingTokenData,
|
||||
hoveredIndex,
|
||||
toggleOpen,
|
||||
shortenedHistory,
|
||||
@@ -334,6 +329,7 @@ export const SearchBarDropdown = ({
|
||||
queryText,
|
||||
totalSuggestions,
|
||||
trace,
|
||||
searchHistory,
|
||||
])
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,21 +1,18 @@
|
||||
import { sendAnalyticsEvent } from '@uniswap/analytics'
|
||||
import { InterfaceEventName } from '@uniswap/analytics-events'
|
||||
import { formatUSDPrice } from '@uniswap/conedison/format'
|
||||
import { useWeb3React } from '@web3-react/core'
|
||||
import clsx from 'clsx'
|
||||
import AssetLogo from 'components/Logo/AssetLogo'
|
||||
import { L2NetworkLogo, LogoContainer } from 'components/Tokens/TokenTable/TokenRow'
|
||||
import QueryTokenLogo from 'components/Logo/QueryTokenLogo'
|
||||
import TokenSafetyIcon from 'components/TokenSafety/TokenSafetyIcon'
|
||||
import { getChainInfo } from 'constants/chainInfo'
|
||||
import { NATIVE_CHAIN_ID } from 'constants/tokens'
|
||||
import { checkWarning } from 'constants/tokenSafety'
|
||||
import { checkSearchTokenWarning } from 'constants/tokenSafety'
|
||||
import { Chain, TokenStandard } from 'graphql/data/__generated__/types-and-hooks'
|
||||
import { SearchToken } from 'graphql/data/SearchTokens'
|
||||
import { getTokenDetailsURL } from 'graphql/data/util'
|
||||
import { Box } from 'nft/components/Box'
|
||||
import { Column, Row } from 'nft/components/Flex'
|
||||
import { VerifiedIcon } from 'nft/components/icons'
|
||||
import { vars } from 'nft/css/sprinkles.css'
|
||||
import { useSearchHistory } from 'nft/hooks'
|
||||
import { FungibleToken, GenieCollection } from 'nft/types'
|
||||
import { GenieCollection } from 'nft/types'
|
||||
import { ethNumberStandardFormatter } from 'nft/utils/currency'
|
||||
import { putCommas } from 'nft/utils/putCommas'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
@@ -23,11 +20,9 @@ import { Link, useNavigate } from 'react-router-dom'
|
||||
import styled from 'styled-components/macro'
|
||||
|
||||
import { getDeltaArrow } from '../Tokens/TokenDetails/PriceChart'
|
||||
import { useAddRecentlySearchedAsset } from './RecentlySearchedAssets'
|
||||
import * as styles from './SearchBar.css'
|
||||
|
||||
const StyledLogoContainer = styled(LogoContainer)`
|
||||
margin-right: 8px;
|
||||
`
|
||||
const PriceChangeContainer = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -63,16 +58,15 @@ export const CollectionRow = ({
|
||||
}: CollectionRowProps) => {
|
||||
const [brokenImage, setBrokenImage] = useState(false)
|
||||
const [loaded, setLoaded] = useState(false)
|
||||
const addToSearchHistory = useSearchHistory(
|
||||
(state: { addItem: (item: FungibleToken | GenieCollection) => void }) => state.addItem
|
||||
)
|
||||
|
||||
const addRecentlySearchedAsset = useAddRecentlySearchedAsset()
|
||||
const navigate = useNavigate()
|
||||
|
||||
const handleClick = useCallback(() => {
|
||||
addToSearchHistory(collection)
|
||||
addRecentlySearchedAsset({ ...collection, isNft: true, chain: Chain.Ethereum })
|
||||
toggleOpen()
|
||||
sendAnalyticsEvent(InterfaceEventName.NAVBAR_RESULT_SELECTED, { ...eventProperties })
|
||||
}, [addToSearchHistory, collection, toggleOpen, eventProperties])
|
||||
}, [addRecentlySearchedAsset, collection, toggleOpen, eventProperties])
|
||||
|
||||
useEffect(() => {
|
||||
const keyDownHandler = (event: KeyboardEvent) => {
|
||||
@@ -130,17 +124,8 @@ export const CollectionRow = ({
|
||||
)
|
||||
}
|
||||
|
||||
function useBridgedAddress(token: FungibleToken): [string | undefined, number | undefined, string | undefined] {
|
||||
const { chainId: connectedChainId } = useWeb3React()
|
||||
const bridgedAddress = connectedChainId ? token.extensions?.bridgeInfo?.[connectedChainId]?.tokenAddress : undefined
|
||||
if (bridgedAddress && connectedChainId) {
|
||||
return [bridgedAddress, connectedChainId, getChainInfo(connectedChainId)?.circleLogoUrl]
|
||||
}
|
||||
return [undefined, undefined, undefined]
|
||||
}
|
||||
|
||||
interface TokenRowProps {
|
||||
token: FungibleToken
|
||||
token: SearchToken
|
||||
isHovered: boolean
|
||||
setHoveredIndex: (index: number | undefined) => void
|
||||
toggleOpen: () => void
|
||||
@@ -149,19 +134,18 @@ interface TokenRowProps {
|
||||
}
|
||||
|
||||
export const TokenRow = ({ token, isHovered, setHoveredIndex, toggleOpen, index, eventProperties }: TokenRowProps) => {
|
||||
const addToSearchHistory = useSearchHistory(
|
||||
(state: { addItem: (item: FungibleToken | GenieCollection) => void }) => state.addItem
|
||||
)
|
||||
const addRecentlySearchedAsset = useAddRecentlySearchedAsset()
|
||||
const navigate = useNavigate()
|
||||
|
||||
const handleClick = useCallback(() => {
|
||||
addToSearchHistory(token)
|
||||
const address = !token.address && token.standard === TokenStandard.Native ? 'NATIVE' : token.address
|
||||
address && addRecentlySearchedAsset({ address, chain: token.chain })
|
||||
|
||||
toggleOpen()
|
||||
sendAnalyticsEvent(InterfaceEventName.NAVBAR_RESULT_SELECTED, { ...eventProperties })
|
||||
}, [addToSearchHistory, toggleOpen, token, eventProperties])
|
||||
}, [addRecentlySearchedAsset, token, toggleOpen, eventProperties])
|
||||
|
||||
const [bridgedAddress, bridgedChain, L2Icon] = useBridgedAddress(token)
|
||||
const tokenDetailsPath = getTokenDetailsURL(bridgedAddress ?? token.address, undefined, bridgedChain ?? token.chainId)
|
||||
const tokenDetailsPath = getTokenDetailsURL(token)
|
||||
// Close the modal on escape
|
||||
useEffect(() => {
|
||||
const keyDownHandler = (event: KeyboardEvent) => {
|
||||
@@ -177,10 +161,11 @@ export const TokenRow = ({ token, isHovered, setHoveredIndex, toggleOpen, index,
|
||||
}
|
||||
}, [toggleOpen, isHovered, token, navigate, handleClick, tokenDetailsPath])
|
||||
|
||||
const arrow = getDeltaArrow(token.price24hChange, 18)
|
||||
const arrow = getDeltaArrow(token.market?.pricePercentChange?.value, 18)
|
||||
|
||||
return (
|
||||
<Link
|
||||
data-cy={`searchbar-token-row-${token.symbol}`}
|
||||
to={tokenDetailsPath}
|
||||
onClick={handleClick}
|
||||
onMouseEnter={() => !isHovered && setHoveredIndex(index)}
|
||||
@@ -189,37 +174,33 @@ export const TokenRow = ({ token, isHovered, setHoveredIndex, toggleOpen, index,
|
||||
style={{ background: isHovered ? vars.color.lightGrayOverlay : 'none' }}
|
||||
>
|
||||
<Row style={{ width: '65%' }}>
|
||||
<StyledLogoContainer>
|
||||
<AssetLogo
|
||||
isNative={token.address === NATIVE_CHAIN_ID}
|
||||
address={token.address}
|
||||
chainId={token.chainId}
|
||||
symbol={token.symbol}
|
||||
size="36px"
|
||||
backupImg={token.logoURI}
|
||||
/>
|
||||
<L2NetworkLogo networkUrl={L2Icon} size="16px" />
|
||||
</StyledLogoContainer>
|
||||
<QueryTokenLogo
|
||||
token={token}
|
||||
symbol={token.symbol}
|
||||
size="36px"
|
||||
backupImg={token.project?.logoUrl}
|
||||
style={{ paddingRight: '8px' }}
|
||||
/>
|
||||
<Column className={styles.suggestionPrimaryContainer}>
|
||||
<Row gap="4" width="full">
|
||||
<Box className={styles.primaryText}>{token.name}</Box>
|
||||
<TokenSafetyIcon warning={checkWarning(token.address)} />
|
||||
<TokenSafetyIcon warning={checkSearchTokenWarning(token)} />
|
||||
</Row>
|
||||
<Box className={styles.secondaryText}>{token.symbol}</Box>
|
||||
</Column>
|
||||
</Row>
|
||||
|
||||
<Column className={styles.suggestionSecondaryContainer}>
|
||||
{token.priceUsd && (
|
||||
{token.market?.price?.value && (
|
||||
<Row gap="4">
|
||||
<Box className={styles.primaryText}>{formatUSDPrice(token.priceUsd)}</Box>
|
||||
<Box className={styles.primaryText}>{formatUSDPrice(token.market.price.value)}</Box>
|
||||
</Row>
|
||||
)}
|
||||
{token.price24hChange && (
|
||||
{token.market?.pricePercentChange?.value && (
|
||||
<PriceChangeContainer>
|
||||
<ArrowCell>{arrow}</ArrowCell>
|
||||
<PriceChangeText isNegative={token.price24hChange < 0}>
|
||||
{Math.abs(token.price24hChange).toFixed(2)}%
|
||||
<PriceChangeText isNegative={token.market.pricePercentChange.value < 0}>
|
||||
{Math.abs(token.market.pricePercentChange.value).toFixed(2)}%
|
||||
</PriceChangeText>
|
||||
</PriceChangeContainer>
|
||||
)}
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import { useWeb3React } from '@web3-react/core'
|
||||
import Web3Status from 'components/Web3Status'
|
||||
import { NftListV2Variant, useNftListV2Flag } from 'featureFlags/flags/nftListV2'
|
||||
import { chainIdToBackendName } from 'graphql/data/util'
|
||||
import { useIsNftPage } from 'hooks/useIsNftPage'
|
||||
import { Box } from 'nft/components/Box'
|
||||
import { Row } from 'nft/components/Flex'
|
||||
import { UniIcon } from 'nft/components/icons'
|
||||
import { useProfilePageState } from 'nft/hooks'
|
||||
import { ProfilePageStateType } from 'nft/types'
|
||||
import { ReactNode } from 'react'
|
||||
import { NavLink, NavLinkProps, useLocation, useNavigate } from 'react-router-dom'
|
||||
import styled from 'styled-components/macro'
|
||||
@@ -54,8 +57,7 @@ export const PageTabs = () => {
|
||||
pathname.startsWith('/pool') ||
|
||||
pathname.startsWith('/add') ||
|
||||
pathname.startsWith('/remove') ||
|
||||
pathname.startsWith('/increase') ||
|
||||
pathname.startsWith('/find')
|
||||
pathname.startsWith('/increase')
|
||||
|
||||
const isNftPage = useIsNftPage()
|
||||
|
||||
@@ -79,6 +81,8 @@ export const PageTabs = () => {
|
||||
|
||||
const Navbar = () => {
|
||||
const isNftPage = useIsNftPage()
|
||||
const sellPageState = useProfilePageState((state) => state.state)
|
||||
const isNftListV2 = useNftListV2Flag() === NftListV2Variant.Enabled
|
||||
const navigate = useNavigate()
|
||||
|
||||
return (
|
||||
@@ -120,7 +124,7 @@ const Navbar = () => {
|
||||
<Box display={{ sm: 'none', lg: 'flex' }}>
|
||||
<MenuDropdown />
|
||||
</Box>
|
||||
{isNftPage && <Bag />}
|
||||
{isNftPage && (!isNftListV2 || sellPageState !== ProfilePageStateType.LISTING) && <Bag />}
|
||||
{!isNftPage && (
|
||||
<Box display={{ sm: 'none', lg: 'flex' }}>
|
||||
<ChainSelector />
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -99,9 +99,9 @@ export default function PositionList({
|
||||
</ToggleLabel>
|
||||
</ToggleWrap>
|
||||
</MobileHeader>
|
||||
{positions.map((p) => {
|
||||
return <PositionListItem key={p.tokenId.toString()} positionDetails={p} />
|
||||
})}
|
||||
{positions.map((p) => (
|
||||
<PositionListItem key={p.tokenId.toString()} {...p} />
|
||||
))}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
38
src/components/PositionListItem/PositionListItem.test.tsx
Normal file
38
src/components/PositionListItem/PositionListItem.test.tsx
Normal file
@@ -0,0 +1,38 @@
|
||||
import { BigNumber } from '@ethersproject/bignumber'
|
||||
import { render, screen } from 'test-utils'
|
||||
|
||||
import PositionListItem from '.'
|
||||
|
||||
jest.mock('hooks/Tokens', () => {
|
||||
const originalModule = jest.requireActual('hooks/Tokens')
|
||||
const uniSDK = jest.requireActual('@uniswap/sdk-core')
|
||||
return {
|
||||
__esModule: true,
|
||||
...originalModule,
|
||||
useToken: jest.fn(
|
||||
() =>
|
||||
new uniSDK.Token(
|
||||
1,
|
||||
'0x39AA39c021dfbaE8faC545936693aC917d5E7563',
|
||||
8,
|
||||
'https://www.example.com',
|
||||
'example.com coin'
|
||||
)
|
||||
),
|
||||
}
|
||||
})
|
||||
|
||||
test('PositionListItem should not render when the name contains a url', () => {
|
||||
const positionDetails = {
|
||||
token0: '0x39AA39c021dfbaE8faC545936693aC917d5E7563',
|
||||
token1: '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2',
|
||||
tokenId: BigNumber.from(436148),
|
||||
fee: 100,
|
||||
liquidity: BigNumber.from('0x5c985aff8059be04'),
|
||||
tickLower: -800,
|
||||
tickUpper: 1600,
|
||||
}
|
||||
render(<PositionListItem {...positionDetails} />)
|
||||
screen.debug()
|
||||
expect(screen.queryByText('.com', { exact: false })).toBe(null)
|
||||
})
|
||||
@@ -1,3 +1,4 @@
|
||||
import { BigNumber } from '@ethersproject/bignumber'
|
||||
import { Trans } from '@lingui/macro'
|
||||
import { Percent, Price, Token } from '@uniswap/sdk-core'
|
||||
import { Position } from '@uniswap/v3-sdk'
|
||||
@@ -15,9 +16,9 @@ import { Link } from 'react-router-dom'
|
||||
import { Bound } from 'state/mint/v3/actions'
|
||||
import styled from 'styled-components/macro'
|
||||
import { HideSmall, MEDIA_WIDTHS, SmallOnly } from 'theme'
|
||||
import { PositionDetails } from 'types/position'
|
||||
import { formatTickPrice } from 'utils/formatTickPrice'
|
||||
import { unwrappedToken } from 'utils/unwrappedToken'
|
||||
import { hasURL } from 'utils/urlChecks'
|
||||
|
||||
import { DAI, USDC_MAINNET, USDT, WBTC, WRAPPED_NATIVE_CURRENCY } from '../../constants/tokens'
|
||||
|
||||
@@ -109,7 +110,13 @@ const DataText = styled.div`
|
||||
`
|
||||
|
||||
interface PositionListItemProps {
|
||||
positionDetails: PositionDetails
|
||||
token0: string
|
||||
token1: string
|
||||
tokenId: BigNumber
|
||||
fee: number
|
||||
liquidity: BigNumber
|
||||
tickLower: number
|
||||
tickUpper: number
|
||||
}
|
||||
|
||||
export function getPriceOrderingFromPositionForUI(position?: Position): {
|
||||
@@ -166,16 +173,15 @@ export function getPriceOrderingFromPositionForUI(position?: Position): {
|
||||
}
|
||||
}
|
||||
|
||||
export default function PositionListItem({ positionDetails }: PositionListItemProps) {
|
||||
const {
|
||||
token0: token0Address,
|
||||
token1: token1Address,
|
||||
fee: feeAmount,
|
||||
liquidity,
|
||||
tickLower,
|
||||
tickUpper,
|
||||
} = positionDetails
|
||||
|
||||
export default function PositionListItem({
|
||||
token0: token0Address,
|
||||
token1: token1Address,
|
||||
tokenId,
|
||||
fee: feeAmount,
|
||||
liquidity,
|
||||
tickLower,
|
||||
tickUpper,
|
||||
}: PositionListItemProps) {
|
||||
const token0 = useToken(token0Address)
|
||||
const token1 = useToken(token1Address)
|
||||
|
||||
@@ -203,10 +209,23 @@ export default function PositionListItem({ positionDetails }: PositionListItemPr
|
||||
// check if price is within range
|
||||
const outOfRange: boolean = pool ? pool.tickCurrent < tickLower || pool.tickCurrent >= tickUpper : false
|
||||
|
||||
const positionSummaryLink = '/pool/' + positionDetails.tokenId
|
||||
const positionSummaryLink = '/pool/' + tokenId
|
||||
|
||||
const removed = liquidity?.eq(0)
|
||||
|
||||
const containsURL = useMemo(
|
||||
() =>
|
||||
[token0?.name, token0?.symbol, token1?.name, token1?.symbol].reduce(
|
||||
(acc, testString) => acc || Boolean(testString && hasURL(testString)),
|
||||
false
|
||||
),
|
||||
[token0?.name, token0?.symbol, token1?.name, token1?.symbol]
|
||||
)
|
||||
|
||||
if (containsURL) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<LinkRow to={positionSummaryLink}>
|
||||
<RowBetween>
|
||||
|
||||
@@ -105,235 +105,240 @@ exports[`renders currency rows correctly when currencies list is non-empty 1`] =
|
||||
}
|
||||
|
||||
<div
|
||||
style="position: relative; height: 10px; width: 100%; overflow: auto; will-change: transform; direction: ltr;"
|
||||
style="padding-right: 4px;"
|
||||
>
|
||||
<div
|
||||
style="height: 168px; width: 100%;"
|
||||
class="CurrencyList_scrollbarStyle__1pi21y70"
|
||||
style="position: relative; height: 10px; width: 100%; overflow: auto; will-change: transform; direction: ltr;"
|
||||
>
|
||||
<div
|
||||
class="c0 c1 c2 c3 token-item-0x6B175474E89094C44Da98b954EedeAC495271d0F"
|
||||
style="position: absolute; left: 0px; top: 0px; height: 56px; width: 100%;"
|
||||
tabindex="0"
|
||||
style="height: 168px; width: 100%;"
|
||||
>
|
||||
<div
|
||||
class="c4"
|
||||
>
|
||||
CurrencyLogo currency=DAI
|
||||
</div>
|
||||
<div
|
||||
class="c5"
|
||||
style="opacity: 1;"
|
||||
class="c0 c1 c2 c3 token-item-0x6B175474E89094C44Da98b954EedeAC495271d0F"
|
||||
style="position: absolute; left: 0px; top: 0px; height: 56px; width: 100%;"
|
||||
tabindex="0"
|
||||
>
|
||||
<div
|
||||
class="c0 c1"
|
||||
class="c4"
|
||||
>
|
||||
CurrencyLogo currency=DAI
|
||||
</div>
|
||||
<div
|
||||
class="c5"
|
||||
style="opacity: 1;"
|
||||
>
|
||||
<div
|
||||
class="c6 css-vurnku"
|
||||
title="Dai Stablecoin"
|
||||
>
|
||||
Dai Stablecoin
|
||||
</div>
|
||||
<div
|
||||
class="c7"
|
||||
class="c0 c1"
|
||||
>
|
||||
<div
|
||||
class="c8"
|
||||
class="c6 css-vurnku"
|
||||
title="Dai Stablecoin"
|
||||
>
|
||||
<svg
|
||||
class="c9"
|
||||
fill="none"
|
||||
height="24"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
viewBox="0 0 24 24"
|
||||
width="24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
Dai Stablecoin
|
||||
</div>
|
||||
<div
|
||||
class="c7"
|
||||
>
|
||||
<div
|
||||
class="c8"
|
||||
>
|
||||
<path
|
||||
d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"
|
||||
/>
|
||||
<line
|
||||
x1="12"
|
||||
x2="12"
|
||||
y1="9"
|
||||
y2="13"
|
||||
/>
|
||||
<line
|
||||
x1="12"
|
||||
x2="12.01"
|
||||
y1="17"
|
||||
y2="17"
|
||||
/>
|
||||
</svg>
|
||||
<svg
|
||||
class="c9"
|
||||
fill="none"
|
||||
height="24"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
viewBox="0 0 24 24"
|
||||
width="24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"
|
||||
/>
|
||||
<line
|
||||
x1="12"
|
||||
x2="12"
|
||||
y1="9"
|
||||
y2="13"
|
||||
/>
|
||||
<line
|
||||
x1="12"
|
||||
x2="12.01"
|
||||
y1="17"
|
||||
y2="17"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="c10 css-yfjwjl"
|
||||
>
|
||||
DAI
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="c4"
|
||||
>
|
||||
<div
|
||||
class="c0 c1 c11"
|
||||
style="justify-self: flex-end;"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="c0 c1 c2 c3 token-item-0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"
|
||||
style="position: absolute; left: 0px; top: 56px; height: 56px; width: 100%;"
|
||||
tabindex="0"
|
||||
>
|
||||
<div
|
||||
class="c4"
|
||||
>
|
||||
CurrencyLogo currency=USDC
|
||||
</div>
|
||||
<div
|
||||
class="c5"
|
||||
style="opacity: 1;"
|
||||
>
|
||||
<div
|
||||
class="c0 c1"
|
||||
>
|
||||
<div
|
||||
class="c6 css-vurnku"
|
||||
title="USD//C"
|
||||
class="c10 css-yfjwjl"
|
||||
>
|
||||
USD//C
|
||||
DAI
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="c4"
|
||||
>
|
||||
<div
|
||||
class="c7"
|
||||
class="c0 c1 c11"
|
||||
style="justify-self: flex-end;"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="c0 c1 c2 c3 token-item-0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"
|
||||
style="position: absolute; left: 0px; top: 56px; height: 56px; width: 100%;"
|
||||
tabindex="0"
|
||||
>
|
||||
<div
|
||||
class="c4"
|
||||
>
|
||||
CurrencyLogo currency=USDC
|
||||
</div>
|
||||
<div
|
||||
class="c5"
|
||||
style="opacity: 1;"
|
||||
>
|
||||
<div
|
||||
class="c0 c1"
|
||||
>
|
||||
<div
|
||||
class="c8"
|
||||
class="c6 css-vurnku"
|
||||
title="USD//C"
|
||||
>
|
||||
<svg
|
||||
class="c9"
|
||||
fill="none"
|
||||
height="24"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
viewBox="0 0 24 24"
|
||||
width="24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
USD//C
|
||||
</div>
|
||||
<div
|
||||
class="c7"
|
||||
>
|
||||
<div
|
||||
class="c8"
|
||||
>
|
||||
<path
|
||||
d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"
|
||||
/>
|
||||
<line
|
||||
x1="12"
|
||||
x2="12"
|
||||
y1="9"
|
||||
y2="13"
|
||||
/>
|
||||
<line
|
||||
x1="12"
|
||||
x2="12.01"
|
||||
y1="17"
|
||||
y2="17"
|
||||
/>
|
||||
</svg>
|
||||
<svg
|
||||
class="c9"
|
||||
fill="none"
|
||||
height="24"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
viewBox="0 0 24 24"
|
||||
width="24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"
|
||||
/>
|
||||
<line
|
||||
x1="12"
|
||||
x2="12"
|
||||
y1="9"
|
||||
y2="13"
|
||||
/>
|
||||
<line
|
||||
x1="12"
|
||||
x2="12.01"
|
||||
y1="17"
|
||||
y2="17"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="c10 css-yfjwjl"
|
||||
>
|
||||
USDC
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="c4"
|
||||
>
|
||||
<div
|
||||
class="c0 c1 c11"
|
||||
style="justify-self: flex-end;"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="c0 c1 c2 c3 token-item-0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599"
|
||||
style="position: absolute; left: 0px; top: 112px; height: 56px; width: 100%;"
|
||||
tabindex="0"
|
||||
>
|
||||
<div
|
||||
class="c4"
|
||||
>
|
||||
CurrencyLogo currency=WBTC
|
||||
</div>
|
||||
<div
|
||||
class="c5"
|
||||
style="opacity: 1;"
|
||||
>
|
||||
<div
|
||||
class="c0 c1"
|
||||
>
|
||||
<div
|
||||
class="c6 css-vurnku"
|
||||
title="Wrapped BTC"
|
||||
class="c10 css-yfjwjl"
|
||||
>
|
||||
Wrapped BTC
|
||||
USDC
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="c4"
|
||||
>
|
||||
<div
|
||||
class="c7"
|
||||
class="c0 c1 c11"
|
||||
style="justify-self: flex-end;"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="c0 c1 c2 c3 token-item-0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599"
|
||||
style="position: absolute; left: 0px; top: 112px; height: 56px; width: 100%;"
|
||||
tabindex="0"
|
||||
>
|
||||
<div
|
||||
class="c4"
|
||||
>
|
||||
CurrencyLogo currency=WBTC
|
||||
</div>
|
||||
<div
|
||||
class="c5"
|
||||
style="opacity: 1;"
|
||||
>
|
||||
<div
|
||||
class="c0 c1"
|
||||
>
|
||||
<div
|
||||
class="c8"
|
||||
class="c6 css-vurnku"
|
||||
title="Wrapped BTC"
|
||||
>
|
||||
<svg
|
||||
class="c9"
|
||||
fill="none"
|
||||
height="24"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
viewBox="0 0 24 24"
|
||||
width="24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"
|
||||
/>
|
||||
<line
|
||||
x1="12"
|
||||
x2="12"
|
||||
y1="9"
|
||||
y2="13"
|
||||
/>
|
||||
<line
|
||||
x1="12"
|
||||
x2="12.01"
|
||||
y1="17"
|
||||
y2="17"
|
||||
/>
|
||||
</svg>
|
||||
Wrapped BTC
|
||||
</div>
|
||||
<div
|
||||
class="c7"
|
||||
>
|
||||
<div
|
||||
class="c8"
|
||||
>
|
||||
<svg
|
||||
class="c9"
|
||||
fill="none"
|
||||
height="24"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
viewBox="0 0 24 24"
|
||||
width="24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"
|
||||
/>
|
||||
<line
|
||||
x1="12"
|
||||
x2="12"
|
||||
y1="9"
|
||||
y2="13"
|
||||
/>
|
||||
<line
|
||||
x1="12"
|
||||
x2="12.01"
|
||||
y1="17"
|
||||
y2="17"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="c10 css-yfjwjl"
|
||||
>
|
||||
WBTC
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="c10 css-yfjwjl"
|
||||
class="c4"
|
||||
>
|
||||
WBTC
|
||||
<div
|
||||
class="c0 c1 c11"
|
||||
style="justify-self: flex-end;"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="c4"
|
||||
>
|
||||
<div
|
||||
class="c0 c1 c11"
|
||||
style="justify-self: flex-end;"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -383,31 +388,36 @@ exports[`renders loading rows when isLoading is true 1`] = `
|
||||
}
|
||||
|
||||
<div
|
||||
style="position: relative; height: 10px; width: 100%; overflow: auto; will-change: transform; direction: ltr;"
|
||||
style="padding-right: 4px;"
|
||||
>
|
||||
<div
|
||||
style="height: 560px; width: 100%;"
|
||||
class="CurrencyList_scrollbarStyle__1pi21y70"
|
||||
style="position: relative; height: 10px; width: 100%; overflow: auto; will-change: transform; direction: ltr;"
|
||||
>
|
||||
<div
|
||||
class="c0 c1"
|
||||
style="height: 560px; width: 100%;"
|
||||
>
|
||||
<div />
|
||||
<div />
|
||||
<div />
|
||||
</div>
|
||||
<div
|
||||
class="c0 c1"
|
||||
>
|
||||
<div />
|
||||
<div />
|
||||
<div />
|
||||
</div>
|
||||
<div
|
||||
class="c0 c1"
|
||||
>
|
||||
<div />
|
||||
<div />
|
||||
<div />
|
||||
<div
|
||||
class="c0 c1"
|
||||
>
|
||||
<div />
|
||||
<div />
|
||||
<div />
|
||||
</div>
|
||||
<div
|
||||
class="c0 c1"
|
||||
>
|
||||
<div />
|
||||
<div />
|
||||
<div />
|
||||
</div>
|
||||
<div
|
||||
class="c0 c1"
|
||||
>
|
||||
<div />
|
||||
<div />
|
||||
<div />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
20
src/components/SearchModal/CurrencyList/index.css.ts
Normal file
20
src/components/SearchModal/CurrencyList/index.css.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { style } from '@vanilla-extract/css'
|
||||
import { themeVars } from 'nft/css/sprinkles.css'
|
||||
|
||||
export const scrollbarStyle = style([
|
||||
{
|
||||
scrollbarWidth: 'thin',
|
||||
scrollbarColor: `${themeVars.colors.backgroundOutline} transparent`,
|
||||
height: '100%',
|
||||
selectors: {
|
||||
'&::-webkit-scrollbar': {
|
||||
background: 'transparent',
|
||||
width: '4px',
|
||||
},
|
||||
'&::-webkit-scrollbar-thumb': {
|
||||
background: `${themeVars.colors.backgroundOutline}`,
|
||||
borderRadius: '8px',
|
||||
},
|
||||
},
|
||||
},
|
||||
])
|
||||
@@ -20,6 +20,7 @@ import CurrencyLogo from '../../Logo/CurrencyLogo'
|
||||
import Row, { RowFixed } from '../../Row'
|
||||
import { MouseoverTooltip } from '../../Tooltip'
|
||||
import { LoadingRows, MenuItem } from '../styleds'
|
||||
import * as styles from './index.css'
|
||||
|
||||
function currencyKey(currency: Currency): string {
|
||||
return currency.isToken ? currency.address : 'ETHER'
|
||||
@@ -288,21 +289,34 @@ export default function CurrencyList({
|
||||
return currencyKey(currency)
|
||||
}, [])
|
||||
|
||||
return isLoading ? (
|
||||
<FixedSizeList height={height} ref={fixedListRef as any} width="100%" itemData={[]} itemCount={10} itemSize={56}>
|
||||
{LoadingRow}
|
||||
</FixedSizeList>
|
||||
) : (
|
||||
<FixedSizeList
|
||||
height={height}
|
||||
ref={fixedListRef as any}
|
||||
width="100%"
|
||||
itemData={itemData}
|
||||
itemCount={itemData.length}
|
||||
itemSize={56}
|
||||
itemKey={itemKey}
|
||||
>
|
||||
{Row}
|
||||
</FixedSizeList>
|
||||
return (
|
||||
<div style={{ paddingRight: '4px' }}>
|
||||
{isLoading ? (
|
||||
<FixedSizeList
|
||||
className={styles.scrollbarStyle}
|
||||
height={height}
|
||||
ref={fixedListRef as any}
|
||||
width="100%"
|
||||
itemData={[]}
|
||||
itemCount={10}
|
||||
itemSize={56}
|
||||
>
|
||||
{LoadingRow}
|
||||
</FixedSizeList>
|
||||
) : (
|
||||
<FixedSizeList
|
||||
className={styles.scrollbarStyle}
|
||||
height={height}
|
||||
ref={fixedListRef as any}
|
||||
width="100%"
|
||||
itemData={itemData}
|
||||
itemCount={itemData.length}
|
||||
itemSize={56}
|
||||
itemKey={itemKey}
|
||||
>
|
||||
{Row}
|
||||
</FixedSizeList>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -32,8 +32,10 @@ import { PaddedColumn, SearchInput, Separator } from './styleds'
|
||||
const ContentWrapper = styled(Column)`
|
||||
background-color: ${({ theme }) => theme.backgroundSurface};
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
flex: 1 1;
|
||||
position: relative;
|
||||
border-radius: 20px;
|
||||
`
|
||||
|
||||
interface CurrencySearchProps {
|
||||
@@ -45,6 +47,7 @@ interface CurrencySearchProps {
|
||||
showCommonBases?: boolean
|
||||
showCurrencyAmount?: boolean
|
||||
disableNonToken?: boolean
|
||||
onlyShowCurrenciesWithBalance?: boolean
|
||||
}
|
||||
|
||||
export function CurrencySearch({
|
||||
@@ -56,6 +59,7 @@ export function CurrencySearch({
|
||||
disableNonToken,
|
||||
onDismiss,
|
||||
isOpen,
|
||||
onlyShowCurrenciesWithBalance,
|
||||
}: CurrencySearchProps) {
|
||||
const { chainId } = useWeb3React()
|
||||
const theme = useTheme()
|
||||
@@ -92,6 +96,10 @@ export function CurrencySearch({
|
||||
!balancesAreLoading
|
||||
? filteredTokens
|
||||
.filter((token) => {
|
||||
if (onlyShowCurrenciesWithBalance) {
|
||||
return balances[token.address]?.greaterThan(0)
|
||||
}
|
||||
|
||||
// If there is no query, filter out unselected user-added tokens with no balance.
|
||||
if (!debouncedQuery && token instanceof UserAddedToken) {
|
||||
if (selectedCurrency?.equals(token) || otherSelectedCurrency?.equals(token)) return true
|
||||
@@ -101,7 +109,15 @@ export function CurrencySearch({
|
||||
})
|
||||
.sort(tokenComparator.bind(null, balances))
|
||||
: [],
|
||||
[balances, balancesAreLoading, debouncedQuery, filteredTokens, otherSelectedCurrency, selectedCurrency]
|
||||
[
|
||||
balances,
|
||||
balancesAreLoading,
|
||||
debouncedQuery,
|
||||
filteredTokens,
|
||||
otherSelectedCurrency,
|
||||
selectedCurrency,
|
||||
onlyShowCurrenciesWithBalance,
|
||||
]
|
||||
)
|
||||
const isLoading = Boolean(balancesAreLoading && !tokenLoaderTimerElapsed)
|
||||
|
||||
@@ -114,11 +130,23 @@ export function CurrencySearch({
|
||||
const s = debouncedQuery.toLowerCase().trim()
|
||||
|
||||
const tokens = filteredSortedTokens.filter((t) => !(t.equals(wrapped) || (disableNonToken && t.isNative)))
|
||||
const natives = (disableNonToken || native.equals(wrapped) ? [wrapped] : [native, wrapped]).filter(
|
||||
(n) => n.symbol?.toLowerCase()?.indexOf(s) !== -1 || n.name?.toLowerCase()?.indexOf(s) !== -1
|
||||
)
|
||||
const shouldShowWrapped =
|
||||
!onlyShowCurrenciesWithBalance || (!balancesAreLoading && balances[wrapped.address]?.greaterThan(0))
|
||||
const natives = (
|
||||
disableNonToken || native.equals(wrapped) ? [wrapped] : shouldShowWrapped ? [native, wrapped] : [native]
|
||||
).filter((n) => n.symbol?.toLowerCase()?.indexOf(s) !== -1 || n.name?.toLowerCase()?.indexOf(s) !== -1)
|
||||
|
||||
return [...natives, ...tokens]
|
||||
}, [debouncedQuery, filteredSortedTokens, wrapped, disableNonToken, native])
|
||||
}, [
|
||||
debouncedQuery,
|
||||
filteredSortedTokens,
|
||||
onlyShowCurrenciesWithBalance,
|
||||
balancesAreLoading,
|
||||
balances,
|
||||
wrapped,
|
||||
disableNonToken,
|
||||
native,
|
||||
])
|
||||
|
||||
const handleCurrencySelect = useCallback(
|
||||
(currency: Currency, hasWarning?: boolean) => {
|
||||
@@ -168,7 +196,9 @@ export function CurrencySearch({
|
||||
|
||||
// if no results on main list, show option to expand into inactive
|
||||
const filteredInactiveTokens = useSearchInactiveTokenLists(
|
||||
filteredTokens.length === 0 || (debouncedQuery.length > 2 && !isAddressSearch) ? debouncedQuery : undefined
|
||||
!onlyShowCurrenciesWithBalance && (filteredTokens.length === 0 || (debouncedQuery.length > 2 && !isAddressSearch))
|
||||
? debouncedQuery
|
||||
: undefined
|
||||
)
|
||||
|
||||
// Timeout token loader after 3 seconds to avoid hanging in a loading state.
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -3,9 +3,11 @@ import { formatCurrencyAmount, NumberType } from '@uniswap/conedison/format'
|
||||
import { Currency } from '@uniswap/sdk-core'
|
||||
import { useWeb3React } from '@web3-react/core'
|
||||
import { NATIVE_CHAIN_ID } from 'constants/tokens'
|
||||
import { FeatureGate } from 'featureFlags/flags/featureFlags'
|
||||
import { CHAIN_ID_TO_BACKEND_NAME } from 'graphql/data/util'
|
||||
import { useStablecoinValue } from 'hooks/useStablecoinPrice'
|
||||
import useCurrencyBalance from 'lib/hooks/useCurrencyBalance'
|
||||
import { useGate } from 'statsig-react'
|
||||
import styled from 'styled-components/macro'
|
||||
import { StyledInternalLink } from 'theme'
|
||||
|
||||
@@ -13,9 +15,10 @@ const Wrapper = styled.div`
|
||||
align-content: center;
|
||||
align-items: center;
|
||||
border: 1px solid ${({ theme }) => theme.backgroundOutline};
|
||||
border-bottom: none;
|
||||
background-color: ${({ theme }) => theme.backgroundSurface};
|
||||
border-radius: 20px 20px 0px 0px;
|
||||
bottom: 56px;
|
||||
bottom: 52px;
|
||||
color: ${({ theme }) => theme.textSecondary};
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
@@ -86,6 +89,7 @@ export default function MobileBalanceSummaryFooter({ token }: { token: Currency
|
||||
const formattedBalance = formatCurrencyAmount(balance, NumberType.TokenNonTx)
|
||||
const formattedUsdValue = formatCurrencyAmount(useStablecoinValue(balance), NumberType.FiatTokenStats)
|
||||
const chain = CHAIN_ID_TO_BACKEND_NAME[token.chainId].toLowerCase()
|
||||
const { value: isDummyGateFlagEnabled } = useGate(FeatureGate.DUMMY)
|
||||
|
||||
return (
|
||||
<Wrapper>
|
||||
@@ -101,7 +105,7 @@ export default function MobileBalanceSummaryFooter({ token }: { token: Currency
|
||||
</BalanceInfo>
|
||||
)}
|
||||
<SwapButton to={`/swap?chainName=${chain}&outputCurrency=${token.isNative ? NATIVE_CHAIN_ID : token.address}`}>
|
||||
<Trans>Swap</Trans>
|
||||
<Trans>{isDummyGateFlagEnabled ? 'Go to Swap' : 'Swap'}</Trans>
|
||||
</SwapButton>
|
||||
</Wrapper>
|
||||
)
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -20,7 +20,6 @@ import TokenDetailsSkeleton, {
|
||||
TokenNameCell,
|
||||
} from 'components/Tokens/TokenDetails/Skeleton'
|
||||
import StatsSection from 'components/Tokens/TokenDetails/StatsSection'
|
||||
import { L2NetworkLogo, LogoContainer } from 'components/Tokens/TokenTable/TokenRow'
|
||||
import TokenSafetyMessage from 'components/TokenSafety/TokenSafetyMessage'
|
||||
import TokenSafetyModal from 'components/TokenSafety/TokenSafetyModal'
|
||||
import Widget from 'components/Widget'
|
||||
@@ -111,7 +110,7 @@ export default function TokenDetails({
|
||||
|
||||
const pageChainId = CHAIN_NAME_TO_CHAIN_ID[chain]
|
||||
|
||||
const tokenQueryData = tokenQuery.tokens?.[0]
|
||||
const tokenQueryData = tokenQuery.token
|
||||
const crossChainMap = useMemo(
|
||||
() =>
|
||||
tokenQueryData?.project?.tokens.reduce((map, current) => {
|
||||
@@ -134,25 +133,27 @@ export default function TokenDetails({
|
||||
if (!address) return
|
||||
const bridgedAddress = crossChainMap[update]
|
||||
if (bridgedAddress) {
|
||||
startTokenTransition(() => navigate(getTokenDetailsURL(bridgedAddress, update)))
|
||||
startTokenTransition(() => navigate(getTokenDetailsURL({ address: bridgedAddress, chain })))
|
||||
} else if (didFetchFromChain || token?.isNative) {
|
||||
startTokenTransition(() => navigate(getTokenDetailsURL(address, update)))
|
||||
startTokenTransition(() => navigate(getTokenDetailsURL({ address, chain })))
|
||||
}
|
||||
},
|
||||
[address, crossChainMap, didFetchFromChain, navigate, token?.isNative]
|
||||
[address, chain, crossChainMap, didFetchFromChain, navigate, token?.isNative]
|
||||
)
|
||||
useOnGlobalChainSwitch(navigateToTokenForChain)
|
||||
|
||||
const navigateToWidgetSelectedToken = useCallback(
|
||||
(token: Currency) => {
|
||||
const address = token.isNative ? NATIVE_CHAIN_ID : token.address
|
||||
startTokenTransition(() => navigate(getTokenDetailsURL(address, chain)))
|
||||
startTokenTransition(() => navigate(getTokenDetailsURL({ address, chain })))
|
||||
},
|
||||
[chain, navigate]
|
||||
)
|
||||
|
||||
const [continueSwap, setContinueSwap] = useState<{ resolve: (value: boolean | PromiseLike<boolean>) => void }>()
|
||||
|
||||
const [openTokenSafetyModal, setOpenTokenSafetyModal] = useState(false)
|
||||
|
||||
// Show token safety modal if Swap-reviewing a warning token, at all times if the current token is blocked
|
||||
const shouldShowSpeedbump = !useIsUserAddedTokenOnChain(address, pageChainId) && tokenWarning !== null
|
||||
const onReviewSwapClick = useCallback(
|
||||
@@ -168,8 +169,6 @@ export default function TokenDetails({
|
||||
[continueSwap, setContinueSwap]
|
||||
)
|
||||
|
||||
const L2Icon = getChainInfo(pageChainId)?.circleLogoUrl
|
||||
|
||||
// address will never be undefined if token is defined; address is checked here to appease typechecker
|
||||
if (token === undefined || !address) {
|
||||
return <InvalidTokenDetails chainName={address && getChainInfo(pageChainId)?.label} />
|
||||
@@ -186,12 +185,10 @@ export default function TokenDetails({
|
||||
<BreadcrumbNavLink to={`/tokens/${chain.toLowerCase()}`}>
|
||||
<ArrowLeft data-testid="token-details-return-button" size={14} /> Tokens
|
||||
</BreadcrumbNavLink>
|
||||
<TokenInfoContainer>
|
||||
<TokenInfoContainer data-testid="token-info-container">
|
||||
<TokenNameCell>
|
||||
<LogoContainer>
|
||||
<CurrencyLogo currency={token} size="32px" />
|
||||
<L2NetworkLogo networkUrl={L2Icon} size="16px" />
|
||||
</LogoContainer>
|
||||
<CurrencyLogo currency={token} size="32px" hideL2Icon={false} />
|
||||
|
||||
{token.name ?? <Trans>Name not found</Trans>}
|
||||
<TokenSymbol>{token.symbol ?? <Trans>Symbol not found</Trans>}</TokenSymbol>
|
||||
</TokenNameCell>
|
||||
@@ -220,22 +217,28 @@ export default function TokenDetails({
|
||||
<TokenDetailsSkeleton />
|
||||
)}
|
||||
|
||||
<RightPanel>
|
||||
<Widget
|
||||
token={token ?? undefined}
|
||||
onTokenChange={navigateToWidgetSelectedToken}
|
||||
onReviewSwapClick={onReviewSwapClick}
|
||||
/>
|
||||
<RightPanel onClick={() => isBlockedToken && setOpenTokenSafetyModal(true)}>
|
||||
<div style={{ pointerEvents: isBlockedToken ? 'none' : 'auto' }}>
|
||||
<Widget
|
||||
defaultTokens={{
|
||||
default: token ?? undefined,
|
||||
}}
|
||||
onDefaultTokenChange={navigateToWidgetSelectedToken}
|
||||
onReviewSwapClick={onReviewSwapClick}
|
||||
/>
|
||||
</div>
|
||||
{tokenWarning && <TokenSafetyMessage tokenAddress={address} warning={tokenWarning} />}
|
||||
{token && <BalanceSummary token={token} />}
|
||||
</RightPanel>
|
||||
{token && <MobileBalanceSummaryFooter token={token} />}
|
||||
|
||||
<TokenSafetyModal
|
||||
isOpen={isBlockedToken || !!continueSwap}
|
||||
isOpen={openTokenSafetyModal || !!continueSwap}
|
||||
tokenAddress={address}
|
||||
onContinue={() => onResolveSwap(true)}
|
||||
onBlocked={() => navigate(-1)}
|
||||
onBlocked={() => {
|
||||
setOpenTokenSafetyModal(false)
|
||||
}}
|
||||
onCancel={() => onResolveSwap(false)}
|
||||
showCancel={true}
|
||||
/>
|
||||
|
||||
@@ -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;
|
||||
@@ -307,7 +289,9 @@ export const HEADER_DESCRIPTIONS: Record<TokenSortMethod, ReactNode | undefined>
|
||||
[TokenSortMethod.PRICE]: undefined,
|
||||
[TokenSortMethod.PERCENT_CHANGE]: undefined,
|
||||
[TokenSortMethod.TOTAL_VALUE_LOCKED]: (
|
||||
<Trans>Total value locked (TVL) is the amount of the asset that’s currently in a Uniswap v3 liquidity pool.</Trans>
|
||||
<Trans>
|
||||
Total value locked (TVL) is the aggregate amount of the asset available across all Uniswap v3 liquidity pools.
|
||||
</Trans>
|
||||
),
|
||||
[TokenSortMethod.VOLUME]: (
|
||||
<Trans>Volume is the amount of the asset that has been traded on Uniswap v3 during the selected time frame.</Trans>
|
||||
@@ -378,15 +362,23 @@ function TokenRow({
|
||||
const rowCells = (
|
||||
<>
|
||||
<ListNumberCell header={header}>{listNumber}</ListNumberCell>
|
||||
<NameCell>{tokenInfo}</NameCell>
|
||||
<PriceCell sortable={header}>{price}</PriceCell>
|
||||
<PercentChangeCell sortable={header}>{percentChange}</PercentChangeCell>
|
||||
<TvlCell sortable={header}>{tvl}</TvlCell>
|
||||
<VolumeCell sortable={header}>{volume}</VolumeCell>
|
||||
<NameCell data-testid="name-cell">{tokenInfo}</NameCell>
|
||||
<PriceCell data-testid="price-cell" sortable={header}>
|
||||
{price}
|
||||
</PriceCell>
|
||||
<PercentChangeCell data-testid="percent-change-cell" sortable={header}>
|
||||
{percentChange}
|
||||
</PercentChangeCell>
|
||||
<TvlCell data-testid="tvl-cell" sortable={header}>
|
||||
{tvl}
|
||||
</TvlCell>
|
||||
<VolumeCell data-testid="volume-cell" sortable={header}>
|
||||
{volume}
|
||||
</VolumeCell>
|
||||
<SparkLineCell>{sparkLine}</SparkLineCell>
|
||||
</>
|
||||
)
|
||||
if (header) return <StyledHeaderRow>{rowCells}</StyledHeaderRow>
|
||||
if (header) return <StyledHeaderRow data-testid="header-row">{rowCells}</StyledHeaderRow>
|
||||
return <StyledTokenRow {...rest}>{rowCells}</StyledTokenRow>
|
||||
}
|
||||
|
||||
@@ -434,34 +426,29 @@ interface LoadedRowProps {
|
||||
tokenListLength: number
|
||||
token: NonNullable<TopToken>
|
||||
sparklineMap: SparklineMap
|
||||
sortRank: number
|
||||
}
|
||||
|
||||
/* Loaded State: row component with token information */
|
||||
export const LoadedRow = forwardRef((props: LoadedRowProps, ref: ForwardedRef<HTMLDivElement>) => {
|
||||
const { tokenListIndex, tokenListLength, token } = props
|
||||
const tokenAddress = token.address
|
||||
const tokenName = token.name
|
||||
const tokenSymbol = token.symbol
|
||||
const { tokenListIndex, tokenListLength, token, sortRank } = props
|
||||
const filterString = useAtomValue(filterStringAtom)
|
||||
const sortAscending = useAtomValue(sortAscendingAtom)
|
||||
|
||||
const lowercaseChainName = useParams<{ chainName?: string }>().chainName?.toUpperCase() ?? 'ethereum'
|
||||
const filterNetwork = lowercaseChainName.toUpperCase()
|
||||
const chainId = CHAIN_NAME_TO_CHAIN_ID[filterNetwork]
|
||||
const L2Icon = getChainInfo(chainId)?.circleLogoUrl
|
||||
const timePeriod = useAtomValue(filterTimeAtom)
|
||||
const delta = token.market?.pricePercentChange?.value
|
||||
const arrow = getDeltaArrow(delta)
|
||||
const smallArrow = getDeltaArrow(delta, 14)
|
||||
const formattedDelta = formatDelta(delta)
|
||||
const rank = sortAscending ? tokenListLength - tokenListIndex : tokenListIndex + 1
|
||||
|
||||
const exploreTokenSelectedEventProperties = {
|
||||
chain_id: chainId,
|
||||
token_address: tokenAddress,
|
||||
token_symbol: tokenSymbol,
|
||||
token_address: token.address,
|
||||
token_symbol: token.symbol,
|
||||
token_list_index: tokenListIndex,
|
||||
token_list_rank: rank,
|
||||
token_list_rank: sortRank,
|
||||
token_list_length: tokenListLength,
|
||||
time_frame: timePeriod,
|
||||
search_token_address_input: filterString,
|
||||
@@ -469,25 +456,22 @@ export const LoadedRow = forwardRef((props: LoadedRowProps, ref: ForwardedRef<HT
|
||||
|
||||
// TODO: currency logo sizing mobile (32px) vs. desktop (24px)
|
||||
return (
|
||||
<div ref={ref} data-testid={`token-table-row-${tokenName}`}>
|
||||
<div ref={ref} data-testid={`token-table-row-${token.symbol}`}>
|
||||
<StyledLink
|
||||
to={getTokenDetailsURL(token.address ?? '', token.chain)}
|
||||
to={getTokenDetailsURL(token)}
|
||||
onClick={() =>
|
||||
sendAnalyticsEvent(InterfaceEventName.EXPLORE_TOKEN_ROW_CLICKED, exploreTokenSelectedEventProperties)
|
||||
}
|
||||
>
|
||||
<TokenRow
|
||||
header={false}
|
||||
listNumber={rank}
|
||||
listNumber={sortRank}
|
||||
tokenInfo={
|
||||
<ClickableName>
|
||||
<LogoContainer>
|
||||
<QueryTokenLogo token={token} />
|
||||
<L2NetworkLogo networkUrl={L2Icon} />
|
||||
</LogoContainer>
|
||||
<QueryTokenLogo token={token} />
|
||||
<TokenInfoCell>
|
||||
<TokenName>{tokenName}</TokenName>
|
||||
<TokenSymbol>{tokenSymbol}</TokenSymbol>
|
||||
<TokenName data-cy="token-name">{token.name}</TokenName>
|
||||
<TokenSymbol>{token.symbol}</TokenSymbol>
|
||||
</TokenInfoCell>
|
||||
</ClickableName>
|
||||
}
|
||||
|
||||
@@ -76,12 +76,11 @@ function LoadingTokenTable({ rowCount = PAGE_SIZE }: { rowCount?: number }) {
|
||||
}
|
||||
|
||||
export default function TokenTable() {
|
||||
// TODO: consider moving prefetched call into app.tsx and passing it here, use a preloaded call & updated on interval every 60s
|
||||
const chainName = validateUrlChainParam(useParams<{ chainName?: string }>().chainName)
|
||||
const { tokens, loadingTokens, sparklines } = useTopTokens(chainName)
|
||||
const { tokens, tokenSortRank, loadingTokens, sparklines } = useTopTokens(chainName)
|
||||
|
||||
/* loading and error state */
|
||||
if (loadingTokens) {
|
||||
if (loadingTokens && !tokens) {
|
||||
return <LoadingTokenTable rowCount={PAGE_SIZE} />
|
||||
} else if (!tokens) {
|
||||
return (
|
||||
@@ -103,13 +102,14 @@ export default function TokenTable() {
|
||||
<TokenDataContainer>
|
||||
{tokens.map(
|
||||
(token, index) =>
|
||||
token && (
|
||||
token?.address && (
|
||||
<LoadedRow
|
||||
key={token?.address}
|
||||
key={token.address}
|
||||
tokenListIndex={index}
|
||||
tokenListLength={tokens.length}
|
||||
token={token}
|
||||
sparklineMap={sparklines}
|
||||
sortRank={tokenSortRank[token.address]}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
|
||||
@@ -2,8 +2,6 @@ import { useWeb3React } from '@web3-react/core'
|
||||
import AddressClaimModal from 'components/claim/AddressClaimModal'
|
||||
import ConnectedAccountBlocked from 'components/ConnectedAccountBlocked'
|
||||
import FiatOnrampModal from 'components/FiatOnrampModal'
|
||||
import { BaseVariant } from 'featureFlags'
|
||||
import { useFiatOnrampFlag } from 'featureFlags/flags/fiatOnramp'
|
||||
import useAccountRiskCheck from 'hooks/useAccountRiskCheck'
|
||||
import { lazy } from 'react'
|
||||
import { useModalIsOpen, useToggleModal } from 'state/application/hooks'
|
||||
@@ -20,7 +18,6 @@ export default function TopLevelModals() {
|
||||
const { account } = useWeb3React()
|
||||
useAccountRiskCheck(account)
|
||||
const accountBlocked = Boolean(blockedAccountModalOpen && account)
|
||||
const fiatOnrampFlagEnabled = useFiatOnrampFlag() === BaseVariant.Enabled
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -29,7 +26,7 @@ export default function TopLevelModals() {
|
||||
<Bag />
|
||||
<TransactionCompleteModal />
|
||||
<AirdropModal />
|
||||
{fiatOnrampFlagEnabled && <FiatOnrampModal />}
|
||||
<FiatOnrampModal />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -9,8 +9,6 @@ import Tooltip from 'components/Tooltip'
|
||||
import { getConnection } from 'connection/utils'
|
||||
import { getChainInfoOrDefault } from 'constants/chainInfo'
|
||||
import { SupportedChainId } from 'constants/chains'
|
||||
import { BaseVariant } from 'featureFlags'
|
||||
import { useFiatOnrampFlag } from 'featureFlags/flags/fiatOnramp'
|
||||
import useCopyClipboard from 'hooks/useCopyClipboard'
|
||||
import useStablecoinPrice from 'hooks/useStablecoinPrice'
|
||||
import useNativeCurrency from 'lib/hooks/useNativeCurrency'
|
||||
@@ -21,7 +19,6 @@ import { ProfilePageStateType } from 'nft/types'
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import { Copy, CreditCard, ExternalLink as ExternalLinkIcon, Info, Power } from 'react-feather'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { Text } from 'rebass'
|
||||
import { useCurrencyBalanceString } from 'state/connection/hooks'
|
||||
import { useAppDispatch } from 'state/hooks'
|
||||
import { useFiatOnrampAck } from 'state/user/hooks'
|
||||
@@ -57,7 +54,7 @@ const BuyCryptoButton = styled(ThemeButton)<{ $animateBorder: boolean }>`
|
||||
border-style: solid;
|
||||
border-width: 1px;
|
||||
height: 40px;
|
||||
margin-top: 12px;
|
||||
margin-top: 8px;
|
||||
animation-direction: alternate;
|
||||
animation-duration: ${({ theme }) => theme.transition.duration.slow};
|
||||
animation-fill-mode: none;
|
||||
@@ -70,7 +67,7 @@ const WalletButton = styled(ThemeButton)`
|
||||
border-radius: 12px;
|
||||
padding-top: 10px;
|
||||
padding-bottom: 10px;
|
||||
margin-top: 12px;
|
||||
margin-top: 4px;
|
||||
color: white;
|
||||
border: none;
|
||||
`
|
||||
@@ -127,15 +124,6 @@ const FiatOnrampAvailabilityExternalLink = styled(ExternalLink)`
|
||||
margin-left: 6px;
|
||||
width: 14px;
|
||||
`
|
||||
const FlexContainer = styled.div`
|
||||
display: flex;
|
||||
`
|
||||
|
||||
const StatusWrapper = styled.div`
|
||||
display: inline-block;
|
||||
margin-top: 4px;
|
||||
width: 70%;
|
||||
`
|
||||
|
||||
const TruncatedTextStyle = css`
|
||||
text-overflow: ellipsis;
|
||||
@@ -143,8 +131,14 @@ const TruncatedTextStyle = css`
|
||||
white-space: nowrap;
|
||||
`
|
||||
|
||||
const AccountNamesWrapper = styled.div`
|
||||
const FlexContainer = styled.div`
|
||||
${TruncatedTextStyle}
|
||||
padding-right: 4px;
|
||||
display: inline-flex;
|
||||
`
|
||||
|
||||
const AccountNamesWrapper = styled.div`
|
||||
min-width: 0;
|
||||
margin-right: 8px;
|
||||
`
|
||||
|
||||
@@ -168,6 +162,9 @@ const StyledLoadingButtonSpinner = styled(LoadingButtonSpinner)`
|
||||
fill: ${({ theme }) => theme.accentAction};
|
||||
`
|
||||
const BalanceWrapper = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
padding: 16px 0;
|
||||
`
|
||||
|
||||
@@ -177,10 +174,6 @@ const HeaderWrapper = styled.div`
|
||||
justify-content: space-between;
|
||||
`
|
||||
|
||||
const AuthenticatedHeaderWrapper = styled.div`
|
||||
padding: 0 16px;
|
||||
`
|
||||
|
||||
const AuthenticatedHeader = () => {
|
||||
const { account, chainId, connector, ENSName } = useWeb3React()
|
||||
const [isCopied, setCopied] = useCopyClipboard()
|
||||
@@ -230,7 +223,6 @@ const AuthenticatedHeader = () => {
|
||||
closeModal()
|
||||
}, [clearCollectionFilters, closeModal, navigate, resetSellAssets, setSellPageState])
|
||||
|
||||
const fiatOnrampFlag = useFiatOnrampFlag()
|
||||
// animate the border of the buy crypto button when a user navigates here from the feature announcement
|
||||
// can be removed when components/FiatOnrampAnnouncment.tsx is no longer used
|
||||
const [acknowledgements, acknowledge] = useFiatOnrampAck()
|
||||
@@ -280,21 +272,19 @@ const AuthenticatedHeader = () => {
|
||||
const closeFiatOnrampUnavailableTooltip = useCallback(() => setShow(false), [setShow])
|
||||
|
||||
return (
|
||||
<AuthenticatedHeaderWrapper>
|
||||
<>
|
||||
<HeaderWrapper>
|
||||
<StatusWrapper>
|
||||
<FlexContainer>
|
||||
<StatusIcon connectionType={connectionType} size={24} />
|
||||
{ENSName ? (
|
||||
<AccountNamesWrapper>
|
||||
<ENSNameContainer>{ENSName}</ENSNameContainer>
|
||||
<AccountContainer>{account && shortenAddress(account, 2, 4)}</AccountContainer>
|
||||
</AccountNamesWrapper>
|
||||
) : (
|
||||
<ThemedText.SubHeader marginTop="2.5px">{account && shortenAddress(account, 2, 4)}</ThemedText.SubHeader>
|
||||
)}
|
||||
</FlexContainer>
|
||||
</StatusWrapper>
|
||||
<FlexContainer>
|
||||
<StatusIcon connectionType={connectionType} size={24} />
|
||||
{ENSName ? (
|
||||
<AccountNamesWrapper>
|
||||
<ENSNameContainer>{ENSName}</ENSNameContainer>
|
||||
<AccountContainer>{account && shortenAddress(account, 2, 4)}</AccountContainer>
|
||||
</AccountNamesWrapper>
|
||||
) : (
|
||||
<ThemedText.SubHeader marginTop="2.5px">{account && shortenAddress(account, 2, 4)}</ThemedText.SubHeader>
|
||||
)}
|
||||
</FlexContainer>
|
||||
<IconContainer>
|
||||
<IconButton onClick={copy} Icon={Copy}>
|
||||
{isCopied ? <Trans>Copied!</Trans> : <Trans>Copy</Trans>}
|
||||
@@ -309,9 +299,10 @@ const AuthenticatedHeader = () => {
|
||||
</HeaderWrapper>
|
||||
<Column>
|
||||
<BalanceWrapper>
|
||||
<Text fontSize={36} fontWeight={400}>
|
||||
<ThemedText.SubHeaderSmall>ETH Balance</ThemedText.SubHeaderSmall>
|
||||
<ThemedText.HeadlineLarge fontSize={36} fontWeight={400}>
|
||||
{balanceString} {nativeCurrencySymbol}
|
||||
</Text>
|
||||
</ThemedText.HeadlineLarge>
|
||||
{amountUSD !== undefined && <USDText>{formatUSDPrice(amountUSD)} USD</USDText>}
|
||||
</BalanceWrapper>
|
||||
<ProfileButton
|
||||
@@ -322,43 +313,43 @@ const AuthenticatedHeader = () => {
|
||||
>
|
||||
<Trans>View and sell NFTs</Trans>
|
||||
</ProfileButton>
|
||||
{fiatOnrampFlag === BaseVariant.Enabled && (
|
||||
<>
|
||||
<BuyCryptoButton
|
||||
$animateBorder={animateBuyCryptoButtonBorder}
|
||||
size={ButtonSize.medium}
|
||||
emphasis={ButtonEmphasis.medium}
|
||||
onClick={handleBuyCryptoClick}
|
||||
disabled={disableBuyCryptoButton}
|
||||
>
|
||||
{error ? (
|
||||
<ThemedText.BodyPrimary>{error}</ThemedText.BodyPrimary>
|
||||
<BuyCryptoButton
|
||||
$animateBorder={animateBuyCryptoButtonBorder}
|
||||
size={ButtonSize.medium}
|
||||
emphasis={ButtonEmphasis.medium}
|
||||
onClick={handleBuyCryptoClick}
|
||||
disabled={disableBuyCryptoButton}
|
||||
>
|
||||
{error ? (
|
||||
<ThemedText.BodyPrimary>{error}</ThemedText.BodyPrimary>
|
||||
) : (
|
||||
<>
|
||||
{fiatOnrampAvailabilityLoading ? (
|
||||
<StyledLoadingButtonSpinner />
|
||||
) : (
|
||||
<>
|
||||
{fiatOnrampAvailabilityLoading ? <StyledLoadingButtonSpinner /> : <CreditCard />}{' '}
|
||||
<Trans>Buy crypto</Trans>
|
||||
</>
|
||||
)}
|
||||
</BuyCryptoButton>
|
||||
{Boolean(!fiatOnrampAvailable && fiatOnrampAvailabilityChecked) && (
|
||||
<FiatOnrampNotAvailableText marginTop="8px">
|
||||
<Trans>Not available in your region</Trans>
|
||||
<Tooltip
|
||||
show={showFiatOnrampUnavailableTooltip}
|
||||
text={<Trans>Moonpay is not available in some regions. Click to learn more.</Trans>}
|
||||
>
|
||||
<FiatOnrampAvailabilityExternalLink
|
||||
onMouseEnter={openFiatOnrampUnavailableTooltip}
|
||||
onMouseLeave={closeFiatOnrampUnavailableTooltip}
|
||||
style={{ color: 'inherit' }}
|
||||
href="https://support.uniswap.org/hc/en-us/articles/11306664890381-Why-isn-t-MoonPay-available-in-my-region-"
|
||||
>
|
||||
<StyledInfoIcon />
|
||||
</FiatOnrampAvailabilityExternalLink>
|
||||
</Tooltip>
|
||||
</FiatOnrampNotAvailableText>
|
||||
)}
|
||||
</>
|
||||
<CreditCard height="20px" width="20px" />
|
||||
)}{' '}
|
||||
<Trans>Buy crypto</Trans>
|
||||
</>
|
||||
)}
|
||||
</BuyCryptoButton>
|
||||
{Boolean(!fiatOnrampAvailable && fiatOnrampAvailabilityChecked) && (
|
||||
<FiatOnrampNotAvailableText marginTop="8px">
|
||||
<Trans>Not available in your region</Trans>
|
||||
<Tooltip
|
||||
show={showFiatOnrampUnavailableTooltip}
|
||||
text={<Trans>Moonpay is not available in some regions. Click to learn more.</Trans>}
|
||||
>
|
||||
<FiatOnrampAvailabilityExternalLink
|
||||
onMouseEnter={openFiatOnrampUnavailableTooltip}
|
||||
onMouseLeave={closeFiatOnrampUnavailableTooltip}
|
||||
style={{ color: 'inherit' }}
|
||||
href="https://support.uniswap.org/hc/en-us/articles/11306664890381-Why-isn-t-MoonPay-available-in-my-region-"
|
||||
>
|
||||
<StyledInfoIcon />
|
||||
</FiatOnrampAvailabilityExternalLink>
|
||||
</Tooltip>
|
||||
</FiatOnrampNotAvailableText>
|
||||
)}
|
||||
{isUnclaimed && (
|
||||
<UNIButton onClick={openClaimModal} size={ButtonSize.medium} emphasis={ButtonEmphasis.medium}>
|
||||
@@ -371,7 +362,7 @@ const AuthenticatedHeader = () => {
|
||||
</UNIButton>
|
||||
)}
|
||||
</Column>
|
||||
</AuthenticatedHeaderWrapper>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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,8 +70,8 @@ it('loads Wallet Modal on desktop', async () => {
|
||||
|
||||
it('loads Wallet Modal on desktop with generic Injected', async () => {
|
||||
jest.spyOn(connectionUtils, 'getIsInjected').mockReturnValue(true)
|
||||
jest.spyOn(connectionUtils, 'getHasMetaMaskExtensionInstalled').mockReturnValue(false)
|
||||
jest.spyOn(connectionUtils, 'getHasCoinbaseExtensionInstalled').mockReturnValue(false)
|
||||
jest.spyOn(connectionUtils, 'getIsMetaMaskWallet').mockReturnValue(false)
|
||||
jest.spyOn(connectionUtils, 'getIsCoinbaseWallet').mockReturnValue(false)
|
||||
|
||||
render(<WalletModal pendingTransactions={[]} confirmedTransactions={[]} />)
|
||||
expect(screen.getByText('Browser Wallet')).toBeInTheDocument()
|
||||
@@ -82,8 +82,8 @@ it('loads Wallet Modal on desktop with generic Injected', async () => {
|
||||
|
||||
it('loads Wallet Modal on desktop with MetaMask installed', async () => {
|
||||
jest.spyOn(connectionUtils, 'getIsInjected').mockReturnValue(true)
|
||||
jest.spyOn(connectionUtils, 'getHasMetaMaskExtensionInstalled').mockReturnValue(true)
|
||||
jest.spyOn(connectionUtils, 'getHasCoinbaseExtensionInstalled').mockReturnValue(false)
|
||||
jest.spyOn(connectionUtils, 'getIsMetaMaskWallet').mockReturnValue(true)
|
||||
jest.spyOn(connectionUtils, 'getIsCoinbaseWallet').mockReturnValue(false)
|
||||
|
||||
render(<WalletModal pendingTransactions={[]} confirmedTransactions={[]} />)
|
||||
expect(screen.getByText('MetaMask')).toBeInTheDocument()
|
||||
@@ -96,8 +96,8 @@ it('loads Wallet Modal on mobile', async () => {
|
||||
UserAgentMock.isMobile = true
|
||||
|
||||
jest.spyOn(connectionUtils, 'getIsInjected').mockReturnValue(false)
|
||||
jest.spyOn(connectionUtils, 'getHasMetaMaskExtensionInstalled').mockReturnValue(false)
|
||||
jest.spyOn(connectionUtils, 'getHasCoinbaseExtensionInstalled').mockReturnValue(false)
|
||||
jest.spyOn(connectionUtils, 'getIsMetaMaskWallet').mockReturnValue(false)
|
||||
jest.spyOn(connectionUtils, 'getIsCoinbaseWallet').mockReturnValue(false)
|
||||
|
||||
render(<WalletModal pendingTransactions={[]} confirmedTransactions={[]} />)
|
||||
expect(screen.getByText('Open in Coinbase Wallet')).toBeInTheDocument()
|
||||
@@ -109,8 +109,8 @@ it('loads Wallet Modal on MetaMask browser', async () => {
|
||||
UserAgentMock.isMobile = true
|
||||
|
||||
jest.spyOn(connectionUtils, 'getIsInjected').mockReturnValue(true)
|
||||
jest.spyOn(connectionUtils, 'getHasMetaMaskExtensionInstalled').mockReturnValue(true)
|
||||
jest.spyOn(connectionUtils, 'getHasCoinbaseExtensionInstalled').mockReturnValue(false)
|
||||
jest.spyOn(connectionUtils, 'getIsMetaMaskWallet').mockReturnValue(true)
|
||||
jest.spyOn(connectionUtils, 'getIsCoinbaseWallet').mockReturnValue(false)
|
||||
|
||||
render(<WalletModal pendingTransactions={[]} confirmedTransactions={[]} />)
|
||||
expect(screen.getByText('MetaMask')).toBeInTheDocument()
|
||||
@@ -121,8 +121,8 @@ it('loads Wallet Modal on Coinbase Wallet browser', async () => {
|
||||
UserAgentMock.isMobile = true
|
||||
|
||||
jest.spyOn(connectionUtils, 'getIsInjected').mockReturnValue(true)
|
||||
jest.spyOn(connectionUtils, 'getHasMetaMaskExtensionInstalled').mockReturnValue(false)
|
||||
jest.spyOn(connectionUtils, 'getHasCoinbaseExtensionInstalled').mockReturnValue(true)
|
||||
jest.spyOn(connectionUtils, 'getIsMetaMaskWallet').mockReturnValue(false)
|
||||
jest.spyOn(connectionUtils, 'getIsCoinbaseWallet').mockReturnValue(true)
|
||||
|
||||
render(<WalletModal pendingTransactions={[]} confirmedTransactions={[]} />)
|
||||
expect(screen.getByText('Coinbase Wallet')).toBeInTheDocument()
|
||||
|
||||
@@ -10,9 +10,9 @@ import { networkConnection } from 'connection'
|
||||
import {
|
||||
getConnection,
|
||||
getConnectionName,
|
||||
getHasCoinbaseExtensionInstalled,
|
||||
getHasMetaMaskExtensionInstalled,
|
||||
getIsCoinbaseWallet,
|
||||
getIsInjected,
|
||||
getIsMetaMaskWallet,
|
||||
} from 'connection/utils'
|
||||
import usePrevious from 'hooks/usePrevious'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
@@ -253,8 +253,8 @@ export default function WalletModal({
|
||||
|
||||
function getOptions() {
|
||||
const isInjected = getIsInjected()
|
||||
const hasMetaMaskExtension = getHasMetaMaskExtensionInstalled()
|
||||
const hasCoinbaseExtension = getHasCoinbaseExtensionInstalled()
|
||||
const hasMetaMaskExtension = getIsMetaMaskWallet()
|
||||
const hasCoinbaseExtension = getIsCoinbaseWallet()
|
||||
|
||||
const isCoinbaseWalletBrowser = isMobile && hasCoinbaseExtension
|
||||
const isMetaMaskBrowser = isMobile && hasMetaMaskExtension
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { useWeb3React, Web3ReactHooks, Web3ReactProvider } from '@web3-react/core'
|
||||
import { Connector } from '@web3-react/types'
|
||||
import { Connection } from 'connection'
|
||||
import { ConnectionType, setMetMaskErrorHandler } from 'connection'
|
||||
import { getConnectionName } from 'connection/utils'
|
||||
import { isSupportedChain } from 'constants/chains'
|
||||
import { RPC_PROVIDERS } from 'constants/providers'
|
||||
@@ -9,19 +8,8 @@ import { TraceJsonRpcVariant, useTraceJsonRpcFlag } from 'featureFlags/flags/tra
|
||||
import useEagerlyConnect from 'hooks/useEagerlyConnect'
|
||||
import useOrderedConnections from 'hooks/useOrderedConnections'
|
||||
import { ReactNode, useEffect, useMemo } from 'react'
|
||||
import { updateConnectionError } from 'state/connection/reducer'
|
||||
import { useAppDispatch } from 'state/hooks'
|
||||
|
||||
export default function Web3Provider({ children }: { children: ReactNode }) {
|
||||
const dispatch = useAppDispatch()
|
||||
|
||||
// Set metamask error handler for metamask disconnection warning modal.
|
||||
useEffect(() => {
|
||||
setMetMaskErrorHandler((error: Error) =>
|
||||
dispatch(updateConnectionError({ connectionType: ConnectionType.INJECTED, error: error.message }))
|
||||
)
|
||||
}, [dispatch])
|
||||
|
||||
useEagerlyConnect()
|
||||
const connections = useOrderedConnections()
|
||||
const connectors: [Connector, Web3ReactHooks][] = connections.map(({ hooks, connector }) => [connector, hooks])
|
||||
|
||||
@@ -1,97 +0,0 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import { ButtonPrimary } from 'components/Button'
|
||||
import { AutoColumn } from 'components/Column'
|
||||
import Modal from 'components/Modal'
|
||||
import { RowBetween } from 'components/Row'
|
||||
import { AlertTriangle } from 'react-feather'
|
||||
import { Text } from 'rebass'
|
||||
import styled from 'styled-components/macro'
|
||||
import { CloseIcon, ThemedText } from 'theme'
|
||||
|
||||
import { useModalIsOpen, useToggleMetamaskConnectionErrorModal } from '../../state/application/hooks'
|
||||
import { ApplicationModal } from '../../state/application/reducer'
|
||||
|
||||
const Wrapper = styled.div`
|
||||
width: 100%;
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
align-items: center;
|
||||
`
|
||||
|
||||
const Container = styled.div`
|
||||
width: 100%;
|
||||
padding: 32px 32px;
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
align-items: center;
|
||||
`
|
||||
|
||||
const LogoContainer = styled.div`
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
`
|
||||
|
||||
const ShortColumn = styled(AutoColumn)`
|
||||
margin-top: 10px;
|
||||
`
|
||||
|
||||
const InfoText = styled(Text)`
|
||||
padding: 0 12px 0 12px;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
text-align: center;
|
||||
`
|
||||
|
||||
const StyledButton = styled(ButtonPrimary)`
|
||||
margin-top: 24px;
|
||||
width: 100%;
|
||||
font-weight: 600;
|
||||
`
|
||||
|
||||
const WarningIcon = styled(AlertTriangle)`
|
||||
width: 76px;
|
||||
height: 76px;
|
||||
margin-top: 4px;
|
||||
margin-bottom: 28px;
|
||||
stroke-width: 1px;
|
||||
margin-right: 4px;
|
||||
color: ${({ theme }) => theme.accentCritical};
|
||||
`
|
||||
|
||||
const onReconnect = () => window.location.reload()
|
||||
|
||||
const header = 'Wallet disconnected'
|
||||
const description = 'A Metamask error caused your wallet to disconnect. Reload the page to reconnect.'
|
||||
|
||||
export default function MetamaskConnectionError() {
|
||||
const modalOpen = useModalIsOpen(ApplicationModal.METAMASK_CONNECTION_ERROR)
|
||||
const toggleModal = useToggleMetamaskConnectionErrorModal()
|
||||
|
||||
return (
|
||||
<Modal isOpen={modalOpen} onDismiss={toggleModal} minHeight={false} maxHeight={90}>
|
||||
<Wrapper>
|
||||
<RowBetween style={{ padding: '1rem' }}>
|
||||
<div />
|
||||
<CloseIcon onClick={toggleModal} />
|
||||
</RowBetween>
|
||||
<Container>
|
||||
<AutoColumn>
|
||||
<LogoContainer>
|
||||
<WarningIcon />
|
||||
</LogoContainer>
|
||||
</AutoColumn>
|
||||
<ShortColumn>
|
||||
<InfoText>
|
||||
<ThemedText.HeadlineSmall marginBottom="8px">{header}</ThemedText.HeadlineSmall>
|
||||
<ThemedText.BodySmall>{description}</ThemedText.BodySmall>
|
||||
</InfoText>
|
||||
</ShortColumn>
|
||||
<StyledButton onClick={onReconnect}>
|
||||
<Trans>Reload</Trans>
|
||||
</StyledButton>
|
||||
</Container>
|
||||
</Wrapper>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
@@ -5,12 +5,12 @@ import { useWeb3React } from '@web3-react/core'
|
||||
import { FiatOnrampAnnouncement } from 'components/FiatOnrampAnnouncement'
|
||||
import { IconWrapper } from 'components/Identicon/StatusIcon'
|
||||
import WalletDropdown from 'components/WalletDropdown'
|
||||
import { getConnection, getIsMetaMask } from 'connection/utils'
|
||||
import { getConnection } from 'connection/utils'
|
||||
import { Portal } from 'nft/components/common/Portal'
|
||||
import { useIsNftClaimAvailable } from 'nft/hooks/useIsNftClaimAvailable'
|
||||
import { getIsValidSwapQuote } from 'pages/Swap'
|
||||
import { darken } from 'polished'
|
||||
import { useCallback, useEffect, useMemo, useRef } from 'react'
|
||||
import { useCallback, useMemo, useRef } from 'react'
|
||||
import { AlertTriangle, ChevronDown, ChevronUp } from 'react-feather'
|
||||
import { useAppSelector } from 'state/hooks'
|
||||
import { useDerivedSwapInfo } from 'state/swap/hooks'
|
||||
@@ -22,7 +22,6 @@ import { useOnClickOutside } from '../../hooks/useOnClickOutside'
|
||||
import {
|
||||
useCloseModal,
|
||||
useModalIsOpen,
|
||||
useOpenMetamaskConnectionErrorModal,
|
||||
useToggleWalletDropdown,
|
||||
useToggleWalletModal,
|
||||
} from '../../state/application/hooks'
|
||||
@@ -35,7 +34,6 @@ import StatusIcon from '../Identicon/StatusIcon'
|
||||
import Loader from '../Loader'
|
||||
import { RowBetween } from '../Row'
|
||||
import WalletModal from '../WalletModal'
|
||||
import MetamaskConnectionError from './MetamaskConnectionError'
|
||||
|
||||
// https://stackoverflow.com/a/31617326
|
||||
const FULL_BORDER_RADIUS = 9999
|
||||
@@ -213,16 +211,10 @@ function Web3StatusInner() {
|
||||
toggleWalletDropdown()
|
||||
}, [toggleWalletDropdown])
|
||||
const toggleWalletModal = useToggleWalletModal()
|
||||
const openMetamaskConnectionErrorModal = useOpenMetamaskConnectionErrorModal()
|
||||
const walletIsOpen = useModalIsOpen(ApplicationModal.WALLET_DROPDOWN)
|
||||
const isClaimAvailable = useIsNftClaimAvailable((state) => state.isClaimAvailable)
|
||||
|
||||
const error = useAppSelector((state) => state.connection.errorByConnectionType[connectionType])
|
||||
useEffect(() => {
|
||||
if (getIsMetaMask(connectionType) && error) {
|
||||
openMetamaskConnectionErrorModal()
|
||||
}
|
||||
}, [error, connectionType, openMetamaskConnectionErrorModal])
|
||||
|
||||
const allTransactions = useAllTransactions()
|
||||
|
||||
@@ -326,7 +318,6 @@ export default function Web3Status() {
|
||||
<Web3StatusInner />
|
||||
<FiatOnrampAnnouncement />
|
||||
<WalletModal ENSName={ENSName ?? undefined} pendingTransactions={pending} confirmedTransactions={confirmed} />
|
||||
<MetamaskConnectionError />
|
||||
<Portal>
|
||||
<span ref={walletRef}>
|
||||
<WalletDropdown />
|
||||
|
||||
@@ -26,16 +26,17 @@ import {
|
||||
getTokenAddress,
|
||||
} from 'lib/utils/analytics'
|
||||
import { useCallback, useState } from 'react'
|
||||
import { useToggleWalletModal } from 'state/application/hooks'
|
||||
import { useIsDarkMode } from 'state/user/hooks'
|
||||
import { computeRealizedPriceImpact } from 'utils/prices'
|
||||
import { switchChain } from 'utils/switchChain'
|
||||
|
||||
import { useSyncWidgetInputs } from './inputs'
|
||||
import { DefaultTokens, useSyncWidgetInputs } from './inputs'
|
||||
import { useSyncWidgetSettings } from './settings'
|
||||
import { DARK_THEME, LIGHT_THEME } from './theme'
|
||||
import { useSyncWidgetTransactions } from './transactions'
|
||||
|
||||
export const WIDGET_WIDTH = 360
|
||||
export const DEFAULT_WIDGET_WIDTH = 360
|
||||
|
||||
const WIDGET_ROUTER_URL = 'https://api.uniswap.org/v1/'
|
||||
|
||||
@@ -44,19 +45,34 @@ function useWidgetTheme() {
|
||||
}
|
||||
|
||||
interface WidgetProps {
|
||||
token?: Currency
|
||||
onTokenChange?: (token: Currency) => void
|
||||
defaultTokens: DefaultTokens
|
||||
width?: number | string
|
||||
onDefaultTokenChange?: (token: Currency) => void
|
||||
onReviewSwapClick?: OnReviewSwapClick
|
||||
}
|
||||
|
||||
export default function Widget({ token, onTokenChange, onReviewSwapClick }: WidgetProps) {
|
||||
export default function Widget({
|
||||
defaultTokens,
|
||||
width = DEFAULT_WIDGET_WIDTH,
|
||||
onDefaultTokenChange,
|
||||
onReviewSwapClick,
|
||||
}: WidgetProps) {
|
||||
const { connector, provider } = useWeb3React()
|
||||
const locale = useActiveLocale()
|
||||
const theme = useWidgetTheme()
|
||||
const { inputs, tokenSelector } = useSyncWidgetInputs({ token, onTokenChange })
|
||||
const { inputs, tokenSelector } = useSyncWidgetInputs({
|
||||
defaultTokens,
|
||||
onDefaultTokenChange,
|
||||
})
|
||||
const { settings } = useSyncWidgetSettings()
|
||||
const { transactions } = useSyncWidgetTransactions()
|
||||
|
||||
const toggleWalletModal = useToggleWalletModal()
|
||||
const onConnectWalletClick = useCallback(() => {
|
||||
toggleWalletModal()
|
||||
return false // prevents the in-widget wallet modal from opening
|
||||
}, [toggleWalletModal])
|
||||
|
||||
const onSwitchChain = useCallback(
|
||||
// TODO(WEB-1757): Widget should not break if this rejects - upstream the catch to ignore it.
|
||||
({ chainId }: AddEthereumChainParameter) => switchChain(connector, Number(chainId)).catch(() => undefined),
|
||||
@@ -152,8 +168,9 @@ export default function Widget({ token, onTokenChange, onReviewSwapClick }: Widg
|
||||
routerUrl={WIDGET_ROUTER_URL}
|
||||
locale={locale}
|
||||
theme={theme}
|
||||
width={WIDGET_WIDTH}
|
||||
width={width}
|
||||
// defaultChainId is excluded - it is always inferred from the passed provider
|
||||
onConnectWalletClick={onConnectWalletClick}
|
||||
provider={provider}
|
||||
onSwitchChain={onSwitchChain}
|
||||
tokenList={EMPTY_TOKEN_LIST} // prevents loading the default token list, as we use our own token selector UI
|
||||
@@ -172,7 +189,7 @@ export default function Widget({ token, onTokenChange, onReviewSwapClick }: Widg
|
||||
)
|
||||
}
|
||||
|
||||
export function WidgetSkeleton() {
|
||||
export function WidgetSkeleton({ width = DEFAULT_WIDGET_WIDTH }: { width?: number | string }) {
|
||||
const theme = useWidgetTheme()
|
||||
return <SwapWidgetSkeleton theme={theme} width={WIDGET_WIDTH} />
|
||||
return <SwapWidgetSkeleton theme={theme} width={width} />
|
||||
}
|
||||
|
||||
@@ -8,10 +8,11 @@ const EMPTY_AMOUNT = ''
|
||||
|
||||
type SwapValue = Required<SwapController>['value']
|
||||
type SwapTokens = Pick<SwapValue, Field.INPUT | Field.OUTPUT> & { default?: Currency }
|
||||
export type DefaultTokens = Partial<SwapTokens>
|
||||
|
||||
function includesDefaultToken(tokens: SwapTokens) {
|
||||
if (!tokens.default) return true
|
||||
return tokens[Field.INPUT]?.equals(tokens.default) || tokens[Field.OUTPUT]?.equals(tokens.default)
|
||||
function missingDefaultToken(tokens: SwapTokens) {
|
||||
if (!tokens.default) return false
|
||||
return !tokens[Field.INPUT]?.equals(tokens.default) && !tokens[Field.OUTPUT]?.equals(tokens.default)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -20,27 +21,31 @@ function includesDefaultToken(tokens: SwapTokens) {
|
||||
* Enforces that token is a part of the returned value.
|
||||
*/
|
||||
export function useSyncWidgetInputs({
|
||||
token,
|
||||
onTokenChange,
|
||||
defaultTokens,
|
||||
onDefaultTokenChange,
|
||||
}: {
|
||||
token?: Currency
|
||||
onTokenChange?: (token: Currency) => void
|
||||
defaultTokens: DefaultTokens
|
||||
onDefaultTokenChange?: (token: Currency) => void
|
||||
}) {
|
||||
const trace = useTrace({ section: InterfaceSectionName.WIDGET })
|
||||
|
||||
const [type, setType] = useState<SwapValue['type']>(TradeType.EXACT_INPUT)
|
||||
const [amount, setAmount] = useState<SwapValue['amount']>(EMPTY_AMOUNT)
|
||||
const [tokens, setTokens] = useState<SwapTokens>({ [Field.OUTPUT]: token, default: token })
|
||||
const [tokens, setTokens] = useState<SwapTokens>(defaultTokens)
|
||||
|
||||
useEffect(() => {
|
||||
setTokens((tokens) => {
|
||||
const update = { ...tokens, default: token }
|
||||
if (!includesDefaultToken(update)) {
|
||||
return { [Field.OUTPUT]: update.default, default: update.default }
|
||||
}
|
||||
return update
|
||||
})
|
||||
}, [token])
|
||||
if (!tokens[Field.INPUT] && !tokens[Field.OUTPUT]) {
|
||||
setTokens((tokens) => {
|
||||
const update = {
|
||||
...tokens,
|
||||
[Field.INPUT]: defaultTokens[Field.INPUT] ?? tokens[Field.INPUT],
|
||||
[Field.OUTPUT]: defaultTokens[Field.OUTPUT] ?? tokens[Field.OUTPUT] ?? defaultTokens.default,
|
||||
default: defaultTokens.default,
|
||||
}
|
||||
return update
|
||||
})
|
||||
}
|
||||
}, [defaultTokens, tokens])
|
||||
|
||||
const onAmountChange = useCallback(
|
||||
(field: Field, amount: string, origin?: 'max') => {
|
||||
@@ -96,13 +101,14 @@ export function useSyncWidgetInputs({
|
||||
return type
|
||||
})
|
||||
|
||||
if (!includesDefaultToken(update)) {
|
||||
onTokenChange?.(update[Field.OUTPUT] || selectingToken)
|
||||
if (missingDefaultToken(update)) {
|
||||
onDefaultTokenChange?.(update[Field.OUTPUT] ?? selectingToken)
|
||||
}
|
||||
setTokens(update)
|
||||
},
|
||||
[onTokenChange, selectingField, tokens]
|
||||
[onDefaultTokenChange, selectingField, tokens]
|
||||
)
|
||||
|
||||
const tokenSelector = (
|
||||
<CurrencySearchModal
|
||||
isOpen={selectingField !== undefined}
|
||||
@@ -110,6 +116,7 @@ export function useSyncWidgetInputs({
|
||||
selectedCurrency={selectingField && tokens[selectingField]}
|
||||
otherSelectedCurrency={selectingField && tokens[invertField(selectingField)]}
|
||||
onCurrencySelect={onTokenSelect}
|
||||
showCommonBases
|
||||
/>
|
||||
)
|
||||
|
||||
@@ -117,11 +124,11 @@ export function useSyncWidgetInputs({
|
||||
() => ({
|
||||
type,
|
||||
amount,
|
||||
// If the default has not yet been handled, preemptively disable the widget by passing no tokens. Effectively,
|
||||
// If the initial state has not yet been set, preemptively disable the widget by passing no tokens. Effectively,
|
||||
// this resets the widget - avoiding rendering stale state - because with no tokens the skeleton will be rendered.
|
||||
...(token && tokens.default?.equals(token) ? tokens : undefined),
|
||||
...(tokens[Field.INPUT] || tokens[Field.OUTPUT] ? tokens : undefined),
|
||||
}),
|
||||
[amount, token, tokens, type]
|
||||
[amount, tokens, type]
|
||||
)
|
||||
const valueHandlers: SwapEventHandlers = useMemo(
|
||||
() => ({ onAmountChange, onSwitchTokens, onTokenSelectorClick }),
|
||||
|
||||
@@ -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,18 +12,21 @@ export function getIsInjected(): boolean {
|
||||
return Boolean(window.ethereum)
|
||||
}
|
||||
|
||||
export function getHasMetaMaskExtensionInstalled(): boolean {
|
||||
return window.ethereum?.isMetaMask ?? false
|
||||
export function getIsBraveWallet(): boolean {
|
||||
return window.ethereum?.isBraveWallet ?? false
|
||||
}
|
||||
|
||||
export function getHasCoinbaseExtensionInstalled(): boolean {
|
||||
export function getIsMetaMaskWallet(): boolean {
|
||||
// When using Brave browser, `isMetaMask` is set to true when using the built-in wallet
|
||||
// This function should return true only when using the MetaMask extension
|
||||
// https://wallet-docs.brave.com/ethereum/wallet-detection#compatability-with-metamask
|
||||
return (window.ethereum?.isMetaMask ?? false) && !getIsBraveWallet()
|
||||
}
|
||||
|
||||
export function getIsCoinbaseWallet(): boolean {
|
||||
return window.ethereum?.isCoinbaseWallet ?? false
|
||||
}
|
||||
|
||||
export function getIsMetaMask(connectionType: ConnectionType): boolean {
|
||||
return connectionType === ConnectionType.INJECTED && getHasMetaMaskExtensionInstalled()
|
||||
}
|
||||
|
||||
const CONNECTIONS = [
|
||||
gnosisSafeConnection,
|
||||
injectedConnection,
|
||||
@@ -56,7 +59,7 @@ export function getConnection(c: Connector | ConnectionType) {
|
||||
|
||||
export function getConnectionName(
|
||||
connectionType: ConnectionType,
|
||||
hasMetaMaskExtension: boolean = getHasMetaMaskExtensionInstalled()
|
||||
hasMetaMaskExtension: boolean = getIsMetaMaskWallet()
|
||||
) {
|
||||
switch (connectionType) {
|
||||
case ConnectionType.INJECTED:
|
||||
|
||||
@@ -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 } = {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
export const UNI_LIST = 'https://tokens.uniswap.org'
|
||||
export const UNI_EXTENDED_LIST = 'https://extendedtokens.uniswap.org/'
|
||||
const UNI_UNSUPPORTED_LIST = 'https://unsupportedtokens.uniswap.org/'
|
||||
export const UNI_LIST = 'https://gateway.ipfs.io/ipns/tokens.uniswap.org'
|
||||
export const UNI_EXTENDED_LIST = 'https://gateway.ipfs.io/ipns/extendedtokens.uniswap.org'
|
||||
const UNI_UNSUPPORTED_LIST = 'https://gateway.ipfs.io/ipns/unsupportedtokens.uniswap.org'
|
||||
const AAVE_LIST = 'tokenlist.aave.eth'
|
||||
const BA_LIST = 'https://raw.githubusercontent.com/The-Blockchain-Association/sec-notice-list/master/ba-sec-list.json'
|
||||
const CMC_ALL_LIST = 'https://api.coinmarketcap.com/data-api/v3/uniswap/all.json'
|
||||
@@ -8,7 +8,6 @@ const COINGECKO_LIST = 'https://tokens.coingecko.com/uniswap/all.json'
|
||||
const COMPOUND_LIST = 'https://raw.githubusercontent.com/compound-finance/token-list/master/compound.tokenlist.json'
|
||||
const GEMINI_LIST = 'https://www.gemini.com/uniswap/manifest.json'
|
||||
const KLEROS_LIST = 't2crtokens.eth'
|
||||
const ROLL_LIST = 'https://app.tryroll.com/tokens.json'
|
||||
const SET_LIST = 'https://raw.githubusercontent.com/SetProtocol/uniswap-tokenlist/main/set.tokenlist.json'
|
||||
const WRAPPED_LIST = 'wrapped.tokensoft.eth'
|
||||
|
||||
@@ -30,7 +29,6 @@ export const DEFAULT_INACTIVE_LIST_URLS: string[] = [
|
||||
GEMINI_LIST,
|
||||
WRAPPED_LIST,
|
||||
SET_LIST,
|
||||
ROLL_LIST,
|
||||
ARBITRUM_LIST,
|
||||
OPTIMISM_LIST,
|
||||
CELO_LIST,
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { Plural, Trans } from '@lingui/macro'
|
||||
import { TokenStandard } from 'graphql/data/__generated__/types-and-hooks'
|
||||
import { SearchToken } from 'graphql/data/SearchTokens'
|
||||
|
||||
import { ZERO_ADDRESS } from './misc'
|
||||
import { NATIVE_CHAIN_ID } from './tokens'
|
||||
@@ -94,3 +96,11 @@ export function checkWarning(tokenAddress: string) {
|
||||
return BlockedWarning
|
||||
}
|
||||
}
|
||||
|
||||
// TODO(cartcrom): Replace all usage of WARNING_LEVEL with SafetyLevel
|
||||
export function checkSearchTokenWarning(token: SearchToken) {
|
||||
if (!token.address) {
|
||||
return token.standard === TokenStandard.Native ? null : StrongWarning
|
||||
}
|
||||
return checkWarning(token.address)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
export enum FeatureFlag {
|
||||
fiatOnramp = 'fiatOnramp',
|
||||
traceJsonRpc = 'traceJsonRpc',
|
||||
permit2 = 'permit2',
|
||||
nftListV2 = 'nftListV2',
|
||||
payWithAnyToken = 'payWithAnyToken',
|
||||
swapWidget = 'swapWidget',
|
||||
gqlRouting = 'gqlRouting',
|
||||
}
|
||||
|
||||
export enum FeatureGate {
|
||||
DUMMY = 'web_dummy_gate_amplitude_id',
|
||||
}
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
import { BaseVariant } from '../index'
|
||||
|
||||
export function useFiatOnrampFlag(): BaseVariant {
|
||||
return BaseVariant.Enabled
|
||||
// return useBaseFlag(FeatureFlag.fiatOnramp)
|
||||
}
|
||||
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 }
|
||||
@@ -1,7 +1,7 @@
|
||||
import { BaseVariant, FeatureFlag, useBaseFlag } from '../index'
|
||||
import { BaseVariant } from '../index'
|
||||
|
||||
export function useNftListV2Flag(): BaseVariant {
|
||||
return useBaseFlag(FeatureFlag.nftListV2)
|
||||
return BaseVariant.Enabled
|
||||
}
|
||||
|
||||
export { BaseVariant as NftListV2Variant }
|
||||
|
||||
24
src/featureFlags/flags/payWithAnyToken.ts
Normal file
24
src/featureFlags/flags/payWithAnyToken.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { UNIVERSAL_ROUTER_ADDRESS } from '@uniswap/universal-router-sdk'
|
||||
import { useWeb3React } from '@web3-react/core'
|
||||
|
||||
import { BaseVariant, FeatureFlag, useBaseFlag } from '../index'
|
||||
|
||||
export function usePayWithAnyTokenFlag(): BaseVariant {
|
||||
return useBaseFlag(FeatureFlag.payWithAnyToken)
|
||||
}
|
||||
|
||||
export function usePayWithAnyTokenEnabled(): boolean {
|
||||
const flagEnabled = usePayWithAnyTokenFlag() === BaseVariant.Enabled
|
||||
const { chainId } = useWeb3React()
|
||||
try {
|
||||
// Detect if the Universal Router is not yet deployed to chainId.
|
||||
// This is necessary so that we can fallback correctly on chains without a Universal Router deployment.
|
||||
// It will be removed once Universal Router is deployed on all supported chains.
|
||||
chainId && UNIVERSAL_ROUTER_ADDRESS(chainId)
|
||||
return flagEnabled
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
export { BaseVariant as PayWithAnyTokenVariant }
|
||||
@@ -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 }
|
||||
59
src/graphql/data/RecentlySearched.ts
Normal file
59
src/graphql/data/RecentlySearched.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import gql from 'graphql-tag'
|
||||
|
||||
gql`
|
||||
query RecentlySearchedAssets($collectionAddresses: [String!]!, $contracts: [ContractInput!]!) {
|
||||
nftCollections(filter: { addresses: $collectionAddresses }) {
|
||||
edges {
|
||||
node {
|
||||
collectionId
|
||||
image {
|
||||
url
|
||||
}
|
||||
isVerified
|
||||
name
|
||||
numAssets
|
||||
nftContracts {
|
||||
address
|
||||
}
|
||||
markets(currencies: ETH) {
|
||||
floorPrice {
|
||||
currency
|
||||
value
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
tokens(contracts: $contracts) {
|
||||
id
|
||||
decimals
|
||||
name
|
||||
chain
|
||||
standard
|
||||
address
|
||||
symbol
|
||||
market(currency: USD) {
|
||||
id
|
||||
price {
|
||||
id
|
||||
value
|
||||
currency
|
||||
}
|
||||
pricePercentChange(duration: DAY) {
|
||||
id
|
||||
value
|
||||
}
|
||||
volume24H: volume(duration: DAY) {
|
||||
id
|
||||
value
|
||||
currency
|
||||
}
|
||||
}
|
||||
project {
|
||||
id
|
||||
logoUrl
|
||||
safetyLevel
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
102
src/graphql/data/SearchTokens.ts
Normal file
102
src/graphql/data/SearchTokens.ts
Normal file
@@ -0,0 +1,102 @@
|
||||
import { WRAPPED_NATIVE_CURRENCY } from 'constants/tokens'
|
||||
import gql from 'graphql-tag'
|
||||
import { useMemo } from 'react'
|
||||
|
||||
import { Chain, SearchTokensQuery, useSearchTokensQuery } from './__generated__/types-and-hooks'
|
||||
import { chainIdToBackendName } from './util'
|
||||
|
||||
gql`
|
||||
query SearchTokens($searchQuery: String!) {
|
||||
searchTokens(searchQuery: $searchQuery) {
|
||||
id
|
||||
decimals
|
||||
name
|
||||
chain
|
||||
standard
|
||||
address
|
||||
symbol
|
||||
market(currency: USD) {
|
||||
id
|
||||
price {
|
||||
id
|
||||
value
|
||||
currency
|
||||
}
|
||||
pricePercentChange(duration: DAY) {
|
||||
id
|
||||
value
|
||||
}
|
||||
volume24H: volume(duration: DAY) {
|
||||
id
|
||||
value
|
||||
currency
|
||||
}
|
||||
}
|
||||
project {
|
||||
id
|
||||
logoUrl
|
||||
safetyLevel
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export type SearchToken = NonNullable<NonNullable<SearchTokensQuery['searchTokens']>[number]>
|
||||
|
||||
function isMoreRevelantToken(current: SearchToken, existing: SearchToken | undefined, searchChain: Chain) {
|
||||
if (!existing) return true
|
||||
|
||||
// Always priotize natives, and if both tokens are native, prefer native on current chain (i.e. Matic on Polygon over Matic on Mainnet )
|
||||
if (current.standard === 'NATIVE' && (existing.standard !== 'NATIVE' || current.chain === searchChain)) return true
|
||||
|
||||
// Prefer tokens on the searched chain, otherwise prefer mainnet tokens
|
||||
return current.chain === searchChain || (existing.chain !== searchChain && current.chain === Chain.Ethereum)
|
||||
}
|
||||
|
||||
// Places natives first, wrapped native on current chain next, then sorts by volume
|
||||
function searchTokenSortFunction(
|
||||
searchChain: Chain,
|
||||
wrappedNativeAddress: string | undefined,
|
||||
a: SearchToken,
|
||||
b: SearchToken
|
||||
) {
|
||||
if (a.standard === 'NATIVE') {
|
||||
if (b.standard === 'NATIVE') {
|
||||
if (a.chain === searchChain) return -1
|
||||
else if (b.chain === searchChain) return 1
|
||||
else return 0
|
||||
} else return -1
|
||||
} else if (b.standard === 'NATIVE') return 1
|
||||
else if (wrappedNativeAddress && a.address === wrappedNativeAddress) return -1
|
||||
else if (wrappedNativeAddress && b.address === wrappedNativeAddress) return 1
|
||||
else return (b.market?.volume24H?.value ?? 0) - (a.market?.volume24H?.value ?? 0)
|
||||
}
|
||||
|
||||
export function useSearchTokens(searchQuery: string, chainId: number) {
|
||||
const { data, loading, error } = useSearchTokensQuery({
|
||||
variables: {
|
||||
searchQuery,
|
||||
},
|
||||
})
|
||||
|
||||
const sortedTokens = useMemo(() => {
|
||||
const searchChain = chainIdToBackendName(chainId)
|
||||
// Stores results, allowing overwriting cross-chain tokens w/ more 'relevant token'
|
||||
const selectionMap: { [projectId: string]: SearchToken } = {}
|
||||
data?.searchTokens?.forEach((token) => {
|
||||
if (token.project?.id) {
|
||||
const existing = selectionMap[token.project.id]
|
||||
if (isMoreRevelantToken(token, existing, searchChain)) selectionMap[token.project.id] = token
|
||||
}
|
||||
})
|
||||
return Object.values(selectionMap).sort(
|
||||
searchTokenSortFunction.bind(null, searchChain, WRAPPED_NATIVE_CURRENCY[chainId]?.address)
|
||||
)
|
||||
}, [data, chainId])
|
||||
|
||||
return {
|
||||
data: sortedTokens,
|
||||
loading,
|
||||
error,
|
||||
}
|
||||
}
|
||||
@@ -14,40 +14,49 @@ The difference between Token and TokenProject:
|
||||
TokenProjectMarket is aggregated market data (aggregated over multiple dexes and centralized exchanges) that we get from coingecko.
|
||||
*/
|
||||
gql`
|
||||
query Token($contract: ContractInput!) {
|
||||
tokens(contracts: [$contract]) {
|
||||
query Token($chain: Chain!, $address: String = null) {
|
||||
token(chain: $chain, address: $address) {
|
||||
id
|
||||
decimals
|
||||
name
|
||||
chain
|
||||
address
|
||||
symbol
|
||||
standard
|
||||
market(currency: USD) {
|
||||
id
|
||||
totalValueLocked {
|
||||
id
|
||||
value
|
||||
currency
|
||||
}
|
||||
price {
|
||||
id
|
||||
value
|
||||
currency
|
||||
}
|
||||
volume24H: volume(duration: DAY) {
|
||||
id
|
||||
value
|
||||
currency
|
||||
}
|
||||
priceHigh52W: priceHighLow(duration: YEAR, highLow: HIGH) {
|
||||
id
|
||||
value
|
||||
}
|
||||
priceLow52W: priceHighLow(duration: YEAR, highLow: LOW) {
|
||||
id
|
||||
value
|
||||
}
|
||||
}
|
||||
project {
|
||||
id
|
||||
description
|
||||
homepageUrl
|
||||
twitterName
|
||||
logoUrl
|
||||
tokens {
|
||||
id
|
||||
chain
|
||||
address
|
||||
}
|
||||
@@ -58,7 +67,7 @@ gql`
|
||||
|
||||
export type { Chain, TokenQuery } from './__generated__/types-and-hooks'
|
||||
|
||||
export type TokenQueryData = NonNullable<TokenQuery['tokens']>[number]
|
||||
export type TokenQueryData = TokenQuery['token']
|
||||
|
||||
// TODO: Return a QueryToken from useTokenQuery instead of TokenQueryData to make it more usable in Currency-centric interfaces.
|
||||
export class QueryToken extends WrappedTokenInfo {
|
||||
|
||||
@@ -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!) {
|
||||
@@ -25,25 +33,32 @@ gql`
|
||||
chain
|
||||
address
|
||||
symbol
|
||||
standard
|
||||
market(currency: USD) {
|
||||
id
|
||||
totalValueLocked {
|
||||
id
|
||||
value
|
||||
currency
|
||||
}
|
||||
price {
|
||||
id
|
||||
value
|
||||
currency
|
||||
}
|
||||
pricePercentChange(duration: $duration) {
|
||||
id
|
||||
currency
|
||||
value
|
||||
}
|
||||
volume(duration: $duration) {
|
||||
id
|
||||
value
|
||||
currency
|
||||
}
|
||||
}
|
||||
project {
|
||||
id
|
||||
logoUrl
|
||||
}
|
||||
}
|
||||
@@ -53,9 +68,13 @@ gql`
|
||||
gql`
|
||||
query TopTokensSparkline($duration: HistoryDuration!, $chain: Chain!) {
|
||||
topTokens(pageSize: 100, page: 1, chain: $chain) {
|
||||
id
|
||||
address
|
||||
chain
|
||||
market(currency: USD) {
|
||||
id
|
||||
priceHistory(duration: $duration) {
|
||||
id
|
||||
timestamp
|
||||
value
|
||||
}
|
||||
@@ -64,11 +83,12 @@ gql`
|
||||
}
|
||||
`
|
||||
|
||||
function useSortedTokens(tokens: NonNullable<TopTokens100Query['topTokens']>) {
|
||||
function useSortedTokens(tokens: TopTokens100Query['topTokens']) {
|
||||
const sortMethod = useAtomValue(sortMethodAtom)
|
||||
const sortAscending = useAtomValue(sortAscendingAtom)
|
||||
|
||||
return useMemo(() => {
|
||||
if (!tokens) return undefined
|
||||
let tokenArray = Array.from(tokens)
|
||||
switch (sortMethod) {
|
||||
case TokenSortMethod.PRICE:
|
||||
@@ -93,12 +113,13 @@ function useSortedTokens(tokens: NonNullable<TopTokens100Query['topTokens']>) {
|
||||
}, [tokens, sortMethod, sortAscending])
|
||||
}
|
||||
|
||||
function useFilteredTokens(tokens: NonNullable<TopTokens100Query['topTokens']>) {
|
||||
function useFilteredTokens(tokens: TopTokens100Query['topTokens']) {
|
||||
const filterString = useAtomValue(filterStringAtom)
|
||||
|
||||
const lowercaseFilterString = useMemo(() => filterString.toLowerCase(), [filterString])
|
||||
|
||||
return useMemo(() => {
|
||||
if (!tokens) return undefined
|
||||
let returnTokens = tokens
|
||||
if (lowercaseFilterString) {
|
||||
returnTokens = returnTokens?.filter((token) => {
|
||||
@@ -119,6 +140,7 @@ export type TopToken = NonNullable<NonNullable<TopTokens100Query>['topTokens']>[
|
||||
|
||||
interface UseTopTokensReturnValue {
|
||||
tokens: TopToken[] | undefined
|
||||
tokenSortRank: Record<string, number>
|
||||
loadingTokens: boolean
|
||||
sparklines: SparklineMap
|
||||
}
|
||||
@@ -127,9 +149,12 @@ export function useTopTokens(chain: Chain): UseTopTokensReturnValue {
|
||||
const chainId = CHAIN_NAME_TO_CHAIN_ID[chain]
|
||||
const duration = toHistoryDuration(useAtomValue(filterTimeAtom))
|
||||
|
||||
const { data: sparklineQuery } = useTopTokensSparklineQuery({
|
||||
variables: { duration, chain },
|
||||
})
|
||||
const { data: sparklineQuery } = usePollQueryWhileMounted(
|
||||
useTopTokensSparklineQuery({
|
||||
variables: { duration, chain },
|
||||
}),
|
||||
PollingInterval.Slow
|
||||
)
|
||||
|
||||
const sparklines = useMemo(() => {
|
||||
const unwrappedTokens = sparklineQuery?.topTokens?.map((topToken) => unwrapToken(chainId, topToken))
|
||||
@@ -140,14 +165,29 @@ export function useTopTokens(chain: Chain): UseTopTokensReturnValue {
|
||||
return map
|
||||
}, [chainId, sparklineQuery?.topTokens])
|
||||
|
||||
const { data, loading: loadingTokens } = useTopTokens100Query({
|
||||
variables: { duration, chain },
|
||||
})
|
||||
const mappedTokens = useMemo(
|
||||
() => data?.topTokens?.map((token) => unwrapToken(chainId, token)) ?? [],
|
||||
[chainId, data]
|
||||
const { data, loading: loadingTokens } = usePollQueryWhileMounted(
|
||||
useTopTokens100Query({
|
||||
variables: { duration, chain },
|
||||
}),
|
||||
PollingInterval.Fast
|
||||
)
|
||||
|
||||
const unwrappedTokens = useMemo(() => data?.topTokens?.map((token) => unwrapToken(chainId, token)), [chainId, data])
|
||||
const sortedTokens = useSortedTokens(unwrappedTokens)
|
||||
const tokenSortRank = useMemo(
|
||||
() =>
|
||||
sortedTokens?.reduce((acc, cur, i) => {
|
||||
if (!cur.address) return acc
|
||||
return {
|
||||
...acc,
|
||||
[cur.address]: i + 1,
|
||||
}
|
||||
}, {}) ?? {},
|
||||
[sortedTokens]
|
||||
)
|
||||
const filteredTokens = useFilteredTokens(sortedTokens)
|
||||
return useMemo(
|
||||
() => ({ tokens: filteredTokens, tokenSortRank, loadingTokens, sparklines }),
|
||||
[filteredTokens, tokenSortRank, loadingTokens, sparklines]
|
||||
)
|
||||
const filteredTokens = useFilteredTokens(mappedTokens)
|
||||
const sortedTokens = useSortedTokens(filteredTokens)
|
||||
return useMemo(() => ({ tokens: sortedTokens, loadingTokens, sparklines }), [loadingTokens, sortedTokens, sparklines])
|
||||
}
|
||||
|
||||
51
src/graphql/data/TrendingTokens.ts
Normal file
51
src/graphql/data/TrendingTokens.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import gql from 'graphql-tag'
|
||||
import { useMemo } from 'react'
|
||||
|
||||
import { useTrendingTokensQuery } from './__generated__/types-and-hooks'
|
||||
import { chainIdToBackendName, unwrapToken } from './util'
|
||||
|
||||
gql`
|
||||
query TrendingTokens($chain: Chain!) {
|
||||
topTokens(pageSize: 4, page: 1, chain: $chain, orderBy: VOLUME) {
|
||||
id
|
||||
decimals
|
||||
name
|
||||
chain
|
||||
standard
|
||||
address
|
||||
symbol
|
||||
market(currency: USD) {
|
||||
id
|
||||
price {
|
||||
id
|
||||
value
|
||||
currency
|
||||
}
|
||||
pricePercentChange(duration: DAY) {
|
||||
id
|
||||
value
|
||||
}
|
||||
volume24H: volume(duration: DAY) {
|
||||
id
|
||||
value
|
||||
currency
|
||||
}
|
||||
}
|
||||
project {
|
||||
id
|
||||
logoUrl
|
||||
safetyLevel
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export default function useTrendingTokens(chainId?: number) {
|
||||
const chain = chainIdToBackendName(chainId)
|
||||
const { data, loading } = useTrendingTokensQuery({ variables: { chain } })
|
||||
|
||||
return useMemo(
|
||||
() => ({ data: data?.topTokens?.map((token) => unwrapToken(chainId ?? 1, token)), loading }),
|
||||
[chainId, data?.topTokens, loading]
|
||||
)
|
||||
}
|
||||
573
src/graphql/data/__generated__/types-and-hooks.ts
generated
573
src/graphql/data/__generated__/types-and-hooks.ts
generated
@@ -82,7 +82,8 @@ export enum Chain {
|
||||
Ethereum = 'ETHEREUM',
|
||||
EthereumGoerli = 'ETHEREUM_GOERLI',
|
||||
Optimism = 'OPTIMISM',
|
||||
Polygon = 'POLYGON'
|
||||
Polygon = 'POLYGON',
|
||||
UnknownChain = 'UNKNOWN_CHAIN'
|
||||
}
|
||||
|
||||
export type ContractInput = {
|
||||
@@ -139,6 +140,49 @@ export enum MarketSortableField {
|
||||
Volume = 'VOLUME'
|
||||
}
|
||||
|
||||
export type NftActivity = {
|
||||
__typename?: 'NftActivity';
|
||||
address: Scalars['String'];
|
||||
asset?: Maybe<NftAsset>;
|
||||
fromAddress: Scalars['String'];
|
||||
id: Scalars['ID'];
|
||||
marketplace?: Maybe<NftMarketplace>;
|
||||
orderStatus?: Maybe<OrderStatus>;
|
||||
price?: Maybe<Amount>;
|
||||
quantity?: Maybe<Scalars['Int']>;
|
||||
timestamp: Scalars['Int'];
|
||||
toAddress?: Maybe<Scalars['String']>;
|
||||
tokenId?: Maybe<Scalars['String']>;
|
||||
transactionHash?: Maybe<Scalars['String']>;
|
||||
type: NftActivityType;
|
||||
url?: Maybe<Scalars['String']>;
|
||||
};
|
||||
|
||||
export type NftActivityConnection = {
|
||||
__typename?: 'NftActivityConnection';
|
||||
edges: Array<NftActivityEdge>;
|
||||
pageInfo: PageInfo;
|
||||
};
|
||||
|
||||
export type NftActivityEdge = {
|
||||
__typename?: 'NftActivityEdge';
|
||||
cursor: Scalars['String'];
|
||||
node: NftActivity;
|
||||
};
|
||||
|
||||
export type NftActivityFilterInput = {
|
||||
activityTypes?: InputMaybe<Array<NftActivityType>>;
|
||||
address?: InputMaybe<Scalars['String']>;
|
||||
tokenId?: InputMaybe<Scalars['String']>;
|
||||
};
|
||||
|
||||
export enum NftActivityType {
|
||||
CancelListing = 'CANCEL_LISTING',
|
||||
Listing = 'LISTING',
|
||||
Sale = 'SALE',
|
||||
Transfer = 'TRANSFER'
|
||||
}
|
||||
|
||||
export type NftApproval = {
|
||||
__typename?: 'NftApproval';
|
||||
approvedAddress: Scalars['String'];
|
||||
@@ -386,6 +430,7 @@ export type NftCollectionTraitStats = {
|
||||
|
||||
export type NftCollectionsFilterInput = {
|
||||
addresses?: InputMaybe<Array<Scalars['String']>>;
|
||||
nameQuery?: InputMaybe<Scalars['String']>;
|
||||
};
|
||||
|
||||
export type NftContract = IContract & {
|
||||
@@ -468,12 +513,45 @@ export enum NftRarityProvider {
|
||||
RaritySniper = 'RARITY_SNIPER'
|
||||
}
|
||||
|
||||
export type NftRouteResponse = {
|
||||
__typename?: 'NftRouteResponse';
|
||||
calldata: Scalars['String'];
|
||||
id: Scalars['ID'];
|
||||
route?: Maybe<Array<NftTrade>>;
|
||||
sendAmount: TokenAmount;
|
||||
toAddress: Scalars['String'];
|
||||
};
|
||||
|
||||
export enum NftStandard {
|
||||
Erc721 = 'ERC721',
|
||||
Erc1155 = 'ERC1155',
|
||||
Noncompliant = 'NONCOMPLIANT'
|
||||
}
|
||||
|
||||
export type NftTrade = {
|
||||
__typename?: 'NftTrade';
|
||||
amount: Scalars['Int'];
|
||||
contractAddress: Scalars['String'];
|
||||
id: Scalars['ID'];
|
||||
marketplace: NftMarketplace;
|
||||
/** price represents the current price of the NFT, which can be different from quotePrice */
|
||||
price: TokenAmount;
|
||||
/** quotePrice represents the last quoted price of the NFT */
|
||||
quotePrice?: Maybe<TokenAmount>;
|
||||
tokenId: Scalars['String'];
|
||||
tokenType: NftStandard;
|
||||
};
|
||||
|
||||
export type NftTradeInput = {
|
||||
amount: Scalars['Int'];
|
||||
contractAddress: Scalars['String'];
|
||||
id: Scalars['ID'];
|
||||
marketplace: NftMarketplace;
|
||||
quotePrice?: InputMaybe<TokenAmountInput>;
|
||||
tokenId: Scalars['String'];
|
||||
tokenType: NftStandard;
|
||||
};
|
||||
|
||||
export type NftTransfer = {
|
||||
__typename?: 'NftTransfer';
|
||||
asset: NftAsset;
|
||||
@@ -504,6 +582,36 @@ export type PageInfo = {
|
||||
startCursor?: Maybe<Scalars['String']>;
|
||||
};
|
||||
|
||||
/** v2 pool parameters as defined by https://github.com/Uniswap/v2-sdk/blob/main/src/entities/pair.ts */
|
||||
export type PairInput = {
|
||||
tokenAmountA: TokenAmountInput;
|
||||
tokenAmountB: TokenAmountInput;
|
||||
};
|
||||
|
||||
export type PermitDetailsInput = {
|
||||
amount: Scalars['String'];
|
||||
expiration: Scalars['String'];
|
||||
nonce: Scalars['String'];
|
||||
token: Scalars['String'];
|
||||
};
|
||||
|
||||
export type PermitInput = {
|
||||
details: PermitDetailsInput;
|
||||
sigDeadline: Scalars['String'];
|
||||
signature: Scalars['String'];
|
||||
spender: Scalars['String'];
|
||||
};
|
||||
|
||||
/** v3 pool parameters as defined by https://github.com/Uniswap/v3-sdk/blob/main/src/entities/pool.ts */
|
||||
export type PoolInput = {
|
||||
fee: Scalars['Int'];
|
||||
liquidity: Scalars['String'];
|
||||
sqrtRatioX96: Scalars['String'];
|
||||
tickCurrent: Scalars['String'];
|
||||
tokenA: TokenInput;
|
||||
tokenB: TokenInput;
|
||||
};
|
||||
|
||||
export type Portfolio = {
|
||||
__typename?: 'Portfolio';
|
||||
assetActivities?: Maybe<Array<Maybe<AssetActivity>>>;
|
||||
@@ -514,11 +622,11 @@ export type Portfolio = {
|
||||
tokenBalances?: Maybe<Array<Maybe<TokenBalance>>>;
|
||||
tokensTotalDenominatedValue?: Maybe<Amount>;
|
||||
tokensTotalDenominatedValueChange?: Maybe<AmountChange>;
|
||||
tokensTotalDenominatedValueHistory?: Maybe<Array<Maybe<TimestampedAmount>>>;
|
||||
};
|
||||
|
||||
|
||||
export type PortfolioAssetActivitiesArgs = {
|
||||
includeOffChain?: InputMaybe<Scalars['Boolean']>;
|
||||
page?: InputMaybe<Scalars['Int']>;
|
||||
pageSize?: InputMaybe<Scalars['Int']>;
|
||||
};
|
||||
@@ -528,18 +636,14 @@ export type PortfolioTokensTotalDenominatedValueChangeArgs = {
|
||||
duration?: InputMaybe<HistoryDuration>;
|
||||
};
|
||||
|
||||
|
||||
export type PortfolioTokensTotalDenominatedValueHistoryArgs = {
|
||||
duration?: InputMaybe<HistoryDuration>;
|
||||
};
|
||||
|
||||
export type Query = {
|
||||
__typename?: 'Query';
|
||||
assetActivities?: Maybe<Array<Maybe<AssetActivity>>>;
|
||||
nftActivity?: Maybe<NftActivityConnection>;
|
||||
nftAssets?: Maybe<NftAssetConnection>;
|
||||
nftBalances?: Maybe<NftBalanceConnection>;
|
||||
nftCollections?: Maybe<NftCollectionConnection>;
|
||||
nftCollectionsById?: Maybe<Array<Maybe<NftCollection>>>;
|
||||
nftRoute?: Maybe<NftRouteResponse>;
|
||||
portfolios?: Maybe<Array<Maybe<Portfolio>>>;
|
||||
searchTokenProjects?: Maybe<Array<Maybe<TokenProject>>>;
|
||||
searchTokens?: Maybe<Array<Maybe<Token>>>;
|
||||
@@ -550,10 +654,11 @@ export type Query = {
|
||||
};
|
||||
|
||||
|
||||
export type QueryAssetActivitiesArgs = {
|
||||
address: Scalars['String'];
|
||||
page?: InputMaybe<Scalars['Int']>;
|
||||
pageSize?: InputMaybe<Scalars['Int']>;
|
||||
export type QueryNftActivityArgs = {
|
||||
chain?: InputMaybe<Chain>;
|
||||
cursor?: InputMaybe<Scalars['String']>;
|
||||
filter?: InputMaybe<NftActivityFilterInput>;
|
||||
limit?: InputMaybe<Scalars['Int']>;
|
||||
};
|
||||
|
||||
|
||||
@@ -574,19 +679,20 @@ export type QueryNftBalancesArgs = {
|
||||
after?: InputMaybe<Scalars['String']>;
|
||||
before?: InputMaybe<Scalars['String']>;
|
||||
chain?: InputMaybe<Chain>;
|
||||
cursor?: InputMaybe<Scalars['String']>;
|
||||
filter?: InputMaybe<NftBalancesFilterInput>;
|
||||
first?: InputMaybe<Scalars['Int']>;
|
||||
last?: InputMaybe<Scalars['Int']>;
|
||||
limit?: InputMaybe<Scalars['Int']>;
|
||||
ownerAddress: Scalars['String'];
|
||||
};
|
||||
|
||||
|
||||
export type QueryNftCollectionsArgs = {
|
||||
after?: InputMaybe<Scalars['String']>;
|
||||
before?: InputMaybe<Scalars['String']>;
|
||||
chain?: InputMaybe<Chain>;
|
||||
cursor?: InputMaybe<Scalars['String']>;
|
||||
filter?: InputMaybe<NftCollectionsFilterInput>;
|
||||
first?: InputMaybe<Scalars['Int']>;
|
||||
last?: InputMaybe<Scalars['Int']>;
|
||||
limit?: InputMaybe<Scalars['Int']>;
|
||||
};
|
||||
|
||||
|
||||
@@ -595,9 +701,16 @@ export type QueryNftCollectionsByIdArgs = {
|
||||
};
|
||||
|
||||
|
||||
export type QueryNftRouteArgs = {
|
||||
chain?: InputMaybe<Chain>;
|
||||
nftTrades: Array<NftTradeInput>;
|
||||
senderAddress: Scalars['String'];
|
||||
tokenTrades?: InputMaybe<Array<TokenTradeInput>>;
|
||||
};
|
||||
|
||||
|
||||
export type QueryPortfoliosArgs = {
|
||||
ownerAddresses: Array<Scalars['String']>;
|
||||
useAltDataSource?: InputMaybe<Scalars['Boolean']>;
|
||||
};
|
||||
|
||||
|
||||
@@ -667,6 +780,18 @@ export type TokenMarketArgs = {
|
||||
currency?: InputMaybe<Currency>;
|
||||
};
|
||||
|
||||
export type TokenAmount = {
|
||||
__typename?: 'TokenAmount';
|
||||
currency: Currency;
|
||||
id: Scalars['ID'];
|
||||
value: Scalars['String'];
|
||||
};
|
||||
|
||||
export type TokenAmountInput = {
|
||||
amount: Scalars['String'];
|
||||
token: TokenInput;
|
||||
};
|
||||
|
||||
export type TokenApproval = {
|
||||
__typename?: 'TokenApproval';
|
||||
approvedAddress: Scalars['String'];
|
||||
@@ -689,6 +814,13 @@ export type TokenBalance = {
|
||||
tokenProjectMarket?: Maybe<TokenProjectMarket>;
|
||||
};
|
||||
|
||||
export type TokenInput = {
|
||||
address: Scalars['String'];
|
||||
chainId: Scalars['Int'];
|
||||
decimals: Scalars['Int'];
|
||||
isNative: Scalars['Boolean'];
|
||||
};
|
||||
|
||||
export type TokenMarket = {
|
||||
__typename?: 'TokenMarket';
|
||||
id: Scalars['ID'];
|
||||
@@ -747,6 +879,7 @@ export type TokenProjectMarketsArgs = {
|
||||
export type TokenProjectMarket = {
|
||||
__typename?: 'TokenProjectMarket';
|
||||
currency: Currency;
|
||||
/** @deprecated Use marketCap */
|
||||
fullyDilutedMarketCap?: Maybe<Amount>;
|
||||
id: Scalars['ID'];
|
||||
marketCap?: Maybe<Amount>;
|
||||
@@ -754,9 +887,12 @@ export type TokenProjectMarket = {
|
||||
priceHighLow?: Maybe<Amount>;
|
||||
priceHistory?: Maybe<Array<Maybe<TimestampedAmount>>>;
|
||||
pricePercentChange?: Maybe<Amount>;
|
||||
/** @deprecated Use pricePercentChange */
|
||||
pricePercentChange24h?: Maybe<Amount>;
|
||||
tokenProject: TokenProject;
|
||||
/** @deprecated Use TokenMarket.volume for Uniswap volume */
|
||||
volume?: Maybe<Amount>;
|
||||
/** @deprecated Use TokenMarket.volume with duration DAY for Uniswap volume */
|
||||
volume24h?: Maybe<Amount>;
|
||||
};
|
||||
|
||||
@@ -794,6 +930,31 @@ export enum TokenStandard {
|
||||
Native = 'NATIVE'
|
||||
}
|
||||
|
||||
export type TokenTradeInput = {
|
||||
permit?: InputMaybe<PermitInput>;
|
||||
routes?: InputMaybe<TokenTradeRoutesInput>;
|
||||
slippageToleranceBasisPoints?: InputMaybe<Scalars['Int']>;
|
||||
tokenAmount: TokenAmountInput;
|
||||
};
|
||||
|
||||
export type TokenTradeRouteInput = {
|
||||
inputAmount: TokenAmountInput;
|
||||
outputAmount: TokenAmountInput;
|
||||
pools: Array<TradePoolInput>;
|
||||
};
|
||||
|
||||
export type TokenTradeRoutesInput = {
|
||||
mixedRoutes?: InputMaybe<Array<TokenTradeRouteInput>>;
|
||||
tradeType: TokenTradeType;
|
||||
v2Routes?: InputMaybe<Array<TokenTradeRouteInput>>;
|
||||
v3Routes?: InputMaybe<Array<TokenTradeRouteInput>>;
|
||||
};
|
||||
|
||||
export enum TokenTradeType {
|
||||
ExactInput = 'EXACT_INPUT',
|
||||
ExactOutput = 'EXACT_OUTPUT'
|
||||
}
|
||||
|
||||
export type TokenTransfer = {
|
||||
__typename?: 'TokenTransfer';
|
||||
asset: Token;
|
||||
@@ -806,6 +967,11 @@ export type TokenTransfer = {
|
||||
transactedValue?: Maybe<Amount>;
|
||||
};
|
||||
|
||||
export type TradePoolInput = {
|
||||
pair?: InputMaybe<PairInput>;
|
||||
pool?: InputMaybe<PoolInput>;
|
||||
};
|
||||
|
||||
export type Transaction = {
|
||||
__typename?: 'Transaction';
|
||||
blockNumber: Scalars['Int'];
|
||||
@@ -831,20 +997,37 @@ export enum TransactionStatus {
|
||||
Pending = 'PENDING'
|
||||
}
|
||||
|
||||
export type TokenQueryVariables = Exact<{
|
||||
contract: ContractInput;
|
||||
export type RecentlySearchedAssetsQueryVariables = Exact<{
|
||||
collectionAddresses: Array<Scalars['String']> | Scalars['String'];
|
||||
contracts: Array<ContractInput> | ContractInput;
|
||||
}>;
|
||||
|
||||
|
||||
export type TokenQuery = { __typename?: 'Query', tokens?: Array<{ __typename?: 'Token', id: string, decimals?: number, name?: string, chain: Chain, address?: string, symbol?: string, market?: { __typename?: 'TokenMarket', totalValueLocked?: { __typename?: 'Amount', value: number, currency?: Currency }, price?: { __typename?: 'Amount', value: number, currency?: Currency }, volume24H?: { __typename?: 'Amount', value: number, currency?: Currency }, priceHigh52W?: { __typename?: 'Amount', value: number }, priceLow52W?: { __typename?: 'Amount', value: number } }, project?: { __typename?: 'TokenProject', description?: string, homepageUrl?: string, twitterName?: string, logoUrl?: string, tokens: Array<{ __typename?: 'Token', chain: Chain, address?: string }> } }> };
|
||||
export type RecentlySearchedAssetsQuery = { __typename?: 'Query', nftCollections?: { __typename?: 'NftCollectionConnection', edges: Array<{ __typename?: 'NftCollectionEdge', node: { __typename?: 'NftCollection', collectionId: string, isVerified?: boolean, name?: string, numAssets?: number, image?: { __typename?: 'Image', url: string }, nftContracts?: Array<{ __typename?: 'NftContract', address: string }>, markets?: Array<{ __typename?: 'NftCollectionMarket', floorPrice?: { __typename?: 'TimestampedAmount', currency?: Currency, value: number } }> } }> }, tokens?: Array<{ __typename?: 'Token', id: string, decimals?: number, name?: string, chain: Chain, standard?: TokenStandard, address?: string, symbol?: string, market?: { __typename?: 'TokenMarket', id: string, price?: { __typename?: 'Amount', id: string, value: number, currency?: Currency }, pricePercentChange?: { __typename?: 'Amount', id: string, value: number }, volume24H?: { __typename?: 'Amount', id: string, value: number, currency?: Currency } }, project?: { __typename?: 'TokenProject', id: string, logoUrl?: string, safetyLevel?: SafetyLevel } }> };
|
||||
|
||||
export type SearchTokensQueryVariables = Exact<{
|
||||
searchQuery: Scalars['String'];
|
||||
}>;
|
||||
|
||||
|
||||
export type SearchTokensQuery = { __typename?: 'Query', searchTokens?: Array<{ __typename?: 'Token', id: string, decimals?: number, name?: string, chain: Chain, standard?: TokenStandard, address?: string, symbol?: string, market?: { __typename?: 'TokenMarket', id: string, price?: { __typename?: 'Amount', id: string, value: number, currency?: Currency }, pricePercentChange?: { __typename?: 'Amount', id: string, value: number }, volume24H?: { __typename?: 'Amount', id: string, value: number, currency?: Currency } }, project?: { __typename?: 'TokenProject', id: string, logoUrl?: string, safetyLevel?: SafetyLevel } }> };
|
||||
|
||||
export type TokenQueryVariables = Exact<{
|
||||
chain: Chain;
|
||||
address?: InputMaybe<Scalars['String']>;
|
||||
}>;
|
||||
|
||||
|
||||
export type TokenQuery = { __typename?: 'Query', token?: { __typename?: 'Token', id: string, decimals?: number, name?: string, chain: Chain, address?: string, symbol?: string, standard?: TokenStandard, market?: { __typename?: 'TokenMarket', id: string, totalValueLocked?: { __typename?: 'Amount', id: string, value: number, currency?: Currency }, price?: { __typename?: 'Amount', id: string, value: number, currency?: Currency }, volume24H?: { __typename?: 'Amount', id: string, value: number, currency?: Currency }, priceHigh52W?: { __typename?: 'Amount', id: string, value: number }, priceLow52W?: { __typename?: 'Amount', id: string, value: number } }, project?: { __typename?: 'TokenProject', id: string, description?: string, homepageUrl?: string, twitterName?: string, logoUrl?: string, tokens: Array<{ __typename?: 'Token', id: string, chain: Chain, address?: string }> } } };
|
||||
|
||||
export type TokenPriceQueryVariables = Exact<{
|
||||
contract: ContractInput;
|
||||
chain: Chain;
|
||||
address?: InputMaybe<Scalars['String']>;
|
||||
duration: HistoryDuration;
|
||||
}>;
|
||||
|
||||
|
||||
export type TokenPriceQuery = { __typename?: 'Query', tokens?: Array<{ __typename?: 'Token', market?: { __typename?: 'TokenMarket', price?: { __typename?: 'Amount', value: number }, priceHistory?: Array<{ __typename?: 'TimestampedAmount', timestamp: number, value: number }> } }> };
|
||||
export type TokenPriceQuery = { __typename?: 'Query', token?: { __typename?: 'Token', id: string, address?: string, chain: Chain, market?: { __typename?: 'TokenMarket', id: string, price?: { __typename?: 'Amount', id: string, value: number }, priceHistory?: Array<{ __typename?: 'TimestampedAmount', id: string, timestamp: number, value: number }> } } };
|
||||
|
||||
export type TopTokens100QueryVariables = Exact<{
|
||||
duration: HistoryDuration;
|
||||
@@ -852,7 +1035,7 @@ export type TopTokens100QueryVariables = Exact<{
|
||||
}>;
|
||||
|
||||
|
||||
export type TopTokens100Query = { __typename?: 'Query', topTokens?: Array<{ __typename?: 'Token', id: string, name?: string, chain: Chain, address?: string, symbol?: string, market?: { __typename?: 'TokenMarket', totalValueLocked?: { __typename?: 'Amount', value: number, currency?: Currency }, price?: { __typename?: 'Amount', value: number, currency?: Currency }, pricePercentChange?: { __typename?: 'Amount', currency?: Currency, value: number }, volume?: { __typename?: 'Amount', value: number, currency?: Currency } }, project?: { __typename?: 'TokenProject', logoUrl?: string } }> };
|
||||
export type TopTokens100Query = { __typename?: 'Query', topTokens?: Array<{ __typename?: 'Token', id: string, name?: string, chain: Chain, address?: string, symbol?: string, standard?: TokenStandard, market?: { __typename?: 'TokenMarket', id: string, totalValueLocked?: { __typename?: 'Amount', id: string, value: number, currency?: Currency }, price?: { __typename?: 'Amount', id: string, value: number, currency?: Currency }, pricePercentChange?: { __typename?: 'Amount', id: string, currency?: Currency, value: number }, volume?: { __typename?: 'Amount', id: string, value: number, currency?: Currency } }, project?: { __typename?: 'TokenProject', id: string, logoUrl?: string } }> };
|
||||
|
||||
export type TopTokensSparklineQueryVariables = Exact<{
|
||||
duration: HistoryDuration;
|
||||
@@ -860,7 +1043,14 @@ export type TopTokensSparklineQueryVariables = Exact<{
|
||||
}>;
|
||||
|
||||
|
||||
export type TopTokensSparklineQuery = { __typename?: 'Query', topTokens?: Array<{ __typename?: 'Token', address?: string, market?: { __typename?: 'TokenMarket', priceHistory?: Array<{ __typename?: 'TimestampedAmount', timestamp: number, value: number }> } }> };
|
||||
export type TopTokensSparklineQuery = { __typename?: 'Query', topTokens?: Array<{ __typename?: 'Token', id: string, address?: string, chain: Chain, market?: { __typename?: 'TokenMarket', id: string, priceHistory?: Array<{ __typename?: 'TimestampedAmount', id: string, timestamp: number, value: number }> } }> };
|
||||
|
||||
export type TrendingTokensQueryVariables = Exact<{
|
||||
chain: Chain;
|
||||
}>;
|
||||
|
||||
|
||||
export type TrendingTokensQuery = { __typename?: 'Query', topTokens?: Array<{ __typename?: 'Token', id: string, decimals?: number, name?: string, chain: Chain, standard?: TokenStandard, address?: string, symbol?: string, market?: { __typename?: 'TokenMarket', id: string, price?: { __typename?: 'Amount', id: string, value: number, currency?: Currency }, pricePercentChange?: { __typename?: 'Amount', id: string, value: number }, volume24H?: { __typename?: 'Amount', id: string, value: number, currency?: Currency } }, project?: { __typename?: 'TokenProject', id: string, logoUrl?: string, safetyLevel?: SafetyLevel } }> };
|
||||
|
||||
export type AssetQueryVariables = Exact<{
|
||||
address: Scalars['String'];
|
||||
@@ -901,44 +1091,212 @@ export type NftBalanceQueryVariables = Exact<{
|
||||
}>;
|
||||
|
||||
|
||||
export type NftBalanceQuery = { __typename?: 'Query', nftBalances?: { __typename?: 'NftBalanceConnection', edges: Array<{ __typename?: 'NftBalanceEdge', node: { __typename?: 'NftBalance', listedMarketplaces?: Array<NftMarketplace>, ownedAsset?: { __typename?: 'NftAsset', id: string, animationUrl?: string, description?: string, flaggedBy?: string, name?: string, ownerAddress?: string, suspiciousFlag?: boolean, tokenId: string, collection?: { __typename?: 'NftCollection', isVerified?: boolean, name?: string, image?: { __typename?: 'Image', url: string }, nftContracts?: Array<{ __typename?: 'NftContract', address: string, chain: Chain, name?: string, standard?: NftStandard, symbol?: string, totalSupply?: number }>, markets?: Array<{ __typename?: 'NftCollectionMarket', floorPrice?: { __typename?: 'TimestampedAmount', value: number } }> }, image?: { __typename?: 'Image', url: string }, originalImage?: { __typename?: 'Image', url: string }, smallImage?: { __typename?: 'Image', url: string }, thumbnail?: { __typename?: 'Image', url: string }, listings?: { __typename?: 'NftOrderConnection', edges: Array<{ __typename?: 'NftOrderEdge', node: { __typename?: 'NftOrder', createdAt: number, marketplace: NftMarketplace, endAt?: number, price: { __typename?: 'Amount', value: number, currency?: Currency } } }> } }, listingFees?: Array<{ __typename?: 'NftFee', payoutAddress: string, basisPoints: number }>, lastPrice?: { __typename?: 'TimestampedAmount', currency?: Currency, timestamp: number, value: number } } }>, pageInfo: { __typename?: 'PageInfo', endCursor?: string, hasNextPage?: boolean, hasPreviousPage?: boolean, startCursor?: string } } };
|
||||
export type NftBalanceQuery = { __typename?: 'Query', nftBalances?: { __typename?: 'NftBalanceConnection', edges: Array<{ __typename?: 'NftBalanceEdge', node: { __typename?: 'NftBalance', listedMarketplaces?: Array<NftMarketplace>, ownedAsset?: { __typename?: 'NftAsset', id: string, animationUrl?: string, description?: string, flaggedBy?: string, name?: string, ownerAddress?: string, suspiciousFlag?: boolean, tokenId: string, collection?: { __typename?: 'NftCollection', isVerified?: boolean, name?: string, twitterName?: string, image?: { __typename?: 'Image', url: string }, nftContracts?: Array<{ __typename?: 'NftContract', address: string, chain: Chain, name?: string, standard?: NftStandard, symbol?: string, totalSupply?: number }>, markets?: Array<{ __typename?: 'NftCollectionMarket', floorPrice?: { __typename?: 'TimestampedAmount', value: number } }> }, image?: { __typename?: 'Image', url: string }, originalImage?: { __typename?: 'Image', url: string }, smallImage?: { __typename?: 'Image', url: string }, thumbnail?: { __typename?: 'Image', url: string }, listings?: { __typename?: 'NftOrderConnection', edges: Array<{ __typename?: 'NftOrderEdge', node: { __typename?: 'NftOrder', createdAt: number, marketplace: NftMarketplace, endAt?: number, price: { __typename?: 'Amount', value: number, currency?: Currency } } }> } }, listingFees?: Array<{ __typename?: 'NftFee', payoutAddress: string, basisPoints: number }>, lastPrice?: { __typename?: 'TimestampedAmount', currency?: Currency, timestamp: number, value: number } } }>, pageInfo: { __typename?: 'PageInfo', endCursor?: string, hasNextPage?: boolean, hasPreviousPage?: boolean, startCursor?: string } } };
|
||||
|
||||
export type NftRouteQueryVariables = Exact<{
|
||||
chain?: InputMaybe<Chain>;
|
||||
senderAddress: Scalars['String'];
|
||||
nftTrades: Array<NftTradeInput> | NftTradeInput;
|
||||
tokenTrades?: InputMaybe<Array<TokenTradeInput> | TokenTradeInput>;
|
||||
}>;
|
||||
|
||||
|
||||
export type NftRouteQuery = { __typename?: 'Query', nftRoute?: { __typename?: 'NftRouteResponse', id: string, calldata: string, toAddress: string, route?: Array<{ __typename?: 'NftTrade', amount: number, contractAddress: string, id: string, marketplace: NftMarketplace, tokenId: string, tokenType: NftStandard, price: { __typename?: 'TokenAmount', id: string, currency: Currency, value: string }, quotePrice?: { __typename?: 'TokenAmount', id: string, currency: Currency, value: string } }>, sendAmount: { __typename?: 'TokenAmount', id: string, currency: Currency, value: string } } };
|
||||
|
||||
|
||||
export const RecentlySearchedAssetsDocument = gql`
|
||||
query RecentlySearchedAssets($collectionAddresses: [String!]!, $contracts: [ContractInput!]!) {
|
||||
nftCollections(filter: {addresses: $collectionAddresses}) {
|
||||
edges {
|
||||
node {
|
||||
collectionId
|
||||
image {
|
||||
url
|
||||
}
|
||||
isVerified
|
||||
name
|
||||
numAssets
|
||||
nftContracts {
|
||||
address
|
||||
}
|
||||
markets(currencies: ETH) {
|
||||
floorPrice {
|
||||
currency
|
||||
value
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
tokens(contracts: $contracts) {
|
||||
id
|
||||
decimals
|
||||
name
|
||||
chain
|
||||
standard
|
||||
address
|
||||
symbol
|
||||
market(currency: USD) {
|
||||
id
|
||||
price {
|
||||
id
|
||||
value
|
||||
currency
|
||||
}
|
||||
pricePercentChange(duration: DAY) {
|
||||
id
|
||||
value
|
||||
}
|
||||
volume24H: volume(duration: DAY) {
|
||||
id
|
||||
value
|
||||
currency
|
||||
}
|
||||
}
|
||||
project {
|
||||
id
|
||||
logoUrl
|
||||
safetyLevel
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
/**
|
||||
* __useRecentlySearchedAssetsQuery__
|
||||
*
|
||||
* To run a query within a React component, call `useRecentlySearchedAssetsQuery` and pass it any options that fit your needs.
|
||||
* When your component renders, `useRecentlySearchedAssetsQuery` returns an object from Apollo Client that contains loading, error, and data properties
|
||||
* you can use to render your UI.
|
||||
*
|
||||
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
|
||||
*
|
||||
* @example
|
||||
* const { data, loading, error } = useRecentlySearchedAssetsQuery({
|
||||
* variables: {
|
||||
* collectionAddresses: // value for 'collectionAddresses'
|
||||
* contracts: // value for 'contracts'
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
export function useRecentlySearchedAssetsQuery(baseOptions: Apollo.QueryHookOptions<RecentlySearchedAssetsQuery, RecentlySearchedAssetsQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useQuery<RecentlySearchedAssetsQuery, RecentlySearchedAssetsQueryVariables>(RecentlySearchedAssetsDocument, options);
|
||||
}
|
||||
export function useRecentlySearchedAssetsLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<RecentlySearchedAssetsQuery, RecentlySearchedAssetsQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useLazyQuery<RecentlySearchedAssetsQuery, RecentlySearchedAssetsQueryVariables>(RecentlySearchedAssetsDocument, options);
|
||||
}
|
||||
export type RecentlySearchedAssetsQueryHookResult = ReturnType<typeof useRecentlySearchedAssetsQuery>;
|
||||
export type RecentlySearchedAssetsLazyQueryHookResult = ReturnType<typeof useRecentlySearchedAssetsLazyQuery>;
|
||||
export type RecentlySearchedAssetsQueryResult = Apollo.QueryResult<RecentlySearchedAssetsQuery, RecentlySearchedAssetsQueryVariables>;
|
||||
export const SearchTokensDocument = gql`
|
||||
query SearchTokens($searchQuery: String!) {
|
||||
searchTokens(searchQuery: $searchQuery) {
|
||||
id
|
||||
decimals
|
||||
name
|
||||
chain
|
||||
standard
|
||||
address
|
||||
symbol
|
||||
market(currency: USD) {
|
||||
id
|
||||
price {
|
||||
id
|
||||
value
|
||||
currency
|
||||
}
|
||||
pricePercentChange(duration: DAY) {
|
||||
id
|
||||
value
|
||||
}
|
||||
volume24H: volume(duration: DAY) {
|
||||
id
|
||||
value
|
||||
currency
|
||||
}
|
||||
}
|
||||
project {
|
||||
id
|
||||
logoUrl
|
||||
safetyLevel
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
/**
|
||||
* __useSearchTokensQuery__
|
||||
*
|
||||
* To run a query within a React component, call `useSearchTokensQuery` and pass it any options that fit your needs.
|
||||
* When your component renders, `useSearchTokensQuery` returns an object from Apollo Client that contains loading, error, and data properties
|
||||
* you can use to render your UI.
|
||||
*
|
||||
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
|
||||
*
|
||||
* @example
|
||||
* const { data, loading, error } = useSearchTokensQuery({
|
||||
* variables: {
|
||||
* searchQuery: // value for 'searchQuery'
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
export function useSearchTokensQuery(baseOptions: Apollo.QueryHookOptions<SearchTokensQuery, SearchTokensQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useQuery<SearchTokensQuery, SearchTokensQueryVariables>(SearchTokensDocument, options);
|
||||
}
|
||||
export function useSearchTokensLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<SearchTokensQuery, SearchTokensQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useLazyQuery<SearchTokensQuery, SearchTokensQueryVariables>(SearchTokensDocument, options);
|
||||
}
|
||||
export type SearchTokensQueryHookResult = ReturnType<typeof useSearchTokensQuery>;
|
||||
export type SearchTokensLazyQueryHookResult = ReturnType<typeof useSearchTokensLazyQuery>;
|
||||
export type SearchTokensQueryResult = Apollo.QueryResult<SearchTokensQuery, SearchTokensQueryVariables>;
|
||||
export const TokenDocument = gql`
|
||||
query Token($contract: ContractInput!) {
|
||||
tokens(contracts: [$contract]) {
|
||||
query Token($chain: Chain!, $address: String = null) {
|
||||
token(chain: $chain, address: $address) {
|
||||
id
|
||||
decimals
|
||||
name
|
||||
chain
|
||||
address
|
||||
symbol
|
||||
standard
|
||||
market(currency: USD) {
|
||||
id
|
||||
totalValueLocked {
|
||||
id
|
||||
value
|
||||
currency
|
||||
}
|
||||
price {
|
||||
id
|
||||
value
|
||||
currency
|
||||
}
|
||||
volume24H: volume(duration: DAY) {
|
||||
id
|
||||
value
|
||||
currency
|
||||
}
|
||||
priceHigh52W: priceHighLow(duration: YEAR, highLow: HIGH) {
|
||||
id
|
||||
value
|
||||
}
|
||||
priceLow52W: priceHighLow(duration: YEAR, highLow: LOW) {
|
||||
id
|
||||
value
|
||||
}
|
||||
}
|
||||
project {
|
||||
id
|
||||
description
|
||||
homepageUrl
|
||||
twitterName
|
||||
logoUrl
|
||||
tokens {
|
||||
id
|
||||
chain
|
||||
address
|
||||
}
|
||||
@@ -959,7 +1317,8 @@ export const TokenDocument = gql`
|
||||
* @example
|
||||
* const { data, loading, error } = useTokenQuery({
|
||||
* variables: {
|
||||
* contract: // value for 'contract'
|
||||
* chain: // value for 'chain'
|
||||
* address: // value for 'address'
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
@@ -975,13 +1334,19 @@ export type TokenQueryHookResult = ReturnType<typeof useTokenQuery>;
|
||||
export type TokenLazyQueryHookResult = ReturnType<typeof useTokenLazyQuery>;
|
||||
export type TokenQueryResult = Apollo.QueryResult<TokenQuery, TokenQueryVariables>;
|
||||
export const TokenPriceDocument = gql`
|
||||
query TokenPrice($contract: ContractInput!, $duration: HistoryDuration!) {
|
||||
tokens(contracts: [$contract]) {
|
||||
query TokenPrice($chain: Chain!, $address: String = null, $duration: HistoryDuration!) {
|
||||
token(chain: $chain, address: $address) {
|
||||
id
|
||||
address
|
||||
chain
|
||||
market(currency: USD) {
|
||||
id
|
||||
price {
|
||||
id
|
||||
value
|
||||
}
|
||||
priceHistory(duration: $duration) {
|
||||
id
|
||||
timestamp
|
||||
value
|
||||
}
|
||||
@@ -1002,7 +1367,8 @@ export const TokenPriceDocument = gql`
|
||||
* @example
|
||||
* const { data, loading, error } = useTokenPriceQuery({
|
||||
* variables: {
|
||||
* contract: // value for 'contract'
|
||||
* chain: // value for 'chain'
|
||||
* address: // value for 'address'
|
||||
* duration: // value for 'duration'
|
||||
* },
|
||||
* });
|
||||
@@ -1026,25 +1392,32 @@ export const TopTokens100Document = gql`
|
||||
chain
|
||||
address
|
||||
symbol
|
||||
standard
|
||||
market(currency: USD) {
|
||||
id
|
||||
totalValueLocked {
|
||||
id
|
||||
value
|
||||
currency
|
||||
}
|
||||
price {
|
||||
id
|
||||
value
|
||||
currency
|
||||
}
|
||||
pricePercentChange(duration: $duration) {
|
||||
id
|
||||
currency
|
||||
value
|
||||
}
|
||||
volume(duration: $duration) {
|
||||
id
|
||||
value
|
||||
currency
|
||||
}
|
||||
}
|
||||
project {
|
||||
id
|
||||
logoUrl
|
||||
}
|
||||
}
|
||||
@@ -1082,9 +1455,13 @@ export type TopTokens100QueryResult = Apollo.QueryResult<TopTokens100Query, TopT
|
||||
export const TopTokensSparklineDocument = gql`
|
||||
query TopTokensSparkline($duration: HistoryDuration!, $chain: Chain!) {
|
||||
topTokens(pageSize: 100, page: 1, chain: $chain) {
|
||||
id
|
||||
address
|
||||
chain
|
||||
market(currency: USD) {
|
||||
id
|
||||
priceHistory(duration: $duration) {
|
||||
id
|
||||
timestamp
|
||||
value
|
||||
}
|
||||
@@ -1121,6 +1498,69 @@ export function useTopTokensSparklineLazyQuery(baseOptions?: Apollo.LazyQueryHoo
|
||||
export type TopTokensSparklineQueryHookResult = ReturnType<typeof useTopTokensSparklineQuery>;
|
||||
export type TopTokensSparklineLazyQueryHookResult = ReturnType<typeof useTopTokensSparklineLazyQuery>;
|
||||
export type TopTokensSparklineQueryResult = Apollo.QueryResult<TopTokensSparklineQuery, TopTokensSparklineQueryVariables>;
|
||||
export const TrendingTokensDocument = gql`
|
||||
query TrendingTokens($chain: Chain!) {
|
||||
topTokens(pageSize: 4, page: 1, chain: $chain, orderBy: VOLUME) {
|
||||
id
|
||||
decimals
|
||||
name
|
||||
chain
|
||||
standard
|
||||
address
|
||||
symbol
|
||||
market(currency: USD) {
|
||||
id
|
||||
price {
|
||||
id
|
||||
value
|
||||
currency
|
||||
}
|
||||
pricePercentChange(duration: DAY) {
|
||||
id
|
||||
value
|
||||
}
|
||||
volume24H: volume(duration: DAY) {
|
||||
id
|
||||
value
|
||||
currency
|
||||
}
|
||||
}
|
||||
project {
|
||||
id
|
||||
logoUrl
|
||||
safetyLevel
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
/**
|
||||
* __useTrendingTokensQuery__
|
||||
*
|
||||
* To run a query within a React component, call `useTrendingTokensQuery` and pass it any options that fit your needs.
|
||||
* When your component renders, `useTrendingTokensQuery` returns an object from Apollo Client that contains loading, error, and data properties
|
||||
* you can use to render your UI.
|
||||
*
|
||||
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
|
||||
*
|
||||
* @example
|
||||
* const { data, loading, error } = useTrendingTokensQuery({
|
||||
* variables: {
|
||||
* chain: // value for 'chain'
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
export function useTrendingTokensQuery(baseOptions: Apollo.QueryHookOptions<TrendingTokensQuery, TrendingTokensQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useQuery<TrendingTokensQuery, TrendingTokensQueryVariables>(TrendingTokensDocument, options);
|
||||
}
|
||||
export function useTrendingTokensLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<TrendingTokensQuery, TrendingTokensQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useLazyQuery<TrendingTokensQuery, TrendingTokensQueryVariables>(TrendingTokensDocument, options);
|
||||
}
|
||||
export type TrendingTokensQueryHookResult = ReturnType<typeof useTrendingTokensQuery>;
|
||||
export type TrendingTokensLazyQueryHookResult = ReturnType<typeof useTrendingTokensLazyQuery>;
|
||||
export type TrendingTokensQueryResult = Apollo.QueryResult<TrendingTokensQuery, TrendingTokensQueryVariables>;
|
||||
export const AssetDocument = gql`
|
||||
query Asset($address: String!, $orderBy: NftAssetSortableField, $asc: Boolean, $filter: NftAssetsFilterInput, $first: Int, $after: String, $last: Int, $before: String) {
|
||||
nftAssets(
|
||||
@@ -1493,6 +1933,7 @@ export const NftBalanceDocument = gql`
|
||||
url
|
||||
}
|
||||
name
|
||||
twitterName
|
||||
nftContracts {
|
||||
address
|
||||
chain
|
||||
@@ -1592,4 +2033,72 @@ export function useNftBalanceLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions
|
||||
}
|
||||
export type NftBalanceQueryHookResult = ReturnType<typeof useNftBalanceQuery>;
|
||||
export type NftBalanceLazyQueryHookResult = ReturnType<typeof useNftBalanceLazyQuery>;
|
||||
export type NftBalanceQueryResult = Apollo.QueryResult<NftBalanceQuery, NftBalanceQueryVariables>;
|
||||
export type NftBalanceQueryResult = Apollo.QueryResult<NftBalanceQuery, NftBalanceQueryVariables>;
|
||||
export const NftRouteDocument = gql`
|
||||
query NftRoute($chain: Chain = ETHEREUM, $senderAddress: String!, $nftTrades: [NftTradeInput!]!, $tokenTrades: [TokenTradeInput!]) {
|
||||
nftRoute(
|
||||
chain: $chain
|
||||
senderAddress: $senderAddress
|
||||
nftTrades: $nftTrades
|
||||
tokenTrades: $tokenTrades
|
||||
) {
|
||||
id
|
||||
calldata
|
||||
route {
|
||||
amount
|
||||
contractAddress
|
||||
id
|
||||
marketplace
|
||||
price {
|
||||
id
|
||||
currency
|
||||
value
|
||||
}
|
||||
quotePrice {
|
||||
id
|
||||
currency
|
||||
value
|
||||
}
|
||||
tokenId
|
||||
tokenType
|
||||
}
|
||||
sendAmount {
|
||||
id
|
||||
currency
|
||||
value
|
||||
}
|
||||
toAddress
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
/**
|
||||
* __useNftRouteQuery__
|
||||
*
|
||||
* To run a query within a React component, call `useNftRouteQuery` and pass it any options that fit your needs.
|
||||
* When your component renders, `useNftRouteQuery` returns an object from Apollo Client that contains loading, error, and data properties
|
||||
* you can use to render your UI.
|
||||
*
|
||||
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
|
||||
*
|
||||
* @example
|
||||
* const { data, loading, error } = useNftRouteQuery({
|
||||
* variables: {
|
||||
* chain: // value for 'chain'
|
||||
* senderAddress: // value for 'senderAddress'
|
||||
* nftTrades: // value for 'nftTrades'
|
||||
* tokenTrades: // value for 'tokenTrades'
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
export function useNftRouteQuery(baseOptions: Apollo.QueryHookOptions<NftRouteQuery, NftRouteQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useQuery<NftRouteQuery, NftRouteQueryVariables>(NftRouteDocument, options);
|
||||
}
|
||||
export function useNftRouteLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<NftRouteQuery, NftRouteQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useLazyQuery<NftRouteQuery, NftRouteQueryVariables>(NftRouteDocument, options);
|
||||
}
|
||||
export type NftRouteQueryHookResult = ReturnType<typeof useNftRouteQuery>;
|
||||
export type NftRouteLazyQueryHookResult = ReturnType<typeof useNftRouteLazyQuery>;
|
||||
export type NftRouteQueryResult = Apollo.QueryResult<NftRouteQuery, NftRouteQueryVariables>;
|
||||
@@ -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
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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,
|
||||
|
||||
39
src/graphql/data/nft/Routing.ts
Normal file
39
src/graphql/data/nft/Routing.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import gql from 'graphql-tag'
|
||||
|
||||
gql`
|
||||
query NftRoute(
|
||||
$chain: Chain = ETHEREUM
|
||||
$senderAddress: String!
|
||||
$nftTrades: [NftTradeInput!]!
|
||||
$tokenTrades: [TokenTradeInput!]
|
||||
) {
|
||||
nftRoute(chain: $chain, senderAddress: $senderAddress, nftTrades: $nftTrades, tokenTrades: $tokenTrades) {
|
||||
id
|
||||
calldata
|
||||
route {
|
||||
amount
|
||||
contractAddress
|
||||
id
|
||||
marketplace
|
||||
price {
|
||||
id
|
||||
currency
|
||||
value
|
||||
}
|
||||
quotePrice {
|
||||
id
|
||||
currency
|
||||
value
|
||||
}
|
||||
tokenId
|
||||
tokenType
|
||||
}
|
||||
sendAmount {
|
||||
id
|
||||
currency
|
||||
value
|
||||
}
|
||||
toAddress
|
||||
}
|
||||
}
|
||||
`
|
||||
@@ -1,9 +1,30 @@
|
||||
import { QueryResult } from '@apollo/client'
|
||||
import { SupportedChainId } from 'constants/chains'
|
||||
import { ZERO_ADDRESS } from 'constants/misc'
|
||||
import { NATIVE_CHAIN_ID, nativeOnChain, WRAPPED_NATIVE_CURRENCY } from 'constants/tokens'
|
||||
import ms from 'ms.macro'
|
||||
import { useEffect } from 'react'
|
||||
|
||||
import { Chain, HistoryDuration } from './__generated__/types-and-hooks'
|
||||
|
||||
export enum PollingInterval {
|
||||
Slow = ms`5m`,
|
||||
Normal = ms`1m`,
|
||||
Fast = ms`12s`, // 12 seconds, block times for mainnet
|
||||
LightningMcQueen = ms`3s`, // 3 seconds, approx block times for polygon
|
||||
}
|
||||
|
||||
// Polls a query only when the current component is mounted, as useQuery's pollInterval prop will continue to poll after unmount
|
||||
export function usePollQueryWhileMounted<T, K>(queryResult: QueryResult<T, K>, interval: PollingInterval) {
|
||||
const { startPolling, stopPolling } = queryResult
|
||||
|
||||
useEffect(() => {
|
||||
startPolling(interval)
|
||||
return stopPolling
|
||||
}, [interval, startPolling, stopPolling])
|
||||
|
||||
return queryResult
|
||||
}
|
||||
|
||||
export enum TimePeriod {
|
||||
HOUR,
|
||||
DAY,
|
||||
@@ -74,17 +95,8 @@ export const CHAIN_NAME_TO_CHAIN_ID: { [key: string]: SupportedChainId } = {
|
||||
|
||||
export const BACKEND_CHAIN_NAMES: Chain[] = [Chain.Ethereum, Chain.Polygon, Chain.Optimism, Chain.Arbitrum, Chain.Celo]
|
||||
|
||||
export function getTokenDetailsURL(address: string, chainName?: Chain, chainId?: number) {
|
||||
if (address === ZERO_ADDRESS && chainId && chainId === SupportedChainId.MAINNET) {
|
||||
return `/tokens/${CHAIN_ID_TO_BACKEND_NAME[chainId].toLowerCase()}/${NATIVE_CHAIN_ID}`
|
||||
} else if (chainName) {
|
||||
return `/tokens/${chainName.toLowerCase()}/${address}`
|
||||
} else if (chainId) {
|
||||
const chainName = CHAIN_ID_TO_BACKEND_NAME[chainId]
|
||||
return chainName ? `/tokens/${chainName.toLowerCase()}/${address}` : ''
|
||||
} else {
|
||||
return ''
|
||||
}
|
||||
export function getTokenDetailsURL({ address, chain }: { address?: string | null; chain: Chain }) {
|
||||
return `/tokens/${chain.toLowerCase()}/${address ?? NATIVE_CHAIN_ID}`
|
||||
}
|
||||
|
||||
export function unwrapToken<
|
||||
|
||||
@@ -24,7 +24,7 @@ export default function useAllV3TicksQuery(poolAddress: string | undefined, skip
|
||||
data,
|
||||
loading: isLoading,
|
||||
error,
|
||||
} = useQuery(query, {
|
||||
} = useQuery<Record<'ticks', Ticks>>(query, {
|
||||
variables: {
|
||||
poolAddress: poolAddress?.toLowerCase(),
|
||||
skip,
|
||||
|
||||
732
src/graphql/thegraph/__generated__/types-and-hooks.ts
generated
732
src/graphql/thegraph/__generated__/types-and-hooks.ts
generated
File diff suppressed because it is too large
Load Diff
@@ -9,30 +9,25 @@ import { useAppDispatch } from 'state/hooks'
|
||||
|
||||
import { fetchTokenList } from '../state/lists/actions'
|
||||
|
||||
export function useFetchListCallback(): (
|
||||
listUrl: string,
|
||||
sendDispatch?: boolean,
|
||||
skipValidation?: boolean
|
||||
) => Promise<TokenList> {
|
||||
export function useFetchListCallback(): (listUrl: string, skipValidation?: boolean) => Promise<TokenList> {
|
||||
const dispatch = useAppDispatch()
|
||||
|
||||
// note: prevent dispatch if using for list search or unsupported list
|
||||
return useCallback(
|
||||
async (listUrl: string, sendDispatch = true, skipValidation?: boolean) => {
|
||||
async (listUrl: string, skipValidation?: boolean) => {
|
||||
const requestId = nanoid()
|
||||
sendDispatch && dispatch(fetchTokenList.pending({ requestId, url: listUrl }))
|
||||
dispatch(fetchTokenList.pending({ requestId, url: listUrl }))
|
||||
return getTokenList(
|
||||
listUrl,
|
||||
(ensName: string) => resolveENSContentHash(ensName, RPC_PROVIDERS[SupportedChainId.MAINNET]),
|
||||
skipValidation
|
||||
)
|
||||
.then((tokenList) => {
|
||||
sendDispatch && dispatch(fetchTokenList.fulfilled({ url: listUrl, tokenList, requestId }))
|
||||
dispatch(fetchTokenList.fulfilled({ url: listUrl, tokenList, requestId }))
|
||||
return tokenList
|
||||
})
|
||||
.catch((error) => {
|
||||
console.debug(`Failed to get list at url ${listUrl}`, error)
|
||||
sendDispatch && dispatch(fetchTokenList.rejected({ url: listUrl, requestId, errorMessage: error.message }))
|
||||
dispatch(fetchTokenList.rejected({ url: listUrl, requestId, errorMessage: error.message }))
|
||||
throw error
|
||||
})
|
||||
},
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
|
||||
export type Allowance =
|
||||
| { state: AllowanceState.LOADING }
|
||||
| {
|
||||
state: AllowanceState.ALLOWED
|
||||
permitSignature?: PermitSignature
|
||||
}
|
||||
| AllowanceRequired
|
||||
|
||||
export default function usePermit2Allowance(amount?: CurrencyAmount<Token>, spender?: string): Allowance {
|
||||
const { account } = useWeb3React()
|
||||
const token = amount?.currency
|
||||
|
||||
const { tokenAllowance, isSyncing: isApprovalSyncing } = useTokenAllowance(token, account, PERMIT2_ADDRESS)
|
||||
const updateTokenAllowance = useUpdateTokenAllowance(amount, PERMIT2_ADDRESS)
|
||||
const isApproved = useMemo(() => {
|
||||
if (!amount || !tokenAllowance) return false
|
||||
return tokenAllowance.greaterThan(amount) || tokenAllowance.equalTo(amount)
|
||||
}, [amount, tokenAllowance])
|
||||
|
||||
// Marks approval as loading from the time it is submitted (pending), until it has confirmed and another block synced.
|
||||
// This avoids re-prompting the user for an already-submitted but not-yet-observed approval, by marking it loading
|
||||
// until it has been re-observed. It wll sync immediately, because confirmation fast-forwards the block number.
|
||||
const [approvalState, setApprovalState] = useState(ApprovalState.SYNCED)
|
||||
const isApprovalLoading = approvalState !== ApprovalState.SYNCED
|
||||
const isApprovalPending = useHasPendingApproval(token, PERMIT2_ADDRESS)
|
||||
useEffect(() => {
|
||||
if (isApprovalPending) {
|
||||
setApprovalState(ApprovalState.PENDING)
|
||||
} else {
|
||||
setApprovalState((state) => {
|
||||
if (state === ApprovalState.PENDING && isApprovalSyncing) {
|
||||
return ApprovalState.SYNCING
|
||||
} else if (state === ApprovalState.SYNCING && !isApprovalSyncing) {
|
||||
return ApprovalState.SYNCED
|
||||
}
|
||||
return state
|
||||
})
|
||||
}
|
||||
}, [isApprovalPending, isApprovalSyncing])
|
||||
|
||||
// Signature and PermitAllowance will expire, so they should be rechecked at an interval.
|
||||
// Calculate now such that the signature will still be valid for the submitting block.
|
||||
const [now, setNow] = useState(Date.now() + AVERAGE_L1_BLOCK_TIME)
|
||||
useInterval(
|
||||
useCallback(() => setNow((Date.now() + AVERAGE_L1_BLOCK_TIME) / 1000), []),
|
||||
AVERAGE_L1_BLOCK_TIME
|
||||
)
|
||||
|
||||
const [signature, setSignature] = useState<PermitSignature>()
|
||||
const isSigned = useMemo(() => {
|
||||
if (!amount || !signature) return false
|
||||
return signature.details.token === token?.address && signature.spender === spender && signature.sigDeadline >= now
|
||||
}, [amount, now, signature, spender, token?.address])
|
||||
|
||||
const { permitAllowance, expiration: permitExpiration, nonce } = usePermitAllowance(token, account, spender)
|
||||
const updatePermitAllowance = useUpdatePermitAllowance(token, spender, nonce, setSignature)
|
||||
const isPermitted = useMemo(() => {
|
||||
if (!amount || !permitAllowance || !permitExpiration) return false
|
||||
return (permitAllowance.greaterThan(amount) || permitAllowance.equalTo(amount)) && permitExpiration >= now
|
||||
}, [amount, now, permitAllowance, permitExpiration])
|
||||
|
||||
const shouldRequestApproval = !(isApproved || isApprovalLoading)
|
||||
const shouldRequestSignature = !(isPermitted || isSigned)
|
||||
const addTransaction = useTransactionAdder()
|
||||
const approveAndPermit = useCallback(async () => {
|
||||
if (shouldRequestApproval) {
|
||||
const { response, info } = await updateTokenAllowance()
|
||||
addTransaction(response, info)
|
||||
}
|
||||
if (shouldRequestSignature) {
|
||||
await updatePermitAllowance()
|
||||
}
|
||||
}, [addTransaction, shouldRequestApproval, shouldRequestSignature, updatePermitAllowance, updateTokenAllowance])
|
||||
|
||||
return useMemo(() => {
|
||||
if (token) {
|
||||
if (!tokenAllowance || !permitAllowance) {
|
||||
return { state: AllowanceState.LOADING }
|
||||
} else if (!(isPermitted || isSigned)) {
|
||||
return { token, state: AllowanceState.REQUIRED, isApprovalLoading: false, approveAndPermit }
|
||||
} else if (!isApproved) {
|
||||
return { token, state: AllowanceState.REQUIRED, isApprovalLoading, approveAndPermit }
|
||||
}
|
||||
}
|
||||
return { token, state: AllowanceState.ALLOWED, permitSignature: !isPermitted && isSigned ? signature : undefined }
|
||||
}, [
|
||||
approveAndPermit,
|
||||
isApprovalLoading,
|
||||
isApproved,
|
||||
isPermitted,
|
||||
isSigned,
|
||||
permitAllowance,
|
||||
signature,
|
||||
token,
|
||||
tokenAllowance,
|
||||
])
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user