Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0904399245 | ||
|
|
aec4c5a44a | ||
|
|
2a3fdf6df3 | ||
|
|
fc342495e3 | ||
|
|
8318981cb2 | ||
|
|
7b553be1cd |
2
.env
2
.env
@@ -4,8 +4,6 @@ REACT_APP_AMPLITUDE_PROXY_URL="https://api.uniswap.org/v1/amplitude-proxy"
|
||||
REACT_APP_AWS_API_REGION="us-east-2"
|
||||
REACT_APP_AWS_API_ENDPOINT="https://beta.api.uniswap.org/v1/graphql"
|
||||
REACT_APP_BNB_RPC_URL="https://rough-sleek-hill.bsc.quiknode.pro/413cc98cbc776cda8fdf1d0f47003583ff73d9bf"
|
||||
REACT_APP_BASE_GOERLI_RPC_URL="https://wiser-compatible-mansion.base-goerli.quiknode.pro/5874f36248e17020a1006149e7f68c63967e1f45/"
|
||||
REACT_APP_BASE_MAINNET_RPC_URL="https://cool-white-diagram.base-mainnet.quiknode.pro/d8f036f35dfab2c68f32dfa822cd971e7a25a117/"
|
||||
REACT_APP_INFURA_KEY="4bf032f2d38a4ed6bb975b80d6340847"
|
||||
REACT_APP_MOONPAY_API="https://api.moonpay.com"
|
||||
REACT_APP_MOONPAY_LINK="https://us-central1-uniswap-mobile.cloudfunctions.net/signMoonpayLinkV2?platform=web&env=staging"
|
||||
|
||||
17
.eslintrc.js
17
.eslintrc.js
@@ -57,22 +57,5 @@ module.exports = {
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['**/*.ts', '**/*.tsx'],
|
||||
excludedFiles: ['src/analytics/*'],
|
||||
rules: {
|
||||
'no-restricted-imports': [
|
||||
'error',
|
||||
{
|
||||
paths: [
|
||||
{
|
||||
name: '@uniswap/analytics',
|
||||
message: `Do not import from '@uniswap/analytics' directly. Use 'analytics' instead.`,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
15
.github/actions/setup/action.yml
vendored
15
.github/actions/setup/action.yml
vendored
@@ -40,11 +40,16 @@ runs:
|
||||
run: yarn contracts
|
||||
shell: bash
|
||||
|
||||
# GraphQL is generated from schema and client-side graphql queries. The schema is always fetched and changes to
|
||||
# client-side queries are hard to detect, so it is always re-generated.
|
||||
# TODO(WEB-2498): Cache based on both fetched schema and client-side graphql queries.
|
||||
# This will require some processing: cp all literal graphql tags into a separate file and hash it?
|
||||
- run: yarn graphql
|
||||
# GraphQL is generated from schema. The schema is always fetched, but if unchanged, graphql does not need to be re-generated.
|
||||
- run: yarn graphql:fetch
|
||||
shell: bash
|
||||
- uses: actions/cache@v3
|
||||
id: graphql-cache
|
||||
with:
|
||||
path: src/graphql/**/__generated__
|
||||
key: ${{ runner.os }}-graphql-${{ hashFiles('src/graphql/**/schema.graphql') }}
|
||||
- if: steps.graphql-cache.outputs.cache-hit != 'true'
|
||||
run: yarn graphql:generate
|
||||
shell: bash
|
||||
|
||||
# Messages are extracted from source.
|
||||
|
||||
15
.github/workflows/3-staging-to-prod.yml
vendored
15
.github/workflows/3-staging-to-prod.yml
vendored
@@ -14,21 +14,6 @@ jobs:
|
||||
environment:
|
||||
name: push/prod
|
||||
steps:
|
||||
- name: Check test status
|
||||
uses: actions/github-script@v6.4.1
|
||||
with:
|
||||
script: |
|
||||
const statuses = await github.rest.repos.listCommitStatusesForRef({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
ref: context.sha
|
||||
})
|
||||
const status = statuses.data.find(status => status.context === 'Test / promotion')?.state || 'missing'
|
||||
core.info('Status: ' + status)
|
||||
if (status !== 'success') {
|
||||
core.setFailed('"Test / promotion" must be successful before pushing')
|
||||
}
|
||||
|
||||
- uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab
|
||||
with:
|
||||
token: ${{ secrets.RELEASE_SERVICE_ACCESS_TOKEN }}
|
||||
|
||||
52
.github/workflows/test.yml
vendored
52
.github/workflows/test.yml
vendored
@@ -100,23 +100,6 @@ jobs:
|
||||
path: build
|
||||
if-no-files-found: error
|
||||
|
||||
cypress-typecheck:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: ./.github/actions/setup
|
||||
- uses: ./.github/actions/cache-on-main
|
||||
with:
|
||||
path: node_modules/.cache
|
||||
key: ${{ runner.os }}-cypress-tsc-${{ github.run_id }}
|
||||
restore-keys: ${{ runner.os }}-cypress-tsc-
|
||||
- run: yarn typecheck:cypress
|
||||
- if: failure() && github.ref_name == 'main'
|
||||
uses: ./.github/actions/report
|
||||
with:
|
||||
name: Cypress typecheck
|
||||
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_TEST_REPORTER_WEBHOOK }}
|
||||
|
||||
# Allows for parallel re-runs of cypress tests without re-building.
|
||||
cypress-rerun:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -186,41 +169,6 @@ jobs:
|
||||
name: Cypress tests
|
||||
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_TEST_REPORTER_WEBHOOK }}
|
||||
|
||||
cloud-typecheck:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: ./.github/actions/setup
|
||||
- uses: ./.github/actions/cache-on-main
|
||||
with:
|
||||
path: node_modules/.cache
|
||||
key: ${{ runner.os }}-cloud-tsc-${{ github.run_id }}
|
||||
restore-keys: ${{ runner.os }}-cloud-tsc-
|
||||
- run: yarn typecheck:cloud
|
||||
- if: failure() && github.ref_name == 'main'
|
||||
uses: ./.github/actions/report
|
||||
with:
|
||||
name: Cloud typecheck
|
||||
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_TEST_REPORTER_WEBHOOK }}
|
||||
|
||||
# TODO(WEB-2537): Setup CodeCOV
|
||||
cloud-tests:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: ./.github/actions/setup
|
||||
- uses: ./.github/actions/cache-on-main
|
||||
with:
|
||||
path: node_modules/.cache
|
||||
key: ${{ runner.os }}-cloud-jest-${{ github.run_id }}
|
||||
restore-keys: ${{ runner.os }}-cloud-jest-
|
||||
- run: yarn test:cloud --coverage --maxWorkers=100%
|
||||
- if: failure() && github.ref_name == 'main'
|
||||
uses: ./.github/actions/report
|
||||
with:
|
||||
name: Cloud tests
|
||||
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_TEST_REPORTER_WEBHOOK }}
|
||||
|
||||
pre:
|
||||
if: ${{ github.ref_name == 'main' || github.ref_name == 'releases/staging' }}
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -19,7 +19,6 @@ schema.graphql
|
||||
# testing
|
||||
/coverage
|
||||
/cache
|
||||
/functions/coverage
|
||||
|
||||
# builds
|
||||
/build
|
||||
@@ -47,7 +46,6 @@ notes.txt
|
||||
|
||||
package-lock.json
|
||||
|
||||
cypress/downloads
|
||||
cypress/videos
|
||||
cypress/screenshots
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import codeCoverageTask from '@cypress/code-coverage/task'
|
||||
import { defineConfig } from 'cypress'
|
||||
import { setupHardhatEvents } from 'cypress-hardhat'
|
||||
import { unlinkSync } from 'fs'
|
||||
|
||||
export default defineConfig({
|
||||
projectId: 'yp82ef',
|
||||
@@ -8,11 +9,22 @@ export default defineConfig({
|
||||
chromeWebSecurity: false,
|
||||
experimentalMemoryManagement: true, // better memory management, see https://github.com/cypress-io/cypress/pull/25462
|
||||
retries: { runMode: 2 },
|
||||
video: false, // GH provides 2 CPUs, and cypress video eats one up, see https://github.com/cypress-io/cypress/issues/20468#issuecomment-1307608025
|
||||
videoCompression: false,
|
||||
e2e: {
|
||||
async setupNodeEvents(on, config) {
|
||||
await setupHardhatEvents(on, config)
|
||||
codeCoverageTask(on, config)
|
||||
|
||||
// Delete recorded videos for specs that passed without flakes.
|
||||
on('after:spec', async (spec, results) => {
|
||||
if (results && results.video) {
|
||||
// If there were no failures (including flakes), delete the recorded video.
|
||||
if (!results.tests?.some((test) => test.attempts.some((attempt) => attempt?.state === 'failed'))) {
|
||||
unlinkSync(results.video)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return config
|
||||
},
|
||||
baseUrl: 'http://localhost:3000',
|
||||
|
||||
@@ -52,7 +52,7 @@ This becomes more relevant as you work with data on the blockchain, as you'll ne
|
||||
|
||||
```
|
||||
cy.hardhat().then(async (hardhat) => {
|
||||
cy.visit(`/swap?inputCurrency=ETH&outputCurrency=${USDC_MAINNET.address}`)
|
||||
cy.visit(`/swap?inputCurrency=ETH&outputCurrency=${USDC_MAINNET.address}`, { ethereum: 'hardhat' })
|
||||
cy.get('#swap-currency-output .token-amount-input').type('1').should('have.value', '1')
|
||||
cy.get('#swap-button').click()
|
||||
cy.contains('Confirm swap').click()
|
||||
@@ -68,7 +68,7 @@ cy.hardhat().then(async (hardhat) => {
|
||||
```
|
||||
|
||||
```
|
||||
cy.visit(`/swap?inputCurrency=ETH&outputCurrency=${USDC_MAINNET.address}`)
|
||||
cy.visit(`/swap?inputCurrency=ETH&outputCurrency=${USDC_MAINNET.address}`, { ethereum: 'hardhat' })
|
||||
cy.get('#swap-currency-output .token-amount-input').type('1').should('have.value', '1')
|
||||
cy.get('#swap-button').click()
|
||||
cy.contains('Confirm swap').click()
|
||||
@@ -87,6 +87,7 @@ cy.hardhat().then(async (hardhat) => {
|
||||
### Working with the blockchain (ie hardhat)
|
||||
|
||||
Our tests use a local hardhat node to simulate blockchain transactions. This can be accessed with `cy.hardhat().then((hardhat) => ...)`.
|
||||
Currently, tests using hardhat must opt-in in when they load the page: `cy.visit('/swap', { ethereum: 'hardhat' })`. This will not be necessary once we've totally migrated to hardhat.
|
||||
|
||||
By default, automining is turned on, so that any transaction that you send to the blockchain is mined immediately. If you want to assert on intermediate states (between sending a transaction and mining it), you can turn off automining: `cy.hardhat({ automine: false })`.
|
||||
|
||||
|
||||
@@ -9,29 +9,36 @@ describe('Add Liquidity', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('loads the token pair', () => {
|
||||
cy.visit('/add/0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984/ETH/500')
|
||||
it('loads the two correct tokens', () => {
|
||||
cy.visit('/add/0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984/0xB4FBF271143F4FBf7B91A5ded31805e42b2208d6/500')
|
||||
cy.get('#add-liquidity-input-tokena .token-symbol-container').should('contain.text', 'UNI')
|
||||
cy.get('#add-liquidity-input-tokenb .token-symbol-container').should('contain.text', 'ETH')
|
||||
cy.contains('0.05% fee tier')
|
||||
})
|
||||
|
||||
it('does not crash if token is duplicated', () => {
|
||||
cy.visit('/add/0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984/0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984')
|
||||
cy.get('#add-liquidity-input-tokena .token-symbol-container').should('contain.text', 'UNI')
|
||||
cy.get('#add-liquidity-input-tokenb .token-symbol-container').should('not.contain.text', 'UNI')
|
||||
it('does not crash if ETH is duplicated', () => {
|
||||
cy.visit('/add/0xB4FBF271143F4FBf7B91A5ded31805e42b2208d6/0xB4FBF271143F4FBf7B91A5ded31805e42b2208d6')
|
||||
cy.get('#add-liquidity-input-tokena .token-symbol-container').should('contain.text', 'ETH')
|
||||
cy.get('#add-liquidity-input-tokenb .token-symbol-container').should('not.contain.text', 'ETH')
|
||||
})
|
||||
|
||||
it('single token can be selected', () => {
|
||||
it.skip('token not in storage is loaded', () => {
|
||||
cy.visit('/add/0x07865c6e87b9f70255377e024ace6630c1eaa37f/0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984')
|
||||
cy.get('#add-liquidity-input-tokena .token-symbol-container').should('contain.text', 'USDC')
|
||||
cy.get('#add-liquidity-input-tokenb .token-symbol-container').should('contain.text', 'UNI')
|
||||
})
|
||||
|
||||
it.skip('single token can be selected', () => {
|
||||
cy.visit('/add/0x07865c6e87b9f70255377e024ace6630c1eaa37f')
|
||||
cy.get('#add-liquidity-input-tokena .token-symbol-container').should('contain.text', 'USDC')
|
||||
cy.visit('/add/0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984')
|
||||
cy.get('#add-liquidity-input-tokena .token-symbol-container').should('contain.text', 'UNI')
|
||||
})
|
||||
|
||||
it('loads fee tier distribution', () => {
|
||||
it.skip('loads fee tier distribution', () => {
|
||||
cy.fixture('feeTierDistribution.json').then((feeTierDistribution) => {
|
||||
cy.intercept('POST', '/subgraphs/name/uniswap/uniswap-v3', (req: CyHttpMessages.IncomingHttpRequest) => {
|
||||
if (hasQuery(req, 'FeeTierDistribution')) {
|
||||
req.alias = 'FeeTierDistribution'
|
||||
if (hasQuery(req, 'FeeTierDistributionQuery')) {
|
||||
req.alias = 'FeeTierDistributionQuery'
|
||||
|
||||
req.reply({
|
||||
body: {
|
||||
@@ -46,57 +53,12 @@ describe('Add Liquidity', () => {
|
||||
}
|
||||
})
|
||||
|
||||
cy.visit('/add/0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984/ETH')
|
||||
cy.wait('@FeeTierDistribution')
|
||||
cy.visit('/add/0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984/0xB4FBF271143F4FBf7B91A5ded31805e42b2208d6')
|
||||
|
||||
cy.wait('@FeeTierDistributionQuery')
|
||||
|
||||
cy.get('#add-liquidity-selected-fee .selected-fee-label').should('contain.text', '0.3% fee tier')
|
||||
cy.get('#add-liquidity-selected-fee .selected-fee-percentage').should('contain.text', '40% select')
|
||||
})
|
||||
})
|
||||
|
||||
it('disables increment and decrement until initial prices are inputted', () => {
|
||||
// ETH / BITCOIN pool (0.05% tier not created)
|
||||
cy.visit('/add/ETH/0x72e4f9F808C49A2a61dE9C5896298920Dc4EEEa9/500')
|
||||
// Set starting price in order to enable price range step counters
|
||||
cy.get('.start-price-input').type('1000')
|
||||
|
||||
// Min Price increment / decrement buttons should be disabled
|
||||
cy.get('[data-testid="increment-price-range"]').eq(0).should('be.disabled')
|
||||
cy.get('[data-testid="decrement-price-range"]').eq(0).should('be.disabled')
|
||||
// Enter min price, which should enable the buttons
|
||||
cy.get('.rate-input-0').eq(0).type('900').blur()
|
||||
cy.get('[data-testid="increment-price-range"]').eq(0).should('not.be.disabled')
|
||||
cy.get('[data-testid="decrement-price-range"]').eq(0).should('not.be.disabled')
|
||||
|
||||
// Repeat for Max Price step counter
|
||||
cy.get('[data-testid="increment-price-range"]').eq(1).should('be.disabled')
|
||||
cy.get('[data-testid="decrement-price-range"]').eq(1).should('be.disabled')
|
||||
// Enter max price, which should enable the buttons
|
||||
cy.get('.rate-input-0').eq(1).type('1100').blur()
|
||||
cy.get('[data-testid="increment-price-range"]').eq(1).should('not.be.disabled')
|
||||
cy.get('[data-testid="decrement-price-range"]').eq(1).should('not.be.disabled')
|
||||
})
|
||||
|
||||
it('allows full range selection on new pool creation', () => {
|
||||
// ETH / BITCOIN pool (0.05% tier not created)
|
||||
cy.visit('/add/ETH/0x72e4f9F808C49A2a61dE9C5896298920Dc4EEEa9/500')
|
||||
// Set starting price in order to enable price range step counters
|
||||
cy.get('.start-price-input').type('1000')
|
||||
cy.get('[data-testid="set-full-range"]').click()
|
||||
// Check that the min price is 0 and the max price is infinity
|
||||
cy.get('.rate-input-0').eq(0).should('have.value', '0')
|
||||
cy.get('.rate-input-0').eq(1).should('have.value', '∞')
|
||||
// Increment and decrement buttons are disabled when full range is selected
|
||||
cy.get('[data-testid="increment-price-range"]').eq(0).should('be.disabled')
|
||||
cy.get('[data-testid="decrement-price-range"]').eq(0).should('be.disabled')
|
||||
cy.get('[data-testid="increment-price-range"]').eq(1).should('be.disabled')
|
||||
cy.get('[data-testid="decrement-price-range"]').eq(1).should('be.disabled')
|
||||
// Check that url params were added
|
||||
cy.url().then((url) => {
|
||||
const params = new URLSearchParams(url)
|
||||
const minPrice = params.get('minPrice')
|
||||
const maxPrice = params.get('maxPrice')
|
||||
// Note: although 0 and ∞ displayed, actual values in query are ticks at limit
|
||||
return minPrice && maxPrice && parseFloat(minPrice) < parseFloat(maxPrice)
|
||||
cy.get('#add-liquidity-selected-fee .selected-fee-percentage').should('contain.text', '40%')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -16,7 +16,7 @@ describe('Buy Crypto Modal', () => {
|
||||
|
||||
it('should open and close, mobile viewport', () => {
|
||||
cy.viewport('iphone-6')
|
||||
cy.visit('/', { featureFlags: [FeatureFlag.fiatOnRampButtonOnSwap] })
|
||||
cy.visit('/')
|
||||
|
||||
// Open the fiat onramp modal
|
||||
cy.get(getTestSelector('buy-fiat-button')).click()
|
||||
|
||||
@@ -3,7 +3,7 @@ import { getTestSelector } from '../../utils'
|
||||
describe('Mini Portfolio account drawer', () => {
|
||||
beforeEach(() => {
|
||||
cy.intercept(/api.uniswap.org\/v1\/graphql/, cy.spy().as('gqlSpy'))
|
||||
cy.visit('/swap')
|
||||
cy.visit('/swap', { ethereum: 'hardhat' })
|
||||
})
|
||||
|
||||
it('fetches balances when account button is first hovered', () => {
|
||||
@@ -41,7 +41,6 @@ describe('Mini Portfolio account drawer', () => {
|
||||
cy.get(getTestSelector('mini-portfolio-navbar')).contains('NFTs').click()
|
||||
cy.get(getTestSelector('mini-portfolio-page')).contains('I Got Plenty')
|
||||
|
||||
cy.intercept(/graphql/, { fixture: 'mini-portfolio/pools.json' })
|
||||
cy.get(getTestSelector('mini-portfolio-navbar')).contains('Pools').click()
|
||||
cy.get(getTestSelector('mini-portfolio-page')).contains('No pools yet')
|
||||
|
||||
@@ -77,36 +76,4 @@ describe('Mini Portfolio account drawer', () => {
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('fetches ENS name', () => {
|
||||
cy.hardhat().then(() => {
|
||||
const haydenAccount = '0x50EC05ADe8280758E2077fcBC08D878D4aef79C3'
|
||||
const haydenENS = 'hayden.eth'
|
||||
|
||||
// Opens the account drawer
|
||||
cy.get(getTestSelector('web3-status-connected')).click()
|
||||
|
||||
// Simulate wallet changing to Hayden's account
|
||||
cy.window().then((win) => win.ethereum.emit('accountsChanged', [haydenAccount]))
|
||||
|
||||
// Hayden's ENS name should be shown
|
||||
cy.contains(haydenENS).should('exist')
|
||||
|
||||
// Close account drawer
|
||||
cy.get(getTestSelector('close-account-drawer')).click()
|
||||
|
||||
// Switch chain to Polygon
|
||||
cy.get(getTestSelector('chain-selector')).eq(1).click()
|
||||
cy.contains('Polygon').click()
|
||||
|
||||
//Reopen account drawer
|
||||
cy.get(getTestSelector('web3-status-connected')).click()
|
||||
|
||||
// Simulate wallet changing to Hayden's account
|
||||
cy.window().then((win) => win.ethereum.emit('accountsChanged', [haydenAccount]))
|
||||
|
||||
// Hayden's ENS name should be shown
|
||||
cy.contains(haydenENS).should('exist')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -94,7 +94,9 @@ describe('mini-portfolio activity history', () => {
|
||||
})
|
||||
|
||||
it('should deduplicate activity history by nonce', () => {
|
||||
cy.visit(`/swap?inputCurrency=ETH&outputCurrency=${USDC_MAINNET.address}`).hardhat({ automine: false })
|
||||
cy.visit(`/swap?inputCurrency=ETH&outputCurrency=${USDC_MAINNET.address}`, { ethereum: 'hardhat' }).hardhat({
|
||||
automine: false,
|
||||
})
|
||||
|
||||
// Input swap info.
|
||||
cy.get('#swap-currency-input .token-amount-input').clear().type('1').should('have.value', '1')
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { getTestSelector } from '../utils'
|
||||
|
||||
const PUDGY_COLLECTION_ADDRESS = '0xbd3531da5cf5857e7cfaa92426877b022e612cf8'
|
||||
const BONSAI_COLLECTION_ADDRESS = '0xec9c519d49856fd2f8133a0741b4dbe002ce211b'
|
||||
|
||||
describe('Testing nfts', () => {
|
||||
it('should load nft leaderboard', () => {
|
||||
@@ -37,10 +38,7 @@ describe('Testing nfts', () => {
|
||||
})
|
||||
|
||||
it('should toggle buy now on details page', () => {
|
||||
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-collection-asset')).first().click()
|
||||
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')
|
||||
@@ -52,7 +50,7 @@ describe('Testing nfts', () => {
|
||||
cy.visit('/')
|
||||
cy.get(getTestSelector('web3-status-connected')).click()
|
||||
cy.get(getTestSelector('mini-portfolio-navbar')).contains('NFTs').click()
|
||||
cy.get(getTestSelector('mini-portfolio-nft')).first().click()
|
||||
cy.get(getTestSelector('mini-portfolio-nft')).click()
|
||||
cy.get(getTestSelector('mini-portfolio-navbar')).should('not.be.visible')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -17,7 +17,9 @@ function initiateSwap() {
|
||||
describe('Permit2', () => {
|
||||
function setupInputs(inputToken: Token, outputToken: Token) {
|
||||
// Sets up a swap between inputToken and outputToken.
|
||||
cy.visit(`/swap/?inputCurrency=${inputToken.address}&outputCurrency=${outputToken.address}`)
|
||||
cy.visit(`/swap/?inputCurrency=${inputToken.address}&outputCurrency=${outputToken.address}`, {
|
||||
ethereum: 'hardhat',
|
||||
})
|
||||
cy.get('#swap-currency-input .token-amount-input').type('0.01')
|
||||
}
|
||||
|
||||
@@ -27,7 +29,7 @@ describe('Permit2', () => {
|
||||
cy.hardhat()
|
||||
.then(({ approval, wallet }) => approval.getTokenAllowanceForPermit2({ owner: wallet, token: inputToken }))
|
||||
.then((allowance) => {
|
||||
Cypress.log({ name: `Token allowance: ${allowance.toString()}` })
|
||||
Cypress.log({ name: `Token allowace: ${allowance.toString()}` })
|
||||
cy.wrap(allowance).should('deep.equal', MaxUint256)
|
||||
})
|
||||
}
|
||||
@@ -37,7 +39,7 @@ describe('Permit2', () => {
|
||||
cy.hardhat()
|
||||
.then(({ approval, wallet }) => approval.getPermit2Allowance({ owner: wallet, token: inputToken }))
|
||||
.then((allowance) => {
|
||||
Cypress.log({ name: `Permit2 allowance: ${allowance.amount.toString()}` })
|
||||
Cypress.log({ name: `Permit2 allowace: ${allowance.amount.toString()}` })
|
||||
cy.wrap(allowance.amount).should('deep.equal', MaxUint160)
|
||||
// Asserts that the on-chain expiration is in 30 days, within a tolerance of 40 seconds.
|
||||
const THIRTY_DAYS_SECONDS = 2_592_000
|
||||
@@ -75,9 +77,8 @@ describe('Permit2', () => {
|
||||
cy.contains('Allow DAI to be used for swapping')
|
||||
cy.wait('@eth_signTypedData_v4')
|
||||
cy.wait('@eth_sendRawTransaction')
|
||||
cy.contains('Swap submitted')
|
||||
cy.hardhat().then((hardhat) => hardhat.mine())
|
||||
cy.contains('Swap success!')
|
||||
cy.contains('Success')
|
||||
cy.get(getTestSelector('popups')).contains('Swapped')
|
||||
expectPermit2AllowanceForUniversalRouterToBeMax(DAI)
|
||||
})
|
||||
@@ -100,7 +101,7 @@ describe('Permit2', () => {
|
||||
// Verify transaction
|
||||
cy.wait('@eth_sendRawTransaction')
|
||||
cy.hardhat().then((hardhat) => hardhat.mine())
|
||||
cy.contains('Swap success!')
|
||||
cy.contains('Success')
|
||||
cy.get(getTestSelector('popups')).contains('Swapped')
|
||||
})
|
||||
|
||||
@@ -143,7 +144,7 @@ describe('Permit2', () => {
|
||||
// Verify transaction
|
||||
cy.wait('@eth_sendRawTransaction')
|
||||
cy.hardhat().then((hardhat) => hardhat.mine())
|
||||
cy.contains('Swap success!')
|
||||
cy.contains('Success')
|
||||
cy.get(getTestSelector('popups')).contains('Swapped')
|
||||
})
|
||||
})
|
||||
@@ -159,7 +160,7 @@ describe('Permit2', () => {
|
||||
initiateSwap()
|
||||
|
||||
// Verify transaction
|
||||
cy.contains('Swap success!')
|
||||
cy.contains('Success')
|
||||
cy.get(getTestSelector('popups')).contains('Swapped')
|
||||
})
|
||||
|
||||
@@ -198,7 +199,7 @@ describe('Permit2', () => {
|
||||
cy.contains('Confirm swap').click()
|
||||
|
||||
// Verify permit2 approval
|
||||
cy.contains('Swap success!')
|
||||
cy.contains('Success')
|
||||
cy.get(getTestSelector('popups')).contains('Swapped')
|
||||
expectPermit2AllowanceForUniversalRouterToBeMax(DAI)
|
||||
})
|
||||
@@ -232,7 +233,7 @@ describe('Permit2', () => {
|
||||
|
||||
// Verify permit2 approval
|
||||
cy.wait('@eth_signTypedData_v4')
|
||||
cy.contains('Swap success!')
|
||||
cy.contains('Success')
|
||||
cy.get(getTestSelector('popups')).contains('Swapped')
|
||||
expectPermit2AllowanceForUniversalRouterToBeMax(DAI)
|
||||
})
|
||||
@@ -250,7 +251,7 @@ describe('Permit2', () => {
|
||||
|
||||
// Verify permit2 approval
|
||||
cy.wait('@eth_signTypedData_v4')
|
||||
cy.contains('Swap success!')
|
||||
cy.contains('Success')
|
||||
cy.get(getTestSelector('popups')).contains('Swapped')
|
||||
expectPermit2AllowanceForUniversalRouterToBeMax(DAI)
|
||||
})
|
||||
|
||||
@@ -1,7 +1,25 @@
|
||||
describe('Remove Liquidity', () => {
|
||||
it('loads the token pair', () => {
|
||||
it('eth remove', () => {
|
||||
cy.visit('/remove/v2/ETH/0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984')
|
||||
cy.get('#remove-liquidity-tokena-symbol').should('contain.text', 'ETH')
|
||||
cy.get('#remove-liquidity-tokenb-symbol').should('contain.text', 'UNI')
|
||||
})
|
||||
|
||||
it('eth remove swap order', () => {
|
||||
cy.visit('/remove/v2/0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984/ETH')
|
||||
cy.get('#remove-liquidity-tokena-symbol').should('contain.text', 'UNI')
|
||||
cy.get('#remove-liquidity-tokenb-symbol').should('contain.text', 'ETH')
|
||||
})
|
||||
|
||||
it('loads the two correct tokens', () => {
|
||||
cy.visit('/remove/v2/0xB4FBF271143F4FBf7B91A5ded31805e42b2208d6/0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984')
|
||||
cy.get('#remove-liquidity-tokena-symbol').should('contain.text', 'WETH')
|
||||
cy.get('#remove-liquidity-tokenb-symbol').should('contain.text', 'UNI')
|
||||
})
|
||||
|
||||
it('does not crash if ETH is duplicated', () => {
|
||||
cy.visit('/remove/v2/0xB4FBF271143F4FBf7B91A5ded31805e42b2208d6/0xB4FBF271143F4FBf7B91A5ded31805e42b2208d6')
|
||||
cy.get('#remove-liquidity-tokena-symbol').should('contain.text', 'WETH')
|
||||
cy.get('#remove-liquidity-tokenb-symbol').should('contain.text', 'WETH')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
import { BigNumber } from '@ethersproject/bignumber'
|
||||
import { CurrencyAmount } from '@uniswap/sdk-core'
|
||||
import { ChainId } from '@uniswap/sdk-core'
|
||||
|
||||
import { DEFAULT_DEADLINE_FROM_NOW } from '../../../src/constants/misc'
|
||||
import { DAI, USDC_MAINNET } from '../../../src/constants/tokens'
|
||||
import { UNI, USDC_MAINNET } from '../../../src/constants/tokens'
|
||||
import { getBalance, getTestSelector } from '../../utils'
|
||||
|
||||
const UNI_MAINNET = UNI[ChainId.MAINNET]
|
||||
|
||||
describe('Swap errors', () => {
|
||||
it('wallet rejection', () => {
|
||||
cy.visit(`/swap?inputCurrency=ETH&outputCurrency=${USDC_MAINNET.address}`)
|
||||
cy.visit(`/swap?inputCurrency=ETH&outputCurrency=${USDC_MAINNET.address}`, { ethereum: 'hardhat' })
|
||||
cy.hardhat().then((hardhat) => {
|
||||
// Stub the wallet to reject any transaction.
|
||||
cy.stub(hardhat.wallet, 'sendTransaction').log(false).rejects(new Error('user cancelled'))
|
||||
@@ -28,7 +30,7 @@ describe('Swap errors', () => {
|
||||
})
|
||||
|
||||
it('transaction past deadline', () => {
|
||||
cy.visit(`/swap?inputCurrency=ETH&outputCurrency=${USDC_MAINNET.address}`)
|
||||
cy.visit(`/swap?inputCurrency=ETH&outputCurrency=${USDC_MAINNET.address}`, { ethereum: 'hardhat' })
|
||||
cy.hardhat({ automine: false })
|
||||
getBalance(USDC_MAINNET).then((initialBalance) => {
|
||||
// Enter amount to swap
|
||||
@@ -62,19 +64,10 @@ describe('Swap errors', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('slippage failure', () => {
|
||||
cy.visit(`/swap?inputCurrency=${USDC_MAINNET.address}&outputCurrency=${DAI.address}`)
|
||||
cy.hardhat({ automine: false }).then(async (hardhat) => {
|
||||
await hardhat.fund(hardhat.wallet, CurrencyAmount.fromRawAmount(USDC_MAINNET, 500e6))
|
||||
await hardhat.mine()
|
||||
await Promise.all([
|
||||
hardhat.approval.setTokenAllowanceForPermit2({ owner: hardhat.wallet, token: USDC_MAINNET }),
|
||||
hardhat.approval.setPermit2Allowance({ owner: hardhat.wallet, token: USDC_MAINNET }),
|
||||
])
|
||||
await hardhat.mine()
|
||||
})
|
||||
|
||||
getBalance(DAI).then((initialBalance) => {
|
||||
it.skip('slippage failure', () => {
|
||||
cy.visit(`/swap?inputCurrency=ETH&outputCurrency=${UNI_MAINNET.address}`, { ethereum: 'hardhat' })
|
||||
cy.hardhat({ automine: false })
|
||||
getBalance(USDC_MAINNET).then((initialBalance) => {
|
||||
// Gas estimation fails for this transaction (that would normally fail), so we stub it.
|
||||
cy.hardhat().then((hardhat) => {
|
||||
const send = cy.stub(hardhat.provider, 'send').log(false)
|
||||
@@ -97,9 +90,7 @@ describe('Swap errors', () => {
|
||||
cy.contains('Confirm swap').click()
|
||||
cy.wait('@eth_sendRawTransaction').wait('@eth_getTransactionReceipt')
|
||||
cy.contains('Swap submitted')
|
||||
if (i === 0) {
|
||||
cy.get(getTestSelector('confirmation-close-icon')).click()
|
||||
}
|
||||
cy.get(getTestSelector('confirmation-close-icon')).click()
|
||||
}
|
||||
cy.get(getTestSelector('web3-status-connected')).should('contain', '2 Pending')
|
||||
|
||||
@@ -107,13 +98,10 @@ describe('Swap errors', () => {
|
||||
cy.hardhat().then((hardhat) => hardhat.mine())
|
||||
cy.wait('@eth_getTransactionReceipt')
|
||||
|
||||
cy.contains('Swap failed')
|
||||
|
||||
// Verify only 1 transaction occurred
|
||||
// Verify transaction did not occur
|
||||
cy.get(getTestSelector('web3-status-connected')).should('not.contain', 'Pending')
|
||||
cy.get(getTestSelector('popups')).contains('Swapped')
|
||||
cy.get(getTestSelector('popups')).contains('Swap failed')
|
||||
getBalance(DAI).should('be.closeTo', initialBalance + 200, 1)
|
||||
getBalance(UNI_MAINNET).should('eq', initialBalance)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -3,7 +3,7 @@ import { getTestSelector } from '../../utils'
|
||||
|
||||
describe('Swap settings', () => {
|
||||
it('Opens and closes the settings menu', () => {
|
||||
cy.visit('/swap', { featureFlags: [FeatureFlag.uniswapXEnabled] })
|
||||
cy.visit('/swap', { featureFlags: [FeatureFlag.uniswapXEnabled], ethereum: 'hardhat' })
|
||||
cy.contains('Settings').should('not.exist')
|
||||
cy.get(getTestSelector('open-settings-dialog-button')).click()
|
||||
cy.contains('Max slippage').should('exist')
|
||||
|
||||
@@ -52,13 +52,13 @@ describe('Swap', () => {
|
||||
})
|
||||
|
||||
it('swaps ETH for USDC', () => {
|
||||
cy.visit('/swap')
|
||||
cy.visit('/swap', { ethereum: 'hardhat' })
|
||||
cy.hardhat({ automine: false })
|
||||
getBalance(USDC_MAINNET).then((initialBalance) => {
|
||||
// Select USDC
|
||||
cy.get('#swap-currency-output .open-currency-select-button').click()
|
||||
cy.get(getTestSelector('token-search-input')).type(USDC_MAINNET.address)
|
||||
cy.get(getTestSelector('common-base-USDC')).click()
|
||||
cy.contains('USDC').click()
|
||||
|
||||
// Enter amount to swap
|
||||
cy.get('#swap-currency-output .token-amount-input').type('1').should('have.value', '1')
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
import { SwapEventName } from '@uniswap/analytics-events'
|
||||
|
||||
import { USDC_MAINNET } from '../../../src/constants/tokens'
|
||||
import { getTestSelector } from '../../utils'
|
||||
|
||||
describe('time-to-swap logging', () => {
|
||||
it('completes two swaps and verifies the TTS logging for the first', () => {
|
||||
cy.visit(`/swap?inputCurrency=ETH&outputCurrency=${USDC_MAINNET.address}`)
|
||||
cy.hardhat()
|
||||
|
||||
// First swap in the session:
|
||||
// Enter amount to swap
|
||||
cy.get('#swap-currency-output .token-amount-input').type('1').should('have.value', '1')
|
||||
cy.get('#swap-currency-input .token-amount-input').should('not.have.value', '')
|
||||
|
||||
// Submit transaction
|
||||
cy.get('#swap-button').click()
|
||||
cy.contains('Confirm swap').click()
|
||||
cy.get(getTestSelector('confirmation-close-icon')).click()
|
||||
|
||||
cy.get(getTestSelector('popups')).contains('Swapped')
|
||||
|
||||
// Verify logging
|
||||
cy.waitForAmplitudeEvent(SwapEventName.SWAP_TRANSACTION_COMPLETED).then((event: any) => {
|
||||
cy.wrap(event.event_properties).should('have.property', 'time_to_swap')
|
||||
cy.wrap(event.event_properties.time_to_swap).should('be.a', 'number')
|
||||
cy.wrap(event.event_properties.time_to_swap).should('be.gte', 0)
|
||||
})
|
||||
|
||||
// Second swap in the session:
|
||||
// Enter amount to swap
|
||||
cy.get('#swap-currency-output .token-amount-input').clear().type('1').should('have.value', '1')
|
||||
cy.get('#swap-currency-input .token-amount-input').should('not.have.value', '')
|
||||
|
||||
// Submit transaction
|
||||
cy.get('#swap-button').click()
|
||||
cy.contains('Confirm swap').click()
|
||||
cy.get(getTestSelector('confirmation-close-icon')).click()
|
||||
|
||||
cy.get(getTestSelector('popups')).contains('Swapped')
|
||||
cy.waitForAmplitudeEvent(SwapEventName.SWAP_TRANSACTION_COMPLETED).then((event: any) => {
|
||||
cy.wrap(event.event_properties).should('not.have.property', 'time_to_swap')
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -6,7 +6,9 @@ const WETH = WETH9[ChainId.MAINNET]
|
||||
|
||||
describe('Swap wrap', () => {
|
||||
beforeEach(() => {
|
||||
cy.visit(`/swap?inputCurrency=ETH&outputCurrency=${WETH.address}`).hardhat({ automine: false })
|
||||
cy.visit(`/swap?inputCurrency=ETH&outputCurrency=${WETH.address}`, { ethereum: 'hardhat' }).hardhat({
|
||||
automine: false,
|
||||
})
|
||||
})
|
||||
|
||||
it('ETH to wETH is same value (wrapped swaps have no price impact)', () => {
|
||||
|
||||
@@ -93,7 +93,9 @@ describe('Token details', () => {
|
||||
beforeEach(() => {
|
||||
// On mobile widths, we just link back to /swap instead of rendering the swap component.
|
||||
cy.viewport(1200, 800)
|
||||
cy.visit(`/tokens/ethereum/${UNI_MAINNET.address}`).then(() => {
|
||||
cy.visit(`/tokens/ethereum/${UNI_MAINNET.address}`, {
|
||||
ethereum: 'hardhat',
|
||||
}).then(() => {
|
||||
cy.wait('@eth_blockNumber')
|
||||
cy.scrollTo('top')
|
||||
})
|
||||
@@ -143,7 +145,7 @@ describe('Token details', () => {
|
||||
})
|
||||
|
||||
it('should show a L2 token even if the user is connected to a different network', () => {
|
||||
cy.visit('/tokens')
|
||||
cy.visit('/tokens', { ethereum: 'hardhat' })
|
||||
cy.get(getTestSelector('tokens-network-filter-selected')).click()
|
||||
cy.get(getTestSelector('tokens-network-filter-option-arbitrum')).click()
|
||||
cy.get(getTestSelector('tokens-network-filter-selected')).should('contain', 'Arbitrum')
|
||||
|
||||
@@ -36,7 +36,7 @@ describe('Token explore', () => {
|
||||
.then(function ($elem) {
|
||||
cy.wrap($elem.text()).as('yearlyEthVol')
|
||||
})
|
||||
cy.get('@dailyEthVol').should('not.equal', cy.get('@yearlyEthVol'))
|
||||
expect(cy.get('@dailyEthVol')).to.not.equal(cy.get('@yearlyEthVol'))
|
||||
})
|
||||
|
||||
it('should navigate to token detail page when row clicked', () => {
|
||||
|
||||
@@ -1,53 +1,45 @@
|
||||
import { getTestSelector } from '../utils'
|
||||
|
||||
describe('Universal search bar', () => {
|
||||
function openSearch() {
|
||||
// can't just type "/" because on mobile it doesn't respond to that
|
||||
cy.get('[data-cy="magnifying-icon"]').parent().eq(1).click()
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
cy.visit('/')
|
||||
cy.get('[data-cy="magnifying-icon"]').parent().eq(1).click()
|
||||
})
|
||||
|
||||
function getSearchBar() {
|
||||
return cy.get('[data-cy="search-bar-input"]').last()
|
||||
}
|
||||
|
||||
it('should yield clickable result that is then added to recent searches', () => {
|
||||
// Search for UNI token by name.
|
||||
openSearch()
|
||||
getSearchBar().clear().type('uni')
|
||||
|
||||
cy.get(getTestSelector('searchbar-token-row-UNI'))
|
||||
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', '%')
|
||||
.click()
|
||||
cy.get('[data-cy="searchbar-token-row-UNI"]').first().click()
|
||||
cy.location('hash').should('equal', '#/tokens/ethereum/0x1f9840a85d5af5bf1d1762f925bdaddc4201f984')
|
||||
|
||||
openSearch()
|
||||
cy.get(getTestSelector('searchbar-dropdown'))
|
||||
.contains(getTestSelector('searchbar-dropdown'), 'Recent searches')
|
||||
.find(getTestSelector('searchbar-token-row-UNI'))
|
||||
.should('exist')
|
||||
})
|
||||
|
||||
it('should go to the selected result when recent results are shown', () => {
|
||||
// Seed recent results with UNI.
|
||||
openSearch()
|
||||
getSearchBar().type('uni')
|
||||
cy.get(getTestSelector('searchbar-token-row-UNI'))
|
||||
getSearchBar().clear().type('{esc}')
|
||||
it.skip('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')
|
||||
|
||||
// Search a different token by name.
|
||||
openSearch()
|
||||
getSearchBar().type('eth')
|
||||
cy.get(getTestSelector('searchbar-token-row-ETH'))
|
||||
// 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)
|
||||
})
|
||||
|
||||
// Validate that we go to the searched/selected result.
|
||||
getSearchBar().type('{enter}')
|
||||
cy.url().should('contain', 'tokens/ethereum/NATIVE')
|
||||
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')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -3,7 +3,7 @@ import { DISCONNECTED_WALLET_USER_STATE } from '../../utils/user-state'
|
||||
|
||||
describe('disconnect wallet', () => {
|
||||
it('should clear state', () => {
|
||||
cy.visit('/swap')
|
||||
cy.visit('/swap', { ethereum: 'hardhat' })
|
||||
cy.get('#swap-currency-input .token-amount-input').clear().type('1')
|
||||
|
||||
// Verify wallet is connected
|
||||
@@ -28,7 +28,7 @@ describe('disconnect wallet', () => {
|
||||
|
||||
describe('connect wallet', () => {
|
||||
it('should load state', () => {
|
||||
cy.visit('/swap', { userState: DISCONNECTED_WALLET_USER_STATE })
|
||||
cy.visit('/swap', { ethereum: 'hardhat', userState: DISCONNECTED_WALLET_USER_STATE })
|
||||
|
||||
// Connect the wallet
|
||||
cy.get(getTestSelector('navbar-connect-wallet')).contains('Connect').click()
|
||||
|
||||
@@ -12,7 +12,7 @@ function switchChain(chain: string) {
|
||||
|
||||
describe('network switching', () => {
|
||||
beforeEach(() => {
|
||||
cy.visit('/swap')
|
||||
cy.visit('/swap', { ethereum: 'hardhat' })
|
||||
cy.get(getTestSelector('web3-status-connected'))
|
||||
})
|
||||
|
||||
@@ -111,7 +111,6 @@ describe('network switching', () => {
|
||||
cy.wait('@wallet_switchEthereumChain')
|
||||
waitsForActiveChain('Polygon')
|
||||
cy.get(getTestSelector('web3-status-connected'))
|
||||
cy.url().should('contain', 'chain=polygon')
|
||||
|
||||
// Verify that the input/output fields were reset
|
||||
cy.get('#swap-currency-input .token-amount-input').should('have.value', '')
|
||||
@@ -123,22 +122,9 @@ describe('network switching', () => {
|
||||
|
||||
describe('network switching from URL param', () => {
|
||||
it('should switch network from URL param', () => {
|
||||
cy.visit('/swap?chain=polygon')
|
||||
cy.visit('/swap?chain=polygon', { ethereum: 'hardhat' })
|
||||
cy.get(getTestSelector('web3-status-connected'))
|
||||
cy.wait('@wallet_switchEthereumChain')
|
||||
waitsForActiveChain('Polygon')
|
||||
})
|
||||
|
||||
it('should be able to switch network after loading from URL param', () => {
|
||||
cy.visit('/swap?chain=polygon')
|
||||
cy.get(getTestSelector('web3-status-connected'))
|
||||
cy.wait('@wallet_switchEthereumChain')
|
||||
waitsForActiveChain('Polygon')
|
||||
|
||||
// switching to another chain clears query param
|
||||
switchChain('Ethereum')
|
||||
cy.wait('@wallet_switchEthereumChain')
|
||||
waitsForActiveChain('Ethereum')
|
||||
cy.url().should('not.contain', 'chain=polygon')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
{}
|
||||
@@ -5,6 +5,7 @@ import { Eip1193Bridge } from '@ethersproject/experimental/lib/eip1193-bridge'
|
||||
import { FeatureFlag } from '../../src/featureFlags'
|
||||
import { UserState } from '../../src/state/user/reducer'
|
||||
import { CONNECTED_WALLET_USER_STATE } from '../utils/user-state'
|
||||
import { injected } from './ethereum'
|
||||
|
||||
declare global {
|
||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||
@@ -12,19 +13,15 @@ declare global {
|
||||
interface ApplicationWindow {
|
||||
ethereum: Eip1193Bridge
|
||||
}
|
||||
interface Chainable<Subject> {
|
||||
/**
|
||||
* Wait for a specific event to be sent to amplitude. If the event is found, the subject will be the event.
|
||||
*
|
||||
* @param {string} eventName - The type of the event to search for e.g. SwapEventName.SWAP_TRANSACTION_COMPLETED
|
||||
* @param {number} timeout - The maximum amount of time (in ms) to wait for the event.
|
||||
* @returns {Chainable<Subject>}
|
||||
*/
|
||||
waitForAmplitudeEvent(eventName: string, timeout?: number): Chainable<Subject>
|
||||
}
|
||||
interface VisitOptions {
|
||||
serviceWorker?: true
|
||||
featureFlags?: Array<FeatureFlag>
|
||||
/**
|
||||
* The mock ethereum provider to inject into the page.
|
||||
* @default 'goerli'
|
||||
*/
|
||||
// TODO(INFRA-175): Migrate all usage of 'goerli' to 'hardhat'.
|
||||
ethereum?: 'goerli' | 'hardhat'
|
||||
/**
|
||||
* Initial user state.
|
||||
* @default {@type import('../utils/user-state').CONNECTED_WALLET_USER_STATE}
|
||||
@@ -42,7 +39,8 @@ Cypress.Commands.overwrite(
|
||||
if (typeof url !== 'string') throw new Error('Invalid arguments. The first argument to cy.visit must be the path.')
|
||||
|
||||
// Add a hash in the URL if it is not present (to use hash-based routing correctly with queryParams).
|
||||
const hashUrl = url.startsWith('/') && url.length > 2 && !url.startsWith('/#') ? `/#${url}` : url
|
||||
let hashUrl = url.startsWith('/') && url.length > 2 && !url.startsWith('/#') ? `/#${url}` : url
|
||||
if (options?.ethereum === 'goerli') hashUrl += `${url.includes('?') ? '&' : '?'}chain=goerli`
|
||||
|
||||
return cy
|
||||
.intercept('/service-worker.js', options?.serviceWorker ? undefined : { statusCode: 404 })
|
||||
@@ -70,29 +68,13 @@ Cypress.Commands.overwrite(
|
||||
}
|
||||
|
||||
// Inject the mock ethereum provider.
|
||||
win.ethereum = provider
|
||||
if (options?.ethereum === 'hardhat') {
|
||||
win.ethereum = provider
|
||||
} else {
|
||||
win.ethereum = injected
|
||||
}
|
||||
},
|
||||
})
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
Cypress.Commands.add('waitForAmplitudeEvent', (eventName, timeout = 5000 /* 5s */) => {
|
||||
const startTime = new Date().getTime()
|
||||
|
||||
function checkRequest() {
|
||||
return cy.wait('@amplitude', { timeout }).then((interception) => {
|
||||
const events = interception.request.body.events
|
||||
const event = events.find((event: any) => event.event_type === eventName)
|
||||
|
||||
if (event) {
|
||||
return cy.wrap(event)
|
||||
} else if (new Date().getTime() - startTime > timeout) {
|
||||
throw new Error(`Event ${eventName} not found within the specified timeout`)
|
||||
} else {
|
||||
return checkRequest()
|
||||
}
|
||||
})
|
||||
}
|
||||
return checkRequest()
|
||||
})
|
||||
|
||||
72
cypress/support/ethereum.ts
Normal file
72
cypress/support/ethereum.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
/**
|
||||
* Updates cy.visit() to include an injected window.ethereum provider.
|
||||
*/
|
||||
|
||||
import { Eip1193Bridge } from '@ethersproject/experimental/lib/eip1193-bridge'
|
||||
import { JsonRpcProvider } from '@ethersproject/providers'
|
||||
import { Wallet } from '@ethersproject/wallet'
|
||||
import { ChainId } from '@uniswap/sdk-core'
|
||||
|
||||
// todo: figure out how env vars actually work in CI
|
||||
// const TEST_PRIVATE_KEY = Cypress.env('INTEGRATION_TEST_PRIVATE_KEY')
|
||||
const TEST_PRIVATE_KEY = '0xe580410d7c37d26c6ad1a837bbae46bc27f9066a466fb3a66e770523b4666d19'
|
||||
|
||||
// address of the above key
|
||||
const TEST_ADDRESS_NEVER_USE = new Wallet(TEST_PRIVATE_KEY).address
|
||||
const CHAIN_ID = ChainId.GOERLI
|
||||
const HEXLIFIED_CHAIN_ID = `0x${CHAIN_ID.toString(16)}`
|
||||
|
||||
const provider = new JsonRpcProvider('https://goerli.infura.io/v3/4bf032f2d38a4ed6bb975b80d6340847', 5)
|
||||
const signer = new Wallet(TEST_PRIVATE_KEY, provider)
|
||||
export const injected = new (class extends Eip1193Bridge {
|
||||
chainId = CHAIN_ID
|
||||
|
||||
async sendAsync(...args: any[]) {
|
||||
console.debug('sendAsync called', ...args)
|
||||
return this.send(...args)
|
||||
}
|
||||
async send(...args: any[]) {
|
||||
console.debug('send called', ...args)
|
||||
const isCallbackForm = typeof args[0] === 'object' && typeof args[1] === 'function'
|
||||
let callback
|
||||
let method
|
||||
let params
|
||||
if (isCallbackForm) {
|
||||
callback = args[1]
|
||||
method = args[0].method
|
||||
params = args[0].params
|
||||
} else {
|
||||
method = args[0]
|
||||
params = args[1]
|
||||
}
|
||||
if (method === 'eth_requestAccounts' || method === 'eth_accounts') {
|
||||
if (isCallbackForm) {
|
||||
callback({ result: [TEST_ADDRESS_NEVER_USE] })
|
||||
} else {
|
||||
return Promise.resolve([TEST_ADDRESS_NEVER_USE])
|
||||
}
|
||||
}
|
||||
if (method === 'eth_chainId') {
|
||||
if (isCallbackForm) {
|
||||
callback(null, { result: HEXLIFIED_CHAIN_ID })
|
||||
} else {
|
||||
return Promise.resolve(HEXLIFIED_CHAIN_ID)
|
||||
}
|
||||
}
|
||||
try {
|
||||
const result = await super.send(method, params)
|
||||
console.debug('result received', method, params, result)
|
||||
if (isCallbackForm) {
|
||||
callback(null, { result })
|
||||
} else {
|
||||
return result
|
||||
}
|
||||
} catch (error) {
|
||||
if (isCallbackForm) {
|
||||
callback(error, null)
|
||||
} else {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
})(signer, provider)
|
||||
@@ -9,8 +9,13 @@ beforeEach(() => {
|
||||
req.headers['origin'] = 'https://app.uniswap.org'
|
||||
})
|
||||
|
||||
// Infura is disabled for cypress tests - calls should be routed through the connected wallet instead.
|
||||
cy.intercept(/infura.io/, { statusCode: 404 })
|
||||
// Infura uses a test endpoint, which allow-lists http://localhost:3000 instead.
|
||||
cy.intercept(/infura.io/, (req) => {
|
||||
req.headers['referer'] = 'http://localhost:3000'
|
||||
req.headers['origin'] = 'http://localhost:3000'
|
||||
req.alias = req.body.method
|
||||
req.continue()
|
||||
})
|
||||
|
||||
// Log requests to hardhat.
|
||||
cy.intercept(/:8545/, logJsonRpc)
|
||||
@@ -19,7 +24,6 @@ beforeEach(() => {
|
||||
cy.intercept('https://api.uniswap.org/v1/amplitude-proxy', (req) => {
|
||||
const requestBody = JSON.stringify(req.body)
|
||||
const byteSize = new Blob([requestBody]).size
|
||||
req.alias = 'amplitude'
|
||||
req.reply(
|
||||
JSON.stringify({
|
||||
code: 200,
|
||||
@@ -45,7 +49,7 @@ function logJsonRpc(req: CyHttpMessages.IncomingHttpRequest) {
|
||||
const log = Cypress.log({
|
||||
autoEnd: false,
|
||||
name: req.body.method,
|
||||
message: req.body.params?.map((param: any) =>
|
||||
message: req.body.params?.map((param: unknown) =>
|
||||
typeof param === 'object' ? '{...}' : param?.toString().substring(0, 10)
|
||||
),
|
||||
})
|
||||
|
||||
@@ -1,13 +1,18 @@
|
||||
{
|
||||
"extends": "../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"composite": false,
|
||||
"esModuleInterop": true,
|
||||
"incremental": true,
|
||||
"isolatedModules": false,
|
||||
"noImplicitAny": false,
|
||||
"lib": ["DOM", "DOM.Iterable", "ESNext"],
|
||||
"noEmit": true,
|
||||
"strict": true,
|
||||
"target": "ES5",
|
||||
"tsBuildInfoFile": "../node_modules/.cache/tsbuildinfo/cypress", // avoid clobbering the build tsbuildinfo
|
||||
"types": ["cypress", "node"],
|
||||
"jsx": "react"
|
||||
},
|
||||
"exclude": ["node_modules"],
|
||||
"include": ["**/*.ts"],
|
||||
"watchOptions": {
|
||||
"excludeDirectories": ["node_modules"]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { ConnectionType } from '../../src/connection/types'
|
||||
import { UserState } from '../../src/state/user/reducer'
|
||||
|
||||
export const CONNECTED_WALLET_USER_STATE: Partial<UserState> = { selectedWallet: ConnectionType.INJECTED }
|
||||
export const CONNECTED_WALLET_USER_STATE: Partial<UserState> = { selectedWallet: 'INJECTED' }
|
||||
|
||||
export const DISCONNECTED_WALLET_USER_STATE: Partial<UserState> = { selectedWallet: undefined }
|
||||
|
||||
@@ -1,50 +0,0 @@
|
||||
# Cloudflare Cloud Functions
|
||||
|
||||
## Purpose
|
||||
|
||||
These functions utilize Cloudflare Functions to dynamically inject meta tags server side for richer link sharing capabilities.
|
||||
|
||||
## Functions
|
||||
|
||||
Currently, there are 2 types of cloudflare functions developed
|
||||
|
||||
- Meta Data Injectors - Workers that inject [Open Graph](https://ogp.me/) standardized meta tags into the `header` of specific webpages.
|
||||
- Currently we support this functionaltiy for three separate webpages: NFT Assets, NFT Collections, and Token Detail Pages
|
||||
- These functions query data from GraphQL and then formats them into HTML `meta` tags to be injected
|
||||
- Dynamically Generated Images - Utilizes Vercel's [Open Graph Image Generation Library](https://vercel.com/docs/concepts/functions/edge-functions/og-image-generation) to create custom thumbnails for specific webpages
|
||||
- Currently supports NFT Assets, NFT Collections, and Token Detail Pages
|
||||
- These functions query data from GraphQL, and utilize `Satori` to convert HTML into a png image response which is then returned when the api is called.
|
||||
- Can be found in the `api/image` folder.
|
||||
|
||||
## Testing
|
||||
|
||||
Testing is done utilizing a custom jest environment as well as Cloudflare's local tester: `wrangler`. Wrangler enables testing locally by running a proxy to wrap `localhost`. Testing can be done the following ways.
|
||||
- Manually by running `yarn start:cloud` to setup wrangler on `localhost:3000`
|
||||
- Automated tests by running `yarn test:cloud` to setup both a jest and wrangler environment and automatically test features
|
||||
|
||||
## Deployment
|
||||
|
||||
Functions will be deployed to Cloudlfare where they will be ran automatically when the appropriate route is hit.
|
||||
|
||||
## Miscellaneous
|
||||
- Caching: In order to speed up webpage requests, repeated GraphQL queries will be saved and pulled using Cloudflare's Cache API.
|
||||
|
||||
## Scripts
|
||||
|
||||
- `yarn start:cloud` (NODE_OPTIONS=--dns-result-order=ipv4first PORT=3001 npx wrangler pages dev --node-compat --proxy=3001 --port=3000 -- yarn start), script to start local wrangler environment
|
||||
- `npx wrangler pages dev`: this basis of this command which starts a local instance of wrangler to test cloud functions
|
||||
- `--node-compat`: wrangler option that enables compatibility with Node.js modules
|
||||
- `--proxy:3001`: telling the proxy to listen on port 3001
|
||||
- `--port=3000`: telling wrangler to run our proxy on port 3000
|
||||
- `NODE_OPTIONS=--dns-result-order=ipv4first`: wrangler still serves to IPv4 which isn't compatible with Node 18 which default resolves to IPv6 so we need to specify to serve to IPv4
|
||||
- `PORT-3001 --yarn start`: runs default yarn start on port 3001
|
||||
- `yarn test:cloud` (NODE_OPTIONS=--experimental-vm-modules yarn jest functions --watch --config=functions/jest.config.json), script to test cloud functions with jest
|
||||
- `NODE_OPTIONS=--experimental-vm-modules`: support for ES Modules and Web Assembly
|
||||
- `--config=functions/jest.config.json`: specifying which config file to use
|
||||
|
||||
## Additional Documents
|
||||
- [Open Graph Protocol](https://ogp.me/)
|
||||
- [Open Graph Image Generation](https://vercel.com/docs/concepts/functions/edge-functions/og-image-generation)
|
||||
- [Cloudflare Workers](https://developers.cloudflare.com/workers/)
|
||||
- [HTML Rewriter](https://developers.cloudflare.com/workers/runtime-apis/html-rewriter/)
|
||||
- [Cache API](https://developers.cloudflare.com/workers/runtime-apis/cache/)
|
||||
@@ -1,20 +0,0 @@
|
||||
import { ApolloClient, InMemoryCache } from '@apollo/client'
|
||||
const GRAPHQL_ENDPOINT = 'https://api.uniswap.org/v1/graphql'
|
||||
|
||||
//TODO: Figure out how to make ApolloClient global variable
|
||||
export default new ApolloClient({
|
||||
connectToDevTools: true,
|
||||
uri: GRAPHQL_ENDPOINT,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Origin: 'https://app.uniswap.org',
|
||||
'User-Agent':
|
||||
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.5735.110 Safari/537.36',
|
||||
},
|
||||
cache: new InMemoryCache(),
|
||||
defaultOptions: {
|
||||
watchQuery: {
|
||||
fetchPolicy: 'cache-first',
|
||||
},
|
||||
},
|
||||
})
|
||||
@@ -1,38 +0,0 @@
|
||||
type MetaTagInjectorInput = {
|
||||
title: string
|
||||
image?: string
|
||||
url: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Listener class for Cloudflare's HTMLRewriter {@link https://developers.cloudflare.com/workers/runtime-apis/html-rewriter}
|
||||
* to inject meta tags into the <head> of an HTML document.
|
||||
*/
|
||||
export class MetaTagInjector implements HTMLRewriterElementContentHandlers {
|
||||
constructor(private input: MetaTagInjectorInput) {}
|
||||
|
||||
append(element: Element, property: string, content: string) {
|
||||
element.append(`<meta property="${property}" content="${content}"/>`, { html: true })
|
||||
}
|
||||
|
||||
element(element: Element) {
|
||||
//Open Graph Tags
|
||||
this.append(element, 'og:title', this.input.title)
|
||||
if (this.input.image) {
|
||||
this.append(element, 'og:image', this.input.image)
|
||||
this.append(element, 'og:image:width', '1200')
|
||||
this.append(element, 'og:image:height', '630')
|
||||
this.append(element, 'og:image:alt', this.input.title)
|
||||
}
|
||||
this.append(element, 'og:type', 'website')
|
||||
this.append(element, 'og:url', this.input.url)
|
||||
|
||||
//Twitter Tags
|
||||
this.append(element, 'twitter:card', 'summary_large_image')
|
||||
this.append(element, 'twitter:title', this.input.title)
|
||||
if (this.input.image) {
|
||||
this.append(element, 'twitter:image', this.input.image)
|
||||
this.append(element, 'twitter:image:alt', this.input.title)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,5 @@
|
||||
"transform": {
|
||||
"'^.+\\.(ts|tsx)?$'": "ts-jest",
|
||||
"^.+\\.(js|jsx)$": "babel-jest"
|
||||
},
|
||||
"testTimeout": 360000,
|
||||
"cacheDirectory": "../node_modules/.cache/cloud-jest"
|
||||
}
|
||||
}
|
||||
}
|
||||
3
functions/nft.test.ts
Normal file
3
functions/nft.test.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
test('example', async () => {
|
||||
expect(true).toBe(true)
|
||||
})
|
||||
@@ -1,15 +0,0 @@
|
||||
/* eslint-disable import/no-unused-modules */
|
||||
import getAsset from '../../utils/getAsset'
|
||||
import getRequest from '../../utils/getRequest'
|
||||
|
||||
export const onRequest: PagesFunction = async ({ params, request, next }) => {
|
||||
const res = next()
|
||||
try {
|
||||
const { index } = params
|
||||
const collectionAddress = index[0]?.toString()
|
||||
const tokenId = index[1]?.toString()
|
||||
return getRequest(res, request.url, () => getAsset(collectionAddress, tokenId, request.url))
|
||||
} catch (e) {
|
||||
return res
|
||||
}
|
||||
}
|
||||
@@ -1,397 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`should inject metadata for valid assets 1`] = `
|
||||
"<!DOCTYPE html>
|
||||
<html translate="no">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
|
||||
<title>Uniswap Interface</title>
|
||||
<meta name="description" content="Swap or provide liquidity on the Uniswap Protocol" />
|
||||
|
||||
<!--
|
||||
. will be replaced with the URL of the \`public\` folder during build.
|
||||
Only files inside the \`public\` folder can be referenced from the HTML.
|
||||
-->
|
||||
<link rel="shortcut icon" type="image/png" href="./favicon.png" />
|
||||
<link rel="apple-touch-icon" sizes="192x192" href="./images/192x192_App_Icon.png" />
|
||||
<link rel="apple-touch-icon" sizes="512x512" href="./images/512x512_App_Icon.png" />
|
||||
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
|
||||
<meta name="theme-color" content="#FC72FF" />
|
||||
<meta
|
||||
http-equiv="Content-Security-Policy"
|
||||
|
||||
content="script-src 'self' 'unsafe-inline'"
|
||||
|
||||
/>
|
||||
|
||||
<!--
|
||||
Apple Smart App Banner for Safari on iOS
|
||||
https://developer.apple.com/documentation/webkit/promoting_apps_with_smart_app_banners
|
||||
-->
|
||||
<meta name="apple-itunes-app" content="app-id=6443944476">
|
||||
|
||||
<!--
|
||||
manifest.json provides metadata used when the app is installed as a PWA.
|
||||
See https://developers.google.com/web/fundamentals/web-app-manifest/
|
||||
-->
|
||||
<link rel="manifest" href="./manifest.json" />
|
||||
|
||||
<link rel="preload" href="./fonts/Inter-roman.var.woff2" as="font" type="font/woff2" crossorigin />
|
||||
|
||||
<style>
|
||||
* {
|
||||
font-family: 'Inter', sans-serif;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/**
|
||||
Explicitly load Inter var from public/ so it does not block LCP's critical path.
|
||||
*/
|
||||
@font-face {
|
||||
font-family: 'Inter custom';
|
||||
font-weight: 100 900;
|
||||
font-style: normal;
|
||||
font-display: block;
|
||||
font-named-instance: 'Regular';
|
||||
src: url(./fonts/Inter-roman.var.woff2) format('woff2 supports variations(gvar)'),
|
||||
url(./fonts/Inter-roman.var.woff2) format('woff2-variations'),
|
||||
url(./fonts/Inter-roman.var.woff2) format('woff2');
|
||||
}
|
||||
|
||||
@supports (font-variation-settings: normal) {
|
||||
* {
|
||||
font-family: 'Inter custom', sans-serif;
|
||||
}
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
button {
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
html {
|
||||
font-size: 16px;
|
||||
font-variant: none;
|
||||
font-smooth: always;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
|
||||
}
|
||||
|
||||
/* Use this to apply network-specific gradient backgrounds, in RadialGradientByChainUpdater.ts */
|
||||
#background-radial-gradient {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
pointer-events: none;
|
||||
width: 200vw;
|
||||
height: 200vh;
|
||||
transform: translate(-50vw, -100vh);
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
html,
|
||||
body,
|
||||
#root {
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
html {
|
||||
background: linear-gradient(180deg, #202738 0%, #070816 100%);
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
html {
|
||||
background: radial-gradient(100% 100% at 50% 0%, rgba(255, 184, 226, 0.51) 0%, rgba(255, 255, 255, 0) 100%), #FFFFFF
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<script defer src="./static/js/bundle.js"></script><meta property="og:title" content="Azuki #2550"/><meta property="og:image" content="https://cdn.center.app/1/0xED5AF388653567Af2F388E6224dC7C4b3241C544/2550/d268b7f60a56306ced68b9762709ceaff4f1ee939f3150e7363fae300a59da12.png"/><meta property="og:image:width" content="1200"/><meta property="og:image:height" content="630"/><meta property="og:image:alt" content="Azuki #2550"/><meta property="og:type" content="website"/><meta property="og:url" content="http://127.0.0.1:3000/nfts/asset/0xed5af388653567af2f388e6224dc7c4b3241c544/2550"/><meta property="twitter:card" content="summary_large_image"/><meta property="twitter:title" content="Azuki #2550"/><meta property="twitter:image" content="https://cdn.center.app/1/0xED5AF388653567Af2F388E6224dC7C4b3241C544/2550/d268b7f60a56306ced68b9762709ceaff4f1ee939f3150e7363fae300a59da12.png"/><meta property="twitter:image:alt" content="Azuki #2550"/></head>
|
||||
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
|
||||
<div id="root">
|
||||
<!-- Triggers the font to load immediately and then is replaced by the app -->
|
||||
<div> </div>
|
||||
</div>
|
||||
|
||||
<div id="background-radial-gradient"></div>
|
||||
</body>
|
||||
</html>
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`should inject metadata for valid assets 2`] = `
|
||||
"<!DOCTYPE html>
|
||||
<html translate="no">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
|
||||
<title>Uniswap Interface</title>
|
||||
<meta name="description" content="Swap or provide liquidity on the Uniswap Protocol" />
|
||||
|
||||
<!--
|
||||
. will be replaced with the URL of the \`public\` folder during build.
|
||||
Only files inside the \`public\` folder can be referenced from the HTML.
|
||||
-->
|
||||
<link rel="shortcut icon" type="image/png" href="./favicon.png" />
|
||||
<link rel="apple-touch-icon" sizes="192x192" href="./images/192x192_App_Icon.png" />
|
||||
<link rel="apple-touch-icon" sizes="512x512" href="./images/512x512_App_Icon.png" />
|
||||
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
|
||||
<meta name="theme-color" content="#FC72FF" />
|
||||
<meta
|
||||
http-equiv="Content-Security-Policy"
|
||||
|
||||
content="script-src 'self' 'unsafe-inline'"
|
||||
|
||||
/>
|
||||
|
||||
<!--
|
||||
Apple Smart App Banner for Safari on iOS
|
||||
https://developer.apple.com/documentation/webkit/promoting_apps_with_smart_app_banners
|
||||
-->
|
||||
<meta name="apple-itunes-app" content="app-id=6443944476">
|
||||
|
||||
<!--
|
||||
manifest.json provides metadata used when the app is installed as a PWA.
|
||||
See https://developers.google.com/web/fundamentals/web-app-manifest/
|
||||
-->
|
||||
<link rel="manifest" href="./manifest.json" />
|
||||
|
||||
<link rel="preload" href="./fonts/Inter-roman.var.woff2" as="font" type="font/woff2" crossorigin />
|
||||
|
||||
<style>
|
||||
* {
|
||||
font-family: 'Inter', sans-serif;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/**
|
||||
Explicitly load Inter var from public/ so it does not block LCP's critical path.
|
||||
*/
|
||||
@font-face {
|
||||
font-family: 'Inter custom';
|
||||
font-weight: 100 900;
|
||||
font-style: normal;
|
||||
font-display: block;
|
||||
font-named-instance: 'Regular';
|
||||
src: url(./fonts/Inter-roman.var.woff2) format('woff2 supports variations(gvar)'),
|
||||
url(./fonts/Inter-roman.var.woff2) format('woff2-variations'),
|
||||
url(./fonts/Inter-roman.var.woff2) format('woff2');
|
||||
}
|
||||
|
||||
@supports (font-variation-settings: normal) {
|
||||
* {
|
||||
font-family: 'Inter custom', sans-serif;
|
||||
}
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
button {
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
html {
|
||||
font-size: 16px;
|
||||
font-variant: none;
|
||||
font-smooth: always;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
|
||||
}
|
||||
|
||||
/* Use this to apply network-specific gradient backgrounds, in RadialGradientByChainUpdater.ts */
|
||||
#background-radial-gradient {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
pointer-events: none;
|
||||
width: 200vw;
|
||||
height: 200vh;
|
||||
transform: translate(-50vw, -100vh);
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
html,
|
||||
body,
|
||||
#root {
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
html {
|
||||
background: linear-gradient(180deg, #202738 0%, #070816 100%);
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
html {
|
||||
background: radial-gradient(100% 100% at 50% 0%, rgba(255, 184, 226, 0.51) 0%, rgba(255, 255, 255, 0) 100%), #FFFFFF
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<script defer src="./static/js/bundle.js"></script><meta property="og:title" content="Bored Ape Yacht Club #3735"/><meta property="og:image" content="https://cdn.center.app/v2/1/697f69bb495aaa24c66638cae921977354f0b8274fc2e2814e455f355e67f01d/88c2ac6b73288e41051d3fd58ff3cef1f4908403f05f4a7d2a8435d003758529.png"/><meta property="og:image:width" content="1200"/><meta property="og:image:height" content="630"/><meta property="og:image:alt" content="Bored Ape Yacht Club #3735"/><meta property="og:type" content="website"/><meta property="og:url" content="http://127.0.0.1:3000/nfts/asset/0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d/3735"/><meta property="twitter:card" content="summary_large_image"/><meta property="twitter:title" content="Bored Ape Yacht Club #3735"/><meta property="twitter:image" content="https://cdn.center.app/v2/1/697f69bb495aaa24c66638cae921977354f0b8274fc2e2814e455f355e67f01d/88c2ac6b73288e41051d3fd58ff3cef1f4908403f05f4a7d2a8435d003758529.png"/><meta property="twitter:image:alt" content="Bored Ape Yacht Club #3735"/></head>
|
||||
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
|
||||
<div id="root">
|
||||
<!-- Triggers the font to load immediately and then is replaced by the app -->
|
||||
<div> </div>
|
||||
</div>
|
||||
|
||||
<div id="background-radial-gradient"></div>
|
||||
</body>
|
||||
</html>
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`should inject metadata for valid assets 3`] = `
|
||||
"<!DOCTYPE html>
|
||||
<html translate="no">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
|
||||
<title>Uniswap Interface</title>
|
||||
<meta name="description" content="Swap or provide liquidity on the Uniswap Protocol" />
|
||||
|
||||
<!--
|
||||
. will be replaced with the URL of the \`public\` folder during build.
|
||||
Only files inside the \`public\` folder can be referenced from the HTML.
|
||||
-->
|
||||
<link rel="shortcut icon" type="image/png" href="./favicon.png" />
|
||||
<link rel="apple-touch-icon" sizes="192x192" href="./images/192x192_App_Icon.png" />
|
||||
<link rel="apple-touch-icon" sizes="512x512" href="./images/512x512_App_Icon.png" />
|
||||
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
|
||||
<meta name="theme-color" content="#FC72FF" />
|
||||
<meta
|
||||
http-equiv="Content-Security-Policy"
|
||||
|
||||
content="script-src 'self' 'unsafe-inline'"
|
||||
|
||||
/>
|
||||
|
||||
<!--
|
||||
Apple Smart App Banner for Safari on iOS
|
||||
https://developer.apple.com/documentation/webkit/promoting_apps_with_smart_app_banners
|
||||
-->
|
||||
<meta name="apple-itunes-app" content="app-id=6443944476">
|
||||
|
||||
<!--
|
||||
manifest.json provides metadata used when the app is installed as a PWA.
|
||||
See https://developers.google.com/web/fundamentals/web-app-manifest/
|
||||
-->
|
||||
<link rel="manifest" href="./manifest.json" />
|
||||
|
||||
<link rel="preload" href="./fonts/Inter-roman.var.woff2" as="font" type="font/woff2" crossorigin />
|
||||
|
||||
<style>
|
||||
* {
|
||||
font-family: 'Inter', sans-serif;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/**
|
||||
Explicitly load Inter var from public/ so it does not block LCP's critical path.
|
||||
*/
|
||||
@font-face {
|
||||
font-family: 'Inter custom';
|
||||
font-weight: 100 900;
|
||||
font-style: normal;
|
||||
font-display: block;
|
||||
font-named-instance: 'Regular';
|
||||
src: url(./fonts/Inter-roman.var.woff2) format('woff2 supports variations(gvar)'),
|
||||
url(./fonts/Inter-roman.var.woff2) format('woff2-variations'),
|
||||
url(./fonts/Inter-roman.var.woff2) format('woff2');
|
||||
}
|
||||
|
||||
@supports (font-variation-settings: normal) {
|
||||
* {
|
||||
font-family: 'Inter custom', sans-serif;
|
||||
}
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
button {
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
html {
|
||||
font-size: 16px;
|
||||
font-variant: none;
|
||||
font-smooth: always;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
|
||||
}
|
||||
|
||||
/* Use this to apply network-specific gradient backgrounds, in RadialGradientByChainUpdater.ts */
|
||||
#background-radial-gradient {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
pointer-events: none;
|
||||
width: 200vw;
|
||||
height: 200vh;
|
||||
transform: translate(-50vw, -100vh);
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
html,
|
||||
body,
|
||||
#root {
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
html {
|
||||
background: linear-gradient(180deg, #202738 0%, #070816 100%);
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
html {
|
||||
background: radial-gradient(100% 100% at 50% 0%, rgba(255, 184, 226, 0.51) 0%, rgba(255, 255, 255, 0) 100%), #FFFFFF
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<script defer src="./static/js/bundle.js"></script><meta property="og:title" content="CryptoPunk #3947"/><meta property="og:image" content="https://cdn.center.app/1/0xb47e3cd837dDF8e4c57F05d70Ab865de6e193BBB/3947/62319d784e7a816d190aa184ffe58550d6ed8eb2e117b218e2ac02f126538ee6.png"/><meta property="og:image:width" content="1200"/><meta property="og:image:height" content="630"/><meta property="og:image:alt" content="CryptoPunk #3947"/><meta property="og:type" content="website"/><meta property="og:url" content="http://127.0.0.1:3000/nfts/asset/0xb47e3cd837ddf8e4c57f05d70ab865de6e193bbb/3947"/><meta property="twitter:card" content="summary_large_image"/><meta property="twitter:title" content="CryptoPunk #3947"/><meta property="twitter:image" content="https://cdn.center.app/1/0xb47e3cd837dDF8e4c57F05d70Ab865de6e193BBB/3947/62319d784e7a816d190aa184ffe58550d6ed8eb2e117b218e2ac02f126538ee6.png"/><meta property="twitter:image:alt" content="CryptoPunk #3947"/></head>
|
||||
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
|
||||
<div id="root">
|
||||
<!-- Triggers the font to load immediately and then is replaced by the app -->
|
||||
<div> </div>
|
||||
</div>
|
||||
|
||||
<div id="background-radial-gradient"></div>
|
||||
</body>
|
||||
</html>
|
||||
"
|
||||
`;
|
||||
@@ -1,64 +0,0 @@
|
||||
const assets = [
|
||||
{
|
||||
address: '0xed5af388653567af2f388e6224dc7c4b3241c544',
|
||||
assetId: '2550',
|
||||
collectionName: 'Azuki',
|
||||
image:
|
||||
'https://cdn.center.app/1/0xED5AF388653567Af2F388E6224dC7C4b3241C544/2550/d268b7f60a56306ced68b9762709ceaff4f1ee939f3150e7363fae300a59da12.png',
|
||||
},
|
||||
{
|
||||
address: '0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d',
|
||||
assetId: '3735',
|
||||
collectionName: 'Bored Ape Yacht Club',
|
||||
image:
|
||||
'https://cdn.center.app/v2/1/697f69bb495aaa24c66638cae921977354f0b8274fc2e2814e455f355e67f01d/88c2ac6b73288e41051d3fd58ff3cef1f4908403f05f4a7d2a8435d003758529.png',
|
||||
},
|
||||
{
|
||||
address: '0xb47e3cd837ddf8e4c57f05d70ab865de6e193bbb',
|
||||
assetId: '3947',
|
||||
collectionName: 'CryptoPunk',
|
||||
image:
|
||||
'https://cdn.center.app/1/0xb47e3cd837dDF8e4c57F05d70Ab865de6e193BBB/3947/62319d784e7a816d190aa184ffe58550d6ed8eb2e117b218e2ac02f126538ee6.png',
|
||||
},
|
||||
]
|
||||
|
||||
test.each(assets)('should inject metadata for valid assets', async (nft) => {
|
||||
const url = 'http://127.0.0.1:3000/nfts/asset/' + nft.address + '/' + nft.assetId
|
||||
const body = await fetch(new Request(url)).then((res) => res.text())
|
||||
expect(body).toMatchSnapshot()
|
||||
expect(body).toContain(`<meta property="og:title" content="${nft.collectionName} #${nft.assetId}"/>`)
|
||||
expect(body).toContain(`<meta property="og:image" content="${nft.image}"/>`)
|
||||
expect(body).toContain(`<meta property="og:image:width" content="1200"/>`)
|
||||
expect(body).toContain(`<meta property="og:image:height" content="630"/>`)
|
||||
expect(body).toContain(`<meta property="og:type" content="website"/>`)
|
||||
expect(body).toContain(`<meta property="og:url" content="${url}"/>`)
|
||||
expect(body).toContain(`<meta property="og:image:alt" content="${nft.collectionName} #${nft.assetId}"/>`)
|
||||
expect(body).toContain(`<meta property="twitter:card" content="summary_large_image"/>`)
|
||||
expect(body).toContain(`<meta property="twitter:title" content="${nft.collectionName} #${nft.assetId}"/>`)
|
||||
expect(body).toContain(`<meta property="twitter:image" content="${nft.image}"/>`)
|
||||
expect(body).toContain(`<meta property="twitter:image:alt" content="${nft.collectionName} #${nft.assetId}"/>`)
|
||||
})
|
||||
|
||||
const invalidAssets = [
|
||||
'http://127.0.0.1:3000/nfts/asset/0xed5af388653567af2f388e6224dc7c4b3241c544/100000',
|
||||
'http://127.0.0.1:3000/nfts/asset/0xed5af388653567af2f388e6224dc7c4b3241c544',
|
||||
'http://127.0.0.1:3000/nfts/asset/0xed5af388653567af2f388e6224dc7c4b3241c545',
|
||||
'http://127.0.0.1:3000/nfts/asset/0xed5af388653567af2f388e6224dc7c4b3241c544/-1',
|
||||
'http://127.0.0.1:3000/nfts/asset/0xed5af388653567af2f388e6224dc7c4b3241c544//',
|
||||
'http://127.0.0.1:3000/nfts/asset/0xed5af388653567af2f388e6224dc7c4b3241c544//2550',
|
||||
]
|
||||
|
||||
test.each(invalidAssets)('should not inject metadata for invalid asset calls', async (url) => {
|
||||
const body = await fetch(new Request(url)).then((res) => res.text())
|
||||
expect(body).not.toContain('og:title')
|
||||
expect(body).not.toContain('og:image')
|
||||
expect(body).not.toContain('og:image:width')
|
||||
expect(body).not.toContain('og:image:height')
|
||||
expect(body).not.toContain('og:type')
|
||||
expect(body).not.toContain('og:url')
|
||||
expect(body).not.toContain('og:image:alt')
|
||||
expect(body).not.toContain('twitter:card')
|
||||
expect(body).not.toContain('twitter:title')
|
||||
expect(body).not.toContain('twitter:image')
|
||||
expect(body).not.toContain('twitter:image:alt')
|
||||
})
|
||||
@@ -1,14 +0,0 @@
|
||||
/* eslint-disable import/no-unused-modules */
|
||||
import getCollection from '../../utils/getCollection'
|
||||
import getRequest from '../../utils/getRequest'
|
||||
|
||||
export const onRequest: PagesFunction = async ({ params, request, next }) => {
|
||||
const res = next()
|
||||
try {
|
||||
const { index } = params
|
||||
const collectionAddress = index?.toString()
|
||||
return getRequest(res, request.url, () => getCollection(collectionAddress, request.url))
|
||||
} catch (e) {
|
||||
return res
|
||||
}
|
||||
}
|
||||
@@ -1,397 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`should inject metadata for valid collections 1`] = `
|
||||
"<!DOCTYPE html>
|
||||
<html translate="no">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
|
||||
<title>Uniswap Interface</title>
|
||||
<meta name="description" content="Swap or provide liquidity on the Uniswap Protocol" />
|
||||
|
||||
<!--
|
||||
. will be replaced with the URL of the \`public\` folder during build.
|
||||
Only files inside the \`public\` folder can be referenced from the HTML.
|
||||
-->
|
||||
<link rel="shortcut icon" type="image/png" href="./favicon.png" />
|
||||
<link rel="apple-touch-icon" sizes="192x192" href="./images/192x192_App_Icon.png" />
|
||||
<link rel="apple-touch-icon" sizes="512x512" href="./images/512x512_App_Icon.png" />
|
||||
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
|
||||
<meta name="theme-color" content="#FC72FF" />
|
||||
<meta
|
||||
http-equiv="Content-Security-Policy"
|
||||
|
||||
content="script-src 'self' 'unsafe-inline'"
|
||||
|
||||
/>
|
||||
|
||||
<!--
|
||||
Apple Smart App Banner for Safari on iOS
|
||||
https://developer.apple.com/documentation/webkit/promoting_apps_with_smart_app_banners
|
||||
-->
|
||||
<meta name="apple-itunes-app" content="app-id=6443944476">
|
||||
|
||||
<!--
|
||||
manifest.json provides metadata used when the app is installed as a PWA.
|
||||
See https://developers.google.com/web/fundamentals/web-app-manifest/
|
||||
-->
|
||||
<link rel="manifest" href="./manifest.json" />
|
||||
|
||||
<link rel="preload" href="./fonts/Inter-roman.var.woff2" as="font" type="font/woff2" crossorigin />
|
||||
|
||||
<style>
|
||||
* {
|
||||
font-family: 'Inter', sans-serif;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/**
|
||||
Explicitly load Inter var from public/ so it does not block LCP's critical path.
|
||||
*/
|
||||
@font-face {
|
||||
font-family: 'Inter custom';
|
||||
font-weight: 100 900;
|
||||
font-style: normal;
|
||||
font-display: block;
|
||||
font-named-instance: 'Regular';
|
||||
src: url(./fonts/Inter-roman.var.woff2) format('woff2 supports variations(gvar)'),
|
||||
url(./fonts/Inter-roman.var.woff2) format('woff2-variations'),
|
||||
url(./fonts/Inter-roman.var.woff2) format('woff2');
|
||||
}
|
||||
|
||||
@supports (font-variation-settings: normal) {
|
||||
* {
|
||||
font-family: 'Inter custom', sans-serif;
|
||||
}
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
button {
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
html {
|
||||
font-size: 16px;
|
||||
font-variant: none;
|
||||
font-smooth: always;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
|
||||
}
|
||||
|
||||
/* Use this to apply network-specific gradient backgrounds, in RadialGradientByChainUpdater.ts */
|
||||
#background-radial-gradient {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
pointer-events: none;
|
||||
width: 200vw;
|
||||
height: 200vh;
|
||||
transform: translate(-50vw, -100vh);
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
html,
|
||||
body,
|
||||
#root {
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
html {
|
||||
background: linear-gradient(180deg, #202738 0%, #070816 100%);
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
html {
|
||||
background: radial-gradient(100% 100% at 50% 0%, rgba(255, 184, 226, 0.51) 0%, rgba(255, 255, 255, 0) 100%), #FFFFFF
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<script defer src="./static/js/bundle.js"></script><meta property="og:title" content="Azuki on Uniswap"/><meta property="og:image" content="https://i.seadn.io/gae/H8jOCJuQokNqGBpkBN5wk1oZwO7LM8bNnrHCaekV2nKjnCqw6UB5oaH8XyNeBDj6bA_n1mjejzhFQUP3O1NfjFLHr3FOaeHcTOOT?w=500&auto=format"/><meta property="og:image:width" content="1200"/><meta property="og:image:height" content="630"/><meta property="og:image:alt" content="Azuki on Uniswap"/><meta property="og:type" content="website"/><meta property="og:url" content="http://127.0.0.1:3000/nfts/collection/0xed5af388653567af2f388e6224dc7c4b3241c544"/><meta property="twitter:card" content="summary_large_image"/><meta property="twitter:title" content="Azuki on Uniswap"/><meta property="twitter:image" content="https://i.seadn.io/gae/H8jOCJuQokNqGBpkBN5wk1oZwO7LM8bNnrHCaekV2nKjnCqw6UB5oaH8XyNeBDj6bA_n1mjejzhFQUP3O1NfjFLHr3FOaeHcTOOT?w=500&auto=format"/><meta property="twitter:image:alt" content="Azuki on Uniswap"/></head>
|
||||
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
|
||||
<div id="root">
|
||||
<!-- Triggers the font to load immediately and then is replaced by the app -->
|
||||
<div> </div>
|
||||
</div>
|
||||
|
||||
<div id="background-radial-gradient"></div>
|
||||
</body>
|
||||
</html>
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`should inject metadata for valid collections 2`] = `
|
||||
"<!DOCTYPE html>
|
||||
<html translate="no">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
|
||||
<title>Uniswap Interface</title>
|
||||
<meta name="description" content="Swap or provide liquidity on the Uniswap Protocol" />
|
||||
|
||||
<!--
|
||||
. will be replaced with the URL of the \`public\` folder during build.
|
||||
Only files inside the \`public\` folder can be referenced from the HTML.
|
||||
-->
|
||||
<link rel="shortcut icon" type="image/png" href="./favicon.png" />
|
||||
<link rel="apple-touch-icon" sizes="192x192" href="./images/192x192_App_Icon.png" />
|
||||
<link rel="apple-touch-icon" sizes="512x512" href="./images/512x512_App_Icon.png" />
|
||||
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
|
||||
<meta name="theme-color" content="#FC72FF" />
|
||||
<meta
|
||||
http-equiv="Content-Security-Policy"
|
||||
|
||||
content="script-src 'self' 'unsafe-inline'"
|
||||
|
||||
/>
|
||||
|
||||
<!--
|
||||
Apple Smart App Banner for Safari on iOS
|
||||
https://developer.apple.com/documentation/webkit/promoting_apps_with_smart_app_banners
|
||||
-->
|
||||
<meta name="apple-itunes-app" content="app-id=6443944476">
|
||||
|
||||
<!--
|
||||
manifest.json provides metadata used when the app is installed as a PWA.
|
||||
See https://developers.google.com/web/fundamentals/web-app-manifest/
|
||||
-->
|
||||
<link rel="manifest" href="./manifest.json" />
|
||||
|
||||
<link rel="preload" href="./fonts/Inter-roman.var.woff2" as="font" type="font/woff2" crossorigin />
|
||||
|
||||
<style>
|
||||
* {
|
||||
font-family: 'Inter', sans-serif;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/**
|
||||
Explicitly load Inter var from public/ so it does not block LCP's critical path.
|
||||
*/
|
||||
@font-face {
|
||||
font-family: 'Inter custom';
|
||||
font-weight: 100 900;
|
||||
font-style: normal;
|
||||
font-display: block;
|
||||
font-named-instance: 'Regular';
|
||||
src: url(./fonts/Inter-roman.var.woff2) format('woff2 supports variations(gvar)'),
|
||||
url(./fonts/Inter-roman.var.woff2) format('woff2-variations'),
|
||||
url(./fonts/Inter-roman.var.woff2) format('woff2');
|
||||
}
|
||||
|
||||
@supports (font-variation-settings: normal) {
|
||||
* {
|
||||
font-family: 'Inter custom', sans-serif;
|
||||
}
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
button {
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
html {
|
||||
font-size: 16px;
|
||||
font-variant: none;
|
||||
font-smooth: always;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
|
||||
}
|
||||
|
||||
/* Use this to apply network-specific gradient backgrounds, in RadialGradientByChainUpdater.ts */
|
||||
#background-radial-gradient {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
pointer-events: none;
|
||||
width: 200vw;
|
||||
height: 200vh;
|
||||
transform: translate(-50vw, -100vh);
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
html,
|
||||
body,
|
||||
#root {
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
html {
|
||||
background: linear-gradient(180deg, #202738 0%, #070816 100%);
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
html {
|
||||
background: radial-gradient(100% 100% at 50% 0%, rgba(255, 184, 226, 0.51) 0%, rgba(255, 255, 255, 0) 100%), #FFFFFF
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<script defer src="./static/js/bundle.js"></script><meta property="og:title" content="Bored Ape Yacht Club on Uniswap"/><meta property="og:image" content="https://i.seadn.io/gae/Ju9CkWtV-1Okvf45wo8UctR-M9He2PjILP0oOvxE89AyiPPGtrR3gysu1Zgy0hjd2xKIgjJJtWIc0ybj4Vd7wv8t3pxDGHoJBzDB?w=500&auto=format"/><meta property="og:image:width" content="1200"/><meta property="og:image:height" content="630"/><meta property="og:image:alt" content="Bored Ape Yacht Club on Uniswap"/><meta property="og:type" content="website"/><meta property="og:url" content="http://127.0.0.1:3000/nfts/collection/0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d"/><meta property="twitter:card" content="summary_large_image"/><meta property="twitter:title" content="Bored Ape Yacht Club on Uniswap"/><meta property="twitter:image" content="https://i.seadn.io/gae/Ju9CkWtV-1Okvf45wo8UctR-M9He2PjILP0oOvxE89AyiPPGtrR3gysu1Zgy0hjd2xKIgjJJtWIc0ybj4Vd7wv8t3pxDGHoJBzDB?w=500&auto=format"/><meta property="twitter:image:alt" content="Bored Ape Yacht Club on Uniswap"/></head>
|
||||
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
|
||||
<div id="root">
|
||||
<!-- Triggers the font to load immediately and then is replaced by the app -->
|
||||
<div> </div>
|
||||
</div>
|
||||
|
||||
<div id="background-radial-gradient"></div>
|
||||
</body>
|
||||
</html>
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`should inject metadata for valid collections 3`] = `
|
||||
"<!DOCTYPE html>
|
||||
<html translate="no">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
|
||||
<title>Uniswap Interface</title>
|
||||
<meta name="description" content="Swap or provide liquidity on the Uniswap Protocol" />
|
||||
|
||||
<!--
|
||||
. will be replaced with the URL of the \`public\` folder during build.
|
||||
Only files inside the \`public\` folder can be referenced from the HTML.
|
||||
-->
|
||||
<link rel="shortcut icon" type="image/png" href="./favicon.png" />
|
||||
<link rel="apple-touch-icon" sizes="192x192" href="./images/192x192_App_Icon.png" />
|
||||
<link rel="apple-touch-icon" sizes="512x512" href="./images/512x512_App_Icon.png" />
|
||||
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
|
||||
<meta name="theme-color" content="#FC72FF" />
|
||||
<meta
|
||||
http-equiv="Content-Security-Policy"
|
||||
|
||||
content="script-src 'self' 'unsafe-inline'"
|
||||
|
||||
/>
|
||||
|
||||
<!--
|
||||
Apple Smart App Banner for Safari on iOS
|
||||
https://developer.apple.com/documentation/webkit/promoting_apps_with_smart_app_banners
|
||||
-->
|
||||
<meta name="apple-itunes-app" content="app-id=6443944476">
|
||||
|
||||
<!--
|
||||
manifest.json provides metadata used when the app is installed as a PWA.
|
||||
See https://developers.google.com/web/fundamentals/web-app-manifest/
|
||||
-->
|
||||
<link rel="manifest" href="./manifest.json" />
|
||||
|
||||
<link rel="preload" href="./fonts/Inter-roman.var.woff2" as="font" type="font/woff2" crossorigin />
|
||||
|
||||
<style>
|
||||
* {
|
||||
font-family: 'Inter', sans-serif;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/**
|
||||
Explicitly load Inter var from public/ so it does not block LCP's critical path.
|
||||
*/
|
||||
@font-face {
|
||||
font-family: 'Inter custom';
|
||||
font-weight: 100 900;
|
||||
font-style: normal;
|
||||
font-display: block;
|
||||
font-named-instance: 'Regular';
|
||||
src: url(./fonts/Inter-roman.var.woff2) format('woff2 supports variations(gvar)'),
|
||||
url(./fonts/Inter-roman.var.woff2) format('woff2-variations'),
|
||||
url(./fonts/Inter-roman.var.woff2) format('woff2');
|
||||
}
|
||||
|
||||
@supports (font-variation-settings: normal) {
|
||||
* {
|
||||
font-family: 'Inter custom', sans-serif;
|
||||
}
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
button {
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
html {
|
||||
font-size: 16px;
|
||||
font-variant: none;
|
||||
font-smooth: always;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
|
||||
}
|
||||
|
||||
/* Use this to apply network-specific gradient backgrounds, in RadialGradientByChainUpdater.ts */
|
||||
#background-radial-gradient {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
pointer-events: none;
|
||||
width: 200vw;
|
||||
height: 200vh;
|
||||
transform: translate(-50vw, -100vh);
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
html,
|
||||
body,
|
||||
#root {
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
html {
|
||||
background: linear-gradient(180deg, #202738 0%, #070816 100%);
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
html {
|
||||
background: radial-gradient(100% 100% at 50% 0%, rgba(255, 184, 226, 0.51) 0%, rgba(255, 255, 255, 0) 100%), #FFFFFF
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<script defer src="./static/js/bundle.js"></script><meta property="og:title" content="CLONE X - X TAKASHI MURAKAMI on Uniswap"/><meta property="og:image" content="https://i.seadn.io/gae/XN0XuD8Uh3jyRWNtPTFeXJg_ht8m5ofDx6aHklOiy4amhFuWUa0JaR6It49AH8tlnYS386Q0TW_-Lmedn0UET_ko1a3CbJGeu5iHMg?w=500&auto=format"/><meta property="og:image:width" content="1200"/><meta property="og:image:height" content="630"/><meta property="og:image:alt" content="CLONE X - X TAKASHI MURAKAMI on Uniswap"/><meta property="og:type" content="website"/><meta property="og:url" content="http://127.0.0.1:3000/nfts/collection/0x49cf6f5d44e70224e2e23fdcdd2c053f30ada28b"/><meta property="twitter:card" content="summary_large_image"/><meta property="twitter:title" content="CLONE X - X TAKASHI MURAKAMI on Uniswap"/><meta property="twitter:image" content="https://i.seadn.io/gae/XN0XuD8Uh3jyRWNtPTFeXJg_ht8m5ofDx6aHklOiy4amhFuWUa0JaR6It49AH8tlnYS386Q0TW_-Lmedn0UET_ko1a3CbJGeu5iHMg?w=500&auto=format"/><meta property="twitter:image:alt" content="CLONE X - X TAKASHI MURAKAMI on Uniswap"/></head>
|
||||
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
|
||||
<div id="root">
|
||||
<!-- Triggers the font to load immediately and then is replaced by the app -->
|
||||
<div> </div>
|
||||
</div>
|
||||
|
||||
<div id="background-radial-gradient"></div>
|
||||
</body>
|
||||
</html>
|
||||
"
|
||||
`;
|
||||
@@ -1,63 +0,0 @@
|
||||
const collections = [
|
||||
{
|
||||
address: '0xed5af388653567af2f388e6224dc7c4b3241c544',
|
||||
collectionName: 'Azuki',
|
||||
image:
|
||||
'https://i.seadn.io/gae/H8jOCJuQokNqGBpkBN5wk1oZwO7LM8bNnrHCaekV2nKjnCqw6UB5oaH8XyNeBDj6bA_n1mjejzhFQUP3O1NfjFLHr3FOaeHcTOOT?w=500&auto=format',
|
||||
},
|
||||
{
|
||||
address: '0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d',
|
||||
collectionName: 'Bored Ape Yacht Club',
|
||||
image:
|
||||
'https://i.seadn.io/gae/Ju9CkWtV-1Okvf45wo8UctR-M9He2PjILP0oOvxE89AyiPPGtrR3gysu1Zgy0hjd2xKIgjJJtWIc0ybj4Vd7wv8t3pxDGHoJBzDB?w=500&auto=format',
|
||||
},
|
||||
{
|
||||
address: '0x49cf6f5d44e70224e2e23fdcdd2c053f30ada28b',
|
||||
collectionName: 'CLONE X - X TAKASHI MURAKAMI',
|
||||
image:
|
||||
'https://i.seadn.io/gae/XN0XuD8Uh3jyRWNtPTFeXJg_ht8m5ofDx6aHklOiy4amhFuWUa0JaR6It49AH8tlnYS386Q0TW_-Lmedn0UET_ko1a3CbJGeu5iHMg?w=500&auto=format',
|
||||
},
|
||||
]
|
||||
|
||||
test.each(collections)('should inject metadata for valid collections', async (collection) => {
|
||||
const url = 'http://127.0.0.1:3000/nfts/collection/' + collection.address
|
||||
const body = await fetch(new Request(url)).then((res) => res.text())
|
||||
expect(body).toMatchSnapshot()
|
||||
expect(body).toContain(`<meta property="og:title" content="${collection.collectionName} on Uniswap"/>`)
|
||||
expect(body).toContain(`<meta property="og:image" content="${collection.image}"/>`)
|
||||
expect(body).toContain(`<meta property="og:image:width" content="1200"/>`)
|
||||
expect(body).toContain(`<meta property="og:image:height" content="630"/>`)
|
||||
expect(body).toContain(`<meta property="og:type" content="website"/>`)
|
||||
expect(body).toContain(`<meta property="og:url" content="${url}"/>`)
|
||||
expect(body).toContain(`<meta property="og:image:alt" content="${collection.collectionName} on Uniswap"/>`)
|
||||
expect(body).toContain(`<meta property="twitter:card" content="summary_large_image"/>`)
|
||||
expect(body).toContain(`<meta property="twitter:title" content="${collection.collectionName} on Uniswap"/>`)
|
||||
expect(body).toContain(`<meta property="twitter:image" content="${collection.image}"/>`)
|
||||
expect(body).toContain(`<meta property="twitter:image:alt" content="${collection.collectionName} on Uniswap"/>`)
|
||||
})
|
||||
|
||||
const invalidCollections = [
|
||||
'http://127.0.0.1:3000/nfts/collection/0xed5af388653567af2f388e6224dc7c4b3241c545',
|
||||
'http://127.0.0.1:3000/nfts/collection/0xed5af388653567af2f388e6224dc7c4b3241c545/10',
|
||||
'http://127.0.0.1:3000/nfts/collection/0xed5af388653567af2f388e6224dc7c4b3241c545//',
|
||||
'http://127.0.0.1:3000/nfts/collection',
|
||||
]
|
||||
|
||||
test.each(invalidCollections)(
|
||||
'should not inject metadata for invalid collection urls',
|
||||
async (url) => {
|
||||
const body = await fetch(new Request(url)).then((res) => res.text())
|
||||
expect(body).not.toContain('og:title')
|
||||
expect(body).not.toContain('og:image')
|
||||
expect(body).not.toContain('og:image:width')
|
||||
expect(body).not.toContain('og:image:height')
|
||||
expect(body).not.toContain('og:type')
|
||||
expect(body).not.toContain('og:url')
|
||||
expect(body).not.toContain('og:image:alt')
|
||||
expect(body).not.toContain('twitter:card')
|
||||
expect(body).not.toContain('twitter:title')
|
||||
expect(body).not.toContain('twitter:image')
|
||||
expect(body).not.toContain('twitter:image:alt')
|
||||
},
|
||||
50000
|
||||
)
|
||||
@@ -1,34 +0,0 @@
|
||||
/* eslint-disable import/no-unused-modules */
|
||||
import { Chain } from '../../src/graphql/data/__generated__/types-and-hooks'
|
||||
import getRequest from '../utils/getRequest'
|
||||
import getToken from '../utils/getToken'
|
||||
|
||||
const convertTokenAddress = (tokenAddress: string, networkName: string) => {
|
||||
if (tokenAddress === 'NATIVE') {
|
||||
switch (networkName) {
|
||||
case Chain.Celo:
|
||||
return '0x471EcE3750Da237f93B8E339c536989b8978a438'
|
||||
case Chain.Polygon:
|
||||
return '0x0000000000000000000000000000000000001010'
|
||||
default:
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
return tokenAddress
|
||||
}
|
||||
|
||||
export const onRequest: PagesFunction = async ({ params, request, next }) => {
|
||||
const res = next()
|
||||
try {
|
||||
const { index } = params
|
||||
const networkName = index[0]?.toString().toUpperCase()
|
||||
const tokenString = index[1]?.toString()
|
||||
if (!tokenString) {
|
||||
return res
|
||||
}
|
||||
const tokenAddress = convertTokenAddress(tokenString, networkName)
|
||||
return getRequest(res, request.url, () => getToken(networkName, tokenAddress, request.url))
|
||||
} catch (e) {
|
||||
return res
|
||||
}
|
||||
}
|
||||
@@ -1,529 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`should inject metadata for valid tokens 1`] = `
|
||||
"<!DOCTYPE html>
|
||||
<html translate="no">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
|
||||
<title>Uniswap Interface</title>
|
||||
<meta name="description" content="Swap or provide liquidity on the Uniswap Protocol" />
|
||||
|
||||
<!--
|
||||
. will be replaced with the URL of the \`public\` folder during build.
|
||||
Only files inside the \`public\` folder can be referenced from the HTML.
|
||||
-->
|
||||
<link rel="shortcut icon" type="image/png" href="./favicon.png" />
|
||||
<link rel="apple-touch-icon" sizes="192x192" href="./images/192x192_App_Icon.png" />
|
||||
<link rel="apple-touch-icon" sizes="512x512" href="./images/512x512_App_Icon.png" />
|
||||
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
|
||||
<meta name="theme-color" content="#FC72FF" />
|
||||
<meta
|
||||
http-equiv="Content-Security-Policy"
|
||||
|
||||
content="script-src 'self' 'unsafe-inline'"
|
||||
|
||||
/>
|
||||
|
||||
<!--
|
||||
Apple Smart App Banner for Safari on iOS
|
||||
https://developer.apple.com/documentation/webkit/promoting_apps_with_smart_app_banners
|
||||
-->
|
||||
<meta name="apple-itunes-app" content="app-id=6443944476">
|
||||
|
||||
<!--
|
||||
manifest.json provides metadata used when the app is installed as a PWA.
|
||||
See https://developers.google.com/web/fundamentals/web-app-manifest/
|
||||
-->
|
||||
<link rel="manifest" href="./manifest.json" />
|
||||
|
||||
<link rel="preload" href="./fonts/Inter-roman.var.woff2" as="font" type="font/woff2" crossorigin />
|
||||
|
||||
<style>
|
||||
* {
|
||||
font-family: 'Inter', sans-serif;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/**
|
||||
Explicitly load Inter var from public/ so it does not block LCP's critical path.
|
||||
*/
|
||||
@font-face {
|
||||
font-family: 'Inter custom';
|
||||
font-weight: 100 900;
|
||||
font-style: normal;
|
||||
font-display: block;
|
||||
font-named-instance: 'Regular';
|
||||
src: url(./fonts/Inter-roman.var.woff2) format('woff2 supports variations(gvar)'),
|
||||
url(./fonts/Inter-roman.var.woff2) format('woff2-variations'),
|
||||
url(./fonts/Inter-roman.var.woff2) format('woff2');
|
||||
}
|
||||
|
||||
@supports (font-variation-settings: normal) {
|
||||
* {
|
||||
font-family: 'Inter custom', sans-serif;
|
||||
}
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
button {
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
html {
|
||||
font-size: 16px;
|
||||
font-variant: none;
|
||||
font-smooth: always;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
|
||||
}
|
||||
|
||||
/* Use this to apply network-specific gradient backgrounds, in RadialGradientByChainUpdater.ts */
|
||||
#background-radial-gradient {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
pointer-events: none;
|
||||
width: 200vw;
|
||||
height: 200vh;
|
||||
transform: translate(-50vw, -100vh);
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
html,
|
||||
body,
|
||||
#root {
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
html {
|
||||
background: linear-gradient(180deg, #202738 0%, #070816 100%);
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
html {
|
||||
background: radial-gradient(100% 100% at 50% 0%, rgba(255, 184, 226, 0.51) 0%, rgba(255, 255, 255, 0) 100%), #FFFFFF
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<script defer src="./static/js/bundle.js"></script><meta property="og:title" content="Get USDC on Uniswap"/><meta property="og:image" content="https://raw.githubusercontent.com/Uniswap/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png"/><meta property="og:image:width" content="1200"/><meta property="og:image:height" content="630"/><meta property="og:image:alt" content="Get USDC on Uniswap"/><meta property="og:type" content="website"/><meta property="og:url" content="http://127.0.0.1:3000/tokens/ethereum/0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48"/><meta property="twitter:card" content="summary_large_image"/><meta property="twitter:title" content="Get USDC on Uniswap"/><meta property="twitter:image" content="https://raw.githubusercontent.com/Uniswap/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png"/><meta property="twitter:image:alt" content="Get USDC on Uniswap"/></head>
|
||||
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
|
||||
<div id="root">
|
||||
<!-- Triggers the font to load immediately and then is replaced by the app -->
|
||||
<div> </div>
|
||||
</div>
|
||||
|
||||
<div id="background-radial-gradient"></div>
|
||||
</body>
|
||||
</html>
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`should inject metadata for valid tokens 2`] = `
|
||||
"<!DOCTYPE html>
|
||||
<html translate="no">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
|
||||
<title>Uniswap Interface</title>
|
||||
<meta name="description" content="Swap or provide liquidity on the Uniswap Protocol" />
|
||||
|
||||
<!--
|
||||
. will be replaced with the URL of the \`public\` folder during build.
|
||||
Only files inside the \`public\` folder can be referenced from the HTML.
|
||||
-->
|
||||
<link rel="shortcut icon" type="image/png" href="./favicon.png" />
|
||||
<link rel="apple-touch-icon" sizes="192x192" href="./images/192x192_App_Icon.png" />
|
||||
<link rel="apple-touch-icon" sizes="512x512" href="./images/512x512_App_Icon.png" />
|
||||
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
|
||||
<meta name="theme-color" content="#FC72FF" />
|
||||
<meta
|
||||
http-equiv="Content-Security-Policy"
|
||||
|
||||
content="script-src 'self' 'unsafe-inline'"
|
||||
|
||||
/>
|
||||
|
||||
<!--
|
||||
Apple Smart App Banner for Safari on iOS
|
||||
https://developer.apple.com/documentation/webkit/promoting_apps_with_smart_app_banners
|
||||
-->
|
||||
<meta name="apple-itunes-app" content="app-id=6443944476">
|
||||
|
||||
<!--
|
||||
manifest.json provides metadata used when the app is installed as a PWA.
|
||||
See https://developers.google.com/web/fundamentals/web-app-manifest/
|
||||
-->
|
||||
<link rel="manifest" href="./manifest.json" />
|
||||
|
||||
<link rel="preload" href="./fonts/Inter-roman.var.woff2" as="font" type="font/woff2" crossorigin />
|
||||
|
||||
<style>
|
||||
* {
|
||||
font-family: 'Inter', sans-serif;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/**
|
||||
Explicitly load Inter var from public/ so it does not block LCP's critical path.
|
||||
*/
|
||||
@font-face {
|
||||
font-family: 'Inter custom';
|
||||
font-weight: 100 900;
|
||||
font-style: normal;
|
||||
font-display: block;
|
||||
font-named-instance: 'Regular';
|
||||
src: url(./fonts/Inter-roman.var.woff2) format('woff2 supports variations(gvar)'),
|
||||
url(./fonts/Inter-roman.var.woff2) format('woff2-variations'),
|
||||
url(./fonts/Inter-roman.var.woff2) format('woff2');
|
||||
}
|
||||
|
||||
@supports (font-variation-settings: normal) {
|
||||
* {
|
||||
font-family: 'Inter custom', sans-serif;
|
||||
}
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
button {
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
html {
|
||||
font-size: 16px;
|
||||
font-variant: none;
|
||||
font-smooth: always;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
|
||||
}
|
||||
|
||||
/* Use this to apply network-specific gradient backgrounds, in RadialGradientByChainUpdater.ts */
|
||||
#background-radial-gradient {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
pointer-events: none;
|
||||
width: 200vw;
|
||||
height: 200vh;
|
||||
transform: translate(-50vw, -100vh);
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
html,
|
||||
body,
|
||||
#root {
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
html {
|
||||
background: linear-gradient(180deg, #202738 0%, #070816 100%);
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
html {
|
||||
background: radial-gradient(100% 100% at 50% 0%, rgba(255, 184, 226, 0.51) 0%, rgba(255, 255, 255, 0) 100%), #FFFFFF
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<script defer src="./static/js/bundle.js"></script><meta property="og:title" content="Get ETH on Uniswap"/><meta property="og:image" content="https://token-icons.s3.amazonaws.com/eth.png"/><meta property="og:image:width" content="1200"/><meta property="og:image:height" content="630"/><meta property="og:image:alt" content="Get ETH on Uniswap"/><meta property="og:type" content="website"/><meta property="og:url" content="http://127.0.0.1:3000/tokens/ethereum/NATIVE"/><meta property="twitter:card" content="summary_large_image"/><meta property="twitter:title" content="Get ETH on Uniswap"/><meta property="twitter:image" content="https://token-icons.s3.amazonaws.com/eth.png"/><meta property="twitter:image:alt" content="Get ETH on Uniswap"/></head>
|
||||
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
|
||||
<div id="root">
|
||||
<!-- Triggers the font to load immediately and then is replaced by the app -->
|
||||
<div> </div>
|
||||
</div>
|
||||
|
||||
<div id="background-radial-gradient"></div>
|
||||
</body>
|
||||
</html>
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`should inject metadata for valid tokens 3`] = `
|
||||
"<!DOCTYPE html>
|
||||
<html translate="no">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
|
||||
<title>Uniswap Interface</title>
|
||||
<meta name="description" content="Swap or provide liquidity on the Uniswap Protocol" />
|
||||
|
||||
<!--
|
||||
. will be replaced with the URL of the \`public\` folder during build.
|
||||
Only files inside the \`public\` folder can be referenced from the HTML.
|
||||
-->
|
||||
<link rel="shortcut icon" type="image/png" href="./favicon.png" />
|
||||
<link rel="apple-touch-icon" sizes="192x192" href="./images/192x192_App_Icon.png" />
|
||||
<link rel="apple-touch-icon" sizes="512x512" href="./images/512x512_App_Icon.png" />
|
||||
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
|
||||
<meta name="theme-color" content="#FC72FF" />
|
||||
<meta
|
||||
http-equiv="Content-Security-Policy"
|
||||
|
||||
content="script-src 'self' 'unsafe-inline'"
|
||||
|
||||
/>
|
||||
|
||||
<!--
|
||||
Apple Smart App Banner for Safari on iOS
|
||||
https://developer.apple.com/documentation/webkit/promoting_apps_with_smart_app_banners
|
||||
-->
|
||||
<meta name="apple-itunes-app" content="app-id=6443944476">
|
||||
|
||||
<!--
|
||||
manifest.json provides metadata used when the app is installed as a PWA.
|
||||
See https://developers.google.com/web/fundamentals/web-app-manifest/
|
||||
-->
|
||||
<link rel="manifest" href="./manifest.json" />
|
||||
|
||||
<link rel="preload" href="./fonts/Inter-roman.var.woff2" as="font" type="font/woff2" crossorigin />
|
||||
|
||||
<style>
|
||||
* {
|
||||
font-family: 'Inter', sans-serif;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/**
|
||||
Explicitly load Inter var from public/ so it does not block LCP's critical path.
|
||||
*/
|
||||
@font-face {
|
||||
font-family: 'Inter custom';
|
||||
font-weight: 100 900;
|
||||
font-style: normal;
|
||||
font-display: block;
|
||||
font-named-instance: 'Regular';
|
||||
src: url(./fonts/Inter-roman.var.woff2) format('woff2 supports variations(gvar)'),
|
||||
url(./fonts/Inter-roman.var.woff2) format('woff2-variations'),
|
||||
url(./fonts/Inter-roman.var.woff2) format('woff2');
|
||||
}
|
||||
|
||||
@supports (font-variation-settings: normal) {
|
||||
* {
|
||||
font-family: 'Inter custom', sans-serif;
|
||||
}
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
button {
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
html {
|
||||
font-size: 16px;
|
||||
font-variant: none;
|
||||
font-smooth: always;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
|
||||
}
|
||||
|
||||
/* Use this to apply network-specific gradient backgrounds, in RadialGradientByChainUpdater.ts */
|
||||
#background-radial-gradient {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
pointer-events: none;
|
||||
width: 200vw;
|
||||
height: 200vh;
|
||||
transform: translate(-50vw, -100vh);
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
html,
|
||||
body,
|
||||
#root {
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
html {
|
||||
background: linear-gradient(180deg, #202738 0%, #070816 100%);
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
html {
|
||||
background: radial-gradient(100% 100% at 50% 0%, rgba(255, 184, 226, 0.51) 0%, rgba(255, 255, 255, 0) 100%), #FFFFFF
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<script defer src="./static/js/bundle.js"></script><meta property="og:title" content="Get MATIC on Uniswap"/><meta property="og:image" content="https://raw.githubusercontent.com/Uniswap/assets/master/blockchains/ethereum/assets/0x7D1AfA7B718fb893dB30A3aBc0Cfc608AaCfeBB0/logo.png"/><meta property="og:image:width" content="1200"/><meta property="og:image:height" content="630"/><meta property="og:image:alt" content="Get MATIC on Uniswap"/><meta property="og:type" content="website"/><meta property="og:url" content="http://127.0.0.1:3000/tokens/polygon/NATIVE"/><meta property="twitter:card" content="summary_large_image"/><meta property="twitter:title" content="Get MATIC on Uniswap"/><meta property="twitter:image" content="https://raw.githubusercontent.com/Uniswap/assets/master/blockchains/ethereum/assets/0x7D1AfA7B718fb893dB30A3aBc0Cfc608AaCfeBB0/logo.png"/><meta property="twitter:image:alt" content="Get MATIC on Uniswap"/></head>
|
||||
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
|
||||
<div id="root">
|
||||
<!-- Triggers the font to load immediately and then is replaced by the app -->
|
||||
<div> </div>
|
||||
</div>
|
||||
|
||||
<div id="background-radial-gradient"></div>
|
||||
</body>
|
||||
</html>
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`should inject metadata for valid tokens 4`] = `
|
||||
"<!DOCTYPE html>
|
||||
<html translate="no">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
|
||||
<title>Uniswap Interface</title>
|
||||
<meta name="description" content="Swap or provide liquidity on the Uniswap Protocol" />
|
||||
|
||||
<!--
|
||||
. will be replaced with the URL of the \`public\` folder during build.
|
||||
Only files inside the \`public\` folder can be referenced from the HTML.
|
||||
-->
|
||||
<link rel="shortcut icon" type="image/png" href="./favicon.png" />
|
||||
<link rel="apple-touch-icon" sizes="192x192" href="./images/192x192_App_Icon.png" />
|
||||
<link rel="apple-touch-icon" sizes="512x512" href="./images/512x512_App_Icon.png" />
|
||||
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
|
||||
<meta name="theme-color" content="#FC72FF" />
|
||||
<meta
|
||||
http-equiv="Content-Security-Policy"
|
||||
|
||||
content="script-src 'self' 'unsafe-inline'"
|
||||
|
||||
/>
|
||||
|
||||
<!--
|
||||
Apple Smart App Banner for Safari on iOS
|
||||
https://developer.apple.com/documentation/webkit/promoting_apps_with_smart_app_banners
|
||||
-->
|
||||
<meta name="apple-itunes-app" content="app-id=6443944476">
|
||||
|
||||
<!--
|
||||
manifest.json provides metadata used when the app is installed as a PWA.
|
||||
See https://developers.google.com/web/fundamentals/web-app-manifest/
|
||||
-->
|
||||
<link rel="manifest" href="./manifest.json" />
|
||||
|
||||
<link rel="preload" href="./fonts/Inter-roman.var.woff2" as="font" type="font/woff2" crossorigin />
|
||||
|
||||
<style>
|
||||
* {
|
||||
font-family: 'Inter', sans-serif;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/**
|
||||
Explicitly load Inter var from public/ so it does not block LCP's critical path.
|
||||
*/
|
||||
@font-face {
|
||||
font-family: 'Inter custom';
|
||||
font-weight: 100 900;
|
||||
font-style: normal;
|
||||
font-display: block;
|
||||
font-named-instance: 'Regular';
|
||||
src: url(./fonts/Inter-roman.var.woff2) format('woff2 supports variations(gvar)'),
|
||||
url(./fonts/Inter-roman.var.woff2) format('woff2-variations'),
|
||||
url(./fonts/Inter-roman.var.woff2) format('woff2');
|
||||
}
|
||||
|
||||
@supports (font-variation-settings: normal) {
|
||||
* {
|
||||
font-family: 'Inter custom', sans-serif;
|
||||
}
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
button {
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
html {
|
||||
font-size: 16px;
|
||||
font-variant: none;
|
||||
font-smooth: always;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
|
||||
}
|
||||
|
||||
/* Use this to apply network-specific gradient backgrounds, in RadialGradientByChainUpdater.ts */
|
||||
#background-radial-gradient {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
pointer-events: none;
|
||||
width: 200vw;
|
||||
height: 200vh;
|
||||
transform: translate(-50vw, -100vh);
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
html,
|
||||
body,
|
||||
#root {
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
html {
|
||||
background: linear-gradient(180deg, #202738 0%, #070816 100%);
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
html {
|
||||
background: radial-gradient(100% 100% at 50% 0%, rgba(255, 184, 226, 0.51) 0%, rgba(255, 255, 255, 0) 100%), #FFFFFF
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<script defer src="./static/js/bundle.js"></script><meta property="og:title" content="Get PEPE on Uniswap"/><meta property="og:image" content="https://raw.githubusercontent.com/Uniswap/assets/master/blockchains/ethereum/assets/0x6982508145454Ce325dDbE47a25d4ec3d2311933/logo.png"/><meta property="og:image:width" content="1200"/><meta property="og:image:height" content="630"/><meta property="og:image:alt" content="Get PEPE on Uniswap"/><meta property="og:type" content="website"/><meta property="og:url" content="http://127.0.0.1:3000/tokens/ethereum/0x6982508145454ce325ddbe47a25d4ec3d2311933"/><meta property="twitter:card" content="summary_large_image"/><meta property="twitter:title" content="Get PEPE on Uniswap"/><meta property="twitter:image" content="https://raw.githubusercontent.com/Uniswap/assets/master/blockchains/ethereum/assets/0x6982508145454Ce325dDbE47a25d4ec3d2311933/logo.png"/><meta property="twitter:image:alt" content="Get PEPE on Uniswap"/></head>
|
||||
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
|
||||
<div id="root">
|
||||
<!-- Triggers the font to load immediately and then is replaced by the app -->
|
||||
<div> </div>
|
||||
</div>
|
||||
|
||||
<div id="background-radial-gradient"></div>
|
||||
</body>
|
||||
</html>
|
||||
"
|
||||
`;
|
||||
@@ -1,70 +0,0 @@
|
||||
const tokens = [
|
||||
{
|
||||
address: '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48',
|
||||
network: 'ethereum',
|
||||
symbol: 'USDC',
|
||||
image:
|
||||
'https://raw.githubusercontent.com/Uniswap/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png',
|
||||
},
|
||||
{
|
||||
address: 'NATIVE',
|
||||
network: 'ethereum',
|
||||
symbol: 'ETH',
|
||||
image: 'https://token-icons.s3.amazonaws.com/eth.png',
|
||||
},
|
||||
{
|
||||
address: 'NATIVE',
|
||||
network: 'polygon',
|
||||
symbol: 'MATIC',
|
||||
image:
|
||||
'https://raw.githubusercontent.com/Uniswap/assets/master/blockchains/ethereum/assets/0x7D1AfA7B718fb893dB30A3aBc0Cfc608AaCfeBB0/logo.png',
|
||||
},
|
||||
{
|
||||
address: '0x6982508145454ce325ddbe47a25d4ec3d2311933',
|
||||
network: 'ethereum',
|
||||
symbol: 'PEPE',
|
||||
image:
|
||||
'https://raw.githubusercontent.com/Uniswap/assets/master/blockchains/ethereum/assets/0x6982508145454Ce325dDbE47a25d4ec3d2311933/logo.png',
|
||||
},
|
||||
]
|
||||
|
||||
test.each(tokens)('should inject metadata for valid tokens', async (token) => {
|
||||
const url = 'http://127.0.0.1:3000/tokens/' + token.network + '/' + token.address
|
||||
const body = await fetch(new Request(url)).then((res) => res.text())
|
||||
expect(body).toMatchSnapshot()
|
||||
expect(body).toContain(`<meta property="og:title" content="Get ${token.symbol} on Uniswap"/>`)
|
||||
expect(body).toContain(`<meta property="og:image" content="${token.image}"/>`)
|
||||
expect(body).toContain(`<meta property="og:image:width" content="1200"/>`)
|
||||
expect(body).toContain(`<meta property="og:image:height" content="630"/>`)
|
||||
expect(body).toContain(`<meta property="og:type" content="website"/>`)
|
||||
expect(body).toContain(`<meta property="og:url" content="${url}"/>`)
|
||||
expect(body).toContain(`<meta property="og:image:alt" content="Get ${token.symbol} on Uniswap"/>`)
|
||||
expect(body).toContain(`<meta property="twitter:card" content="summary_large_image"/>`)
|
||||
expect(body).toContain(`<meta property="twitter:title" content="Get ${token.symbol} on Uniswap"/>`)
|
||||
expect(body).toContain(`<meta property="twitter:image" content="${token.image}"/>`)
|
||||
expect(body).toContain(`<meta property="twitter:image:alt" content="Get ${token.symbol} on Uniswap"/>`)
|
||||
})
|
||||
|
||||
const invalidTokens = [
|
||||
'http://127.0.0.1:3000/tokens/ethereum/0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb49',
|
||||
'http://127.0.0.1:3000/tokens/ethereum',
|
||||
'http://127.0.0.1:3000/tokens/ethereun',
|
||||
'http://127.0.0.1:3000/tokens/ethereum/0x0',
|
||||
'http://127.0.0.1:3000/tokens/ethereum//',
|
||||
'http://127.0.0.1:3000/tokens/potato/?potato=1',
|
||||
]
|
||||
|
||||
test.each(invalidTokens)('should not inject metadata for invalid tokens', async (url) => {
|
||||
const body = await fetch(new Request(url)).then((res) => res.text())
|
||||
expect(body).not.toContain('og:title')
|
||||
expect(body).not.toContain('og:image')
|
||||
expect(body).not.toContain('og:image:width')
|
||||
expect(body).not.toContain('og:image:height')
|
||||
expect(body).not.toContain('og:type')
|
||||
expect(body).not.toContain('og:url')
|
||||
expect(body).not.toContain('og:image:alt')
|
||||
expect(body).not.toContain('twitter:card')
|
||||
expect(body).not.toContain('twitter:title')
|
||||
expect(body).not.toContain('twitter:image')
|
||||
expect(body).not.toContain('twitter:image:alt')
|
||||
})
|
||||
@@ -3,17 +3,17 @@
|
||||
"esModuleInterop": true,
|
||||
"incremental": true,
|
||||
"lib": ["DOM", "DOM.Iterable", "ESNext"],
|
||||
"moduleResolution": "node",
|
||||
"module": "esnext",
|
||||
"noEmit": true,
|
||||
"strict": true,
|
||||
"target": "ESNext",
|
||||
"target": "ES6",
|
||||
"tsBuildInfoFile": "../node_modules/.cache/tsbuildinfo/functions", // avoid clobbering the build tsbuildinfo
|
||||
"types": ["jest", "node", "@cloudflare/workers-types"],
|
||||
"types": ["jest", "node"],
|
||||
"jsx": "react",
|
||||
"skipLibCheck": true,
|
||||
"baseUrl": "functions"
|
||||
"moduleResolution": "NodeNext",
|
||||
},
|
||||
"exclude": ["../node_modules", "../src"],
|
||||
"exclude": ["node_modules"],
|
||||
"include": ["**/*.ts"],
|
||||
"watchOptions": {
|
||||
"excludeDirectories": ["node_modules"]
|
||||
}
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
import CacheMock from 'browser-cache-mock'
|
||||
|
||||
import { mocked } from '../../src/test-utils/mocked'
|
||||
import Cache from './cache'
|
||||
|
||||
const cacheMock = new CacheMock()
|
||||
|
||||
const data = {
|
||||
title: 'test',
|
||||
image: 'testImage',
|
||||
url: 'testUrl',
|
||||
}
|
||||
|
||||
beforeAll(() => {
|
||||
const globalAny: any = global
|
||||
globalAny.caches = {
|
||||
open: async () => cacheMock,
|
||||
...cacheMock,
|
||||
}
|
||||
})
|
||||
|
||||
test('Should put cache properly', async () => {
|
||||
jest.spyOn(cacheMock, 'put')
|
||||
await Cache.put(data, 'https://example.com')
|
||||
expect(cacheMock.put).toHaveBeenCalledWith('https://example.com', expect.anything())
|
||||
const call = mocked(cacheMock.put).mock.calls[0]
|
||||
const response = JSON.parse(await (call[1] as Response).clone().text())
|
||||
expect(response).toStrictEqual(data)
|
||||
|
||||
await expect(Cache.match('https://example.com')).resolves.toStrictEqual(data)
|
||||
})
|
||||
|
||||
test('Should match cache properly', async () => {
|
||||
jest.spyOn(cacheMock, 'match').mockResolvedValueOnce(new Response(JSON.stringify(data)))
|
||||
const response = await Cache.match('https://example.com')
|
||||
expect(response).toStrictEqual(data)
|
||||
})
|
||||
|
||||
test('Should return undefined if not all data is present', async () => {
|
||||
jest.spyOn(cacheMock, 'match').mockResolvedValueOnce(new Response(JSON.stringify({ ...data, title: undefined })))
|
||||
const response = await Cache.match('https://example.com')
|
||||
expect(response).toBeUndefined()
|
||||
})
|
||||
@@ -1,28 +0,0 @@
|
||||
interface Data {
|
||||
title: string
|
||||
image: string
|
||||
url: string
|
||||
}
|
||||
|
||||
const CACHE_NAME = 'functions-cache' as const
|
||||
|
||||
class Cache {
|
||||
async match(request: string): Promise<Data | undefined> {
|
||||
const cache = await caches.open(CACHE_NAME)
|
||||
const response = await cache.match(request)
|
||||
if (!response) return undefined
|
||||
const data: Data = JSON.parse(await response.text())
|
||||
if (!data.title || !data.image || !data.url) return undefined
|
||||
return data
|
||||
}
|
||||
|
||||
async put(data: Data, request: string) {
|
||||
// Set max age to 1 week
|
||||
const response = new Response(JSON.stringify(data))
|
||||
response.headers.set('Cache-Control', 'max-age=604800')
|
||||
const cache = await caches.open(CACHE_NAME)
|
||||
await cache.put(request, response)
|
||||
}
|
||||
}
|
||||
|
||||
export default new Cache()
|
||||
@@ -1,38 +0,0 @@
|
||||
import { AssetDocument } from '../../src/graphql/data/__generated__/types-and-hooks'
|
||||
import client from '../client'
|
||||
|
||||
function formatTitleName(name: string, collectionName: string, tokenId: string) {
|
||||
if (name) {
|
||||
return name
|
||||
}
|
||||
if (collectionName && tokenId) {
|
||||
return collectionName + ' #' + tokenId
|
||||
}
|
||||
if (tokenId) {
|
||||
return 'Asset #' + tokenId
|
||||
}
|
||||
return 'View NFT on Uniswap'
|
||||
}
|
||||
|
||||
export default async function getAsset(collectionAddress: string, tokenId: string, url: string) {
|
||||
const { data } = await client.query({
|
||||
query: AssetDocument,
|
||||
variables: {
|
||||
address: collectionAddress,
|
||||
filter: {
|
||||
tokenIds: [tokenId],
|
||||
},
|
||||
},
|
||||
})
|
||||
const asset = data?.nftAssets?.edges[0]?.node
|
||||
if (!asset) {
|
||||
return undefined
|
||||
}
|
||||
const title = formatTitleName(asset.name, asset.collection?.name, asset.tokenId)
|
||||
const formattedAsset = {
|
||||
title,
|
||||
image: asset.image?.url,
|
||||
url,
|
||||
}
|
||||
return formattedAsset
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
import { CollectionDocument } from '../../src/graphql/data/__generated__/types-and-hooks'
|
||||
import client from '../client'
|
||||
|
||||
export default async function getCollection(collectionAddress: string, url: string) {
|
||||
const { data } = await client.query({
|
||||
query: CollectionDocument,
|
||||
variables: {
|
||||
addresses: collectionAddress,
|
||||
},
|
||||
})
|
||||
const collection = data?.nftCollections?.edges[0]?.node
|
||||
if (!collection || !collection.name) {
|
||||
return undefined
|
||||
}
|
||||
const formattedAsset = {
|
||||
title: collection.name + ' on Uniswap',
|
||||
image: collection.image?.url,
|
||||
url,
|
||||
}
|
||||
return formattedAsset
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
import * as matchers from 'jest-extended'
|
||||
expect.extend(matchers)
|
||||
|
||||
import { mocked } from '../../src/test-utils/mocked'
|
||||
import Cache from './cache'
|
||||
import getRequest from './getRequest'
|
||||
|
||||
jest.mock('./cache', () => ({
|
||||
match: jest.fn(),
|
||||
put: jest.fn(),
|
||||
}))
|
||||
|
||||
test('should call Cache.match before calling getData when request is not cached', async () => {
|
||||
const url = 'https://example.com'
|
||||
const getData = jest.fn().mockResolvedValueOnce({
|
||||
title: 'test',
|
||||
image: 'testImage',
|
||||
url: 'testUrl',
|
||||
})
|
||||
await getRequest(Promise.resolve(new Response()), url, getData)
|
||||
expect(Cache.match).toHaveBeenCalledWith(url)
|
||||
expect(getData).toHaveBeenCalled()
|
||||
expect(Cache.match).toHaveBeenCalledBefore(getData)
|
||||
expect(Cache.put).toHaveBeenCalledAfter(getData)
|
||||
})
|
||||
|
||||
test('getData should not be called when request is cached', async () => {
|
||||
const url = 'https://example.com'
|
||||
mocked(Cache.match).mockResolvedValueOnce({
|
||||
title: 'test',
|
||||
image: 'testImage',
|
||||
url: 'testUrl',
|
||||
})
|
||||
const getData = jest.fn()
|
||||
await getRequest(Promise.resolve(new Response()), url, getData)
|
||||
expect(Cache.match).toHaveBeenCalledWith(url)
|
||||
expect(getData).not.toHaveBeenCalled()
|
||||
})
|
||||
@@ -1,31 +0,0 @@
|
||||
import { MetaTagInjector } from '../components/metaTagInjector'
|
||||
import Cache from './cache'
|
||||
|
||||
export default async function getRequest(
|
||||
res: Promise<Response>,
|
||||
url: string,
|
||||
getData: () => Promise<
|
||||
| {
|
||||
title: string
|
||||
image: string
|
||||
url: string
|
||||
}
|
||||
| undefined
|
||||
>
|
||||
) {
|
||||
try {
|
||||
const cachedData = await Cache.match(url)
|
||||
if (cachedData) {
|
||||
return new HTMLRewriter().on('head', new MetaTagInjector(cachedData)).transform(await res)
|
||||
} else {
|
||||
const data = await getData()
|
||||
if (!data) {
|
||||
return res
|
||||
}
|
||||
await Cache.put(data, url)
|
||||
return new HTMLRewriter().on('head', new MetaTagInjector(data)).transform(await res)
|
||||
}
|
||||
} catch (e) {
|
||||
return res
|
||||
}
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
import { TokenDocument } from '../../src/graphql/data/__generated__/types-and-hooks'
|
||||
import client from '../client'
|
||||
|
||||
function formatTitleName(symbol: string, name: string) {
|
||||
if (symbol) {
|
||||
return 'Get ' + symbol + ' on Uniswap'
|
||||
}
|
||||
if (name) {
|
||||
return 'Get ' + name + ' on Uniswap'
|
||||
}
|
||||
return 'View Token on Uniswap'
|
||||
}
|
||||
|
||||
export default async function getToken(networkName: string, tokenAddress: string | undefined, url: string) {
|
||||
const { data } = await client.query({
|
||||
query: TokenDocument,
|
||||
variables: {
|
||||
chain: networkName,
|
||||
address: tokenAddress,
|
||||
},
|
||||
})
|
||||
const asset = data?.token
|
||||
if (!asset) {
|
||||
return undefined
|
||||
}
|
||||
const title = formatTitleName(asset.symbol, asset.name)
|
||||
const formattedAsset = {
|
||||
title,
|
||||
image: asset.project?.logoUrl,
|
||||
url,
|
||||
}
|
||||
return formattedAsset
|
||||
}
|
||||
@@ -1,9 +1,14 @@
|
||||
import { ChainId } from '@uniswap/sdk-core'
|
||||
import { UNIVERSAL_ROUTER_CREATION_BLOCK } from '@uniswap/universal-router-sdk'
|
||||
|
||||
/* eslint-env node */
|
||||
require('dotenv').config()
|
||||
|
||||
// Block selection is arbitrary, as e2e tests will build up their own state.
|
||||
// The only requirement is that all infrastructure under test (eg Permit2 contracts) are already deployed.
|
||||
// TODO(WEB-2187): Make more dynamic to avoid manually updating
|
||||
const BLOCK_NUMBER = 17693163
|
||||
const POLYGON_BLOCK_NUMBER = 43600000
|
||||
|
||||
const forkingConfig = {
|
||||
httpHeaders: {
|
||||
Origin: 'localhost:3000', // infura allowlists requests by origin
|
||||
@@ -13,12 +18,12 @@ const forkingConfig = {
|
||||
const forks = {
|
||||
[ChainId.MAINNET]: {
|
||||
url: `https://mainnet.infura.io/v3/${process.env.REACT_APP_INFURA_KEY}`,
|
||||
blockNumber: UNIVERSAL_ROUTER_CREATION_BLOCK(ChainId.MAINNET),
|
||||
blockNumber: BLOCK_NUMBER,
|
||||
...forkingConfig,
|
||||
},
|
||||
[ChainId.POLYGON]: {
|
||||
url: `https://polygon-mainnet.infura.io/v3/${process.env.REACT_APP_INFURA_KEY}`,
|
||||
blockNumber: UNIVERSAL_ROUTER_CREATION_BLOCK(ChainId.POLYGON),
|
||||
blockNumber: POLYGON_BLOCK_NUMBER,
|
||||
...forkingConfig,
|
||||
},
|
||||
}
|
||||
|
||||
29
package.json
29
package.json
@@ -19,17 +19,15 @@
|
||||
"i18n": "yarn i18n:extract --clean && yarn i18n:compile",
|
||||
"prepare": "concurrently \"npm:ajv\" \"npm:contracts\" \"npm:graphql\" \"npm:i18n\"",
|
||||
"start": "craco start",
|
||||
"start:cloud": "NODE_OPTIONS=--dns-result-order=ipv4first PORT=3001 npx wrangler pages dev --node-compat --proxy=3001 --port=3000 -- yarn start",
|
||||
"start:cloud": "NODE_OPTIONS=--dns-result-order=ipv4first PORT=3001 npx wrangler pages dev --proxy=3001 --port=3000 -- yarn start",
|
||||
"build": "craco build",
|
||||
"build:e2e": "REACT_APP_CSP_ALLOW_UNSAFE_EVAL=true REACT_APP_ADD_COVERAGE_INSTRUMENTATION=true craco build",
|
||||
"analyze": "source-map-explorer 'build/static/js/*.js' --only-mapped",
|
||||
"serve": "serve build -l 3000",
|
||||
"lint": "yarn eslint --ignore-path .gitignore --cache --cache-location node_modules/.cache/eslint/ .",
|
||||
"typecheck": "tsc",
|
||||
"typecheck:cloud": "tsc -p functions/tsconfig.json",
|
||||
"typecheck:cypress": "tsc -p cypress/tsconfig.json",
|
||||
"test": "craco test",
|
||||
"test:cloud": "NODE_OPTIONS=--experimental-vm-modules yarn jest functions --config=functions/jest.config.json",
|
||||
"test:cloud": "NODE_OPTIONS=--experimental-vm-modules yarn jest functions --watch --config=functions/jest.config.json",
|
||||
"cypress:open": "cypress open --browser chrome --e2e",
|
||||
"cypress:run": "cypress run --browser chrome --e2e",
|
||||
"deduplicate": "yarn-deduplicate --strategy=highest"
|
||||
@@ -70,9 +68,8 @@
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
|
||||
"@babel/preset-env": "^7.22.7",
|
||||
"@cloudflare/workers-types": "^4.20230710.1",
|
||||
"@cloudflare/workers-types": "^4.20230518.0",
|
||||
"@craco/craco": "^7.1.0",
|
||||
"@ethersproject/experimental": "^5.4.0",
|
||||
"@lingui/cli": "^3.9.0",
|
||||
@@ -112,7 +109,6 @@
|
||||
"@walletconnect/types": "^2.8.6",
|
||||
"babel-jest": "^29.6.1",
|
||||
"babel-plugin-istanbul": "^6.1.1",
|
||||
"browser-cache-mock": "^0.1.7",
|
||||
"buffer": "^6.0.3",
|
||||
"concurrently": "^8.0.1",
|
||||
"cypress": "12.12.0",
|
||||
@@ -124,7 +120,6 @@
|
||||
"hardhat": "^2.14.0",
|
||||
"jest": "^29.6.1",
|
||||
"jest-dev-server": "^9.0.0",
|
||||
"jest-extended": "^4.0.1",
|
||||
"jest-fail-on-console": "^3.1.1",
|
||||
"jest-fetch-mock": "^3.0.3",
|
||||
"jest-styled-components": "^7.0.8",
|
||||
@@ -171,24 +166,24 @@
|
||||
"@sentry/tracing": "^7.45.0",
|
||||
"@sentry/types": "^7.45.0",
|
||||
"@types/react-window-infinite-loader": "^1.0.6",
|
||||
"@uniswap/analytics": "^1.4.0",
|
||||
"@uniswap/analytics-events": "^2.14.0",
|
||||
"@uniswap/analytics": "^1.3.1",
|
||||
"@uniswap/analytics-events": "^2.13.0",
|
||||
"@uniswap/conedison": "^1.8.0",
|
||||
"@uniswap/governance": "^1.0.2",
|
||||
"@uniswap/liquidity-staker": "^1.0.2",
|
||||
"@uniswap/merkle-distributor": "^1.0.1",
|
||||
"@uniswap/permit2-sdk": "^1.2.0",
|
||||
"@uniswap/merkle-distributor": "1.0.1",
|
||||
"@uniswap/permit2-sdk": "1.2.0",
|
||||
"@uniswap/redux-multicall": "^1.1.8",
|
||||
"@uniswap/router-sdk": "^1.6.0",
|
||||
"@uniswap/sdk-core": "^4.0.3",
|
||||
"@uniswap/smart-order-router": "^3.15.0",
|
||||
"@uniswap/smart-order-router": "3.13.7",
|
||||
"@uniswap/token-lists": "^1.0.0-beta.33",
|
||||
"@uniswap/uniswapx-sdk": "^1.1.0",
|
||||
"@uniswap/universal-router-sdk": "^1.5.6",
|
||||
"@uniswap/v2-core": "^1.0.1",
|
||||
"@uniswap/uniswapx-sdk": "^1.0.1",
|
||||
"@uniswap/universal-router-sdk": "^1.5.4",
|
||||
"@uniswap/v2-core": "1.0.0",
|
||||
"@uniswap/v2-periphery": "^1.1.0-beta.0",
|
||||
"@uniswap/v2-sdk": "^3.2.0",
|
||||
"@uniswap/v3-core": "^1.0.1",
|
||||
"@uniswap/v3-core": "1.0.0",
|
||||
"@uniswap/v3-periphery": "^1.1.1",
|
||||
"@uniswap/v3-sdk": "^3.10.0",
|
||||
"@vanilla-extract/css": "^1.7.2",
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
[
|
||||
{
|
||||
"relation": ["delegate_permission/common.handle_all_urls"],
|
||||
"target": {
|
||||
"namespace": "android_app",
|
||||
"package_name": "com.uniswap",
|
||||
"sha256_cert_fingerprints":
|
||||
["97:A5:81:51:DA:AF:8F:6E:65:3A:90:1E:82:12:6C:FB:61:2D:36:C7:CF:20:61:6B:A3:4C:52:CA:BC:58:43:8E"]
|
||||
}
|
||||
},
|
||||
[
|
||||
{
|
||||
"relation": ["delegate_permission/common.handle_all_urls"],
|
||||
"target": {
|
||||
"namespace": "android_app",
|
||||
"package_name": "com.uniswap.beta",
|
||||
"sha256_cert_fingerprints":
|
||||
["E5:39:87:DC:4D:FD:4C:1B:A6:74:36:7D:3A:3B:6B:ED:9E:B3:66:89:92:8A:1B:B8:FC:1B:22:56:56:B4:46:A3"]
|
||||
}
|
||||
}
|
||||
],
|
||||
[
|
||||
{
|
||||
"relation": ["delegate_permission/common.handle_all_urls"],
|
||||
"target": {
|
||||
"namespace": "android_app",
|
||||
"package_name": "com.uniswap.dev",
|
||||
"sha256_cert_fingerprints":
|
||||
["A8:A7:D4:DE:46:8E:BE:F6:DE:3B:62:2B:A7:26:60:F2:9A:4C:CD:AF:A6:96:C9:E5:7C:91:68:A1:29:2A:48:D3"]
|
||||
}
|
||||
}
|
||||
]
|
||||
]
|
||||
@@ -1,30 +0,0 @@
|
||||
{
|
||||
"applinks": {
|
||||
"details": [
|
||||
{
|
||||
"appIDs": [
|
||||
"JH3UHGZD75.com.uniswap.mobile",
|
||||
"JH3UHGZD75.com.uniswap.mobile.dev"
|
||||
],
|
||||
"components": [
|
||||
{
|
||||
"#": "/nfts/asset/*",
|
||||
"comment": "NFT Item"
|
||||
},
|
||||
{
|
||||
"#": "/nfts/collection/*",
|
||||
"comment": "NFT Collection"
|
||||
},
|
||||
{
|
||||
"#": "/tokens/*",
|
||||
"comment": "Token address"
|
||||
},
|
||||
{
|
||||
"#": "/address/*",
|
||||
"comment": "Wallet address"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 5.0 KiB |
@@ -1,3 +0,0 @@
|
||||
<svg width="55" height="55" viewBox="0 0 55 55" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M8.48223 46.7073C7.39664 45.6264 6.85526 43.9605 6.85814 41.7096V36.8327C6.8693 36.41 6.71466 35.9999 6.42734 35.69L2.981 32.2403C1.37846 30.6564 0.577148 29.0983 0.577148 27.5661C0.577148 26.0338 1.36838 24.4786 2.95082 22.9004L6.39716 19.4508C6.68527 19.1414 6.84004 18.7309 6.82795 18.3081L6.82795 13.4182C6.82795 11.1501 7.36932 9.47985 8.45205 8.40759C9.53477 7.33532 11.1876 6.79776 13.4105 6.79488L18.3128 6.79488C18.5176 6.80189 18.7217 6.7673 18.9128 6.69319C19.1038 6.61908 19.2778 6.50698 19.4243 6.36368L22.9138 2.91403C24.4991 1.34732 26.0527 0.559659 27.5749 0.551034C29.097 0.54241 30.6508 1.33007 32.2361 2.91403L35.6824 6.36368C35.834 6.50867 36.0132 6.6216 36.2093 6.69569C36.4055 6.76978 36.6145 6.80351 36.824 6.79488H41.6963C43.9421 6.79488 45.605 7.33677 46.6849 8.42054C47.7647 9.5043 48.3061 11.1702 48.309 13.4182V18.2951C48.2969 18.7179 48.4517 19.1285 48.7398 19.4378L52.1861 22.8875C53.7686 24.4686 54.5655 26.0238 54.577 27.5531C54.5885 29.0825 53.7915 30.6362 52.1861 32.2145L48.7398 35.6641C48.4525 35.974 48.2978 36.3842 48.309 36.8068V41.6837C48.309 43.9289 47.7633 45.5948 46.6719 46.6814C45.5806 47.768 43.922 48.3099 41.6963 48.3071H36.824C36.6144 48.2977 36.4052 48.3311 36.2089 48.4052C36.0127 48.4793 35.8336 48.5927 35.6824 48.7383L32.2361 52.1879C30.6508 53.7546 29.097 54.5423 27.5749 54.5509C26.0527 54.5595 24.4991 53.7719 22.9138 52.1879L19.4243 48.7383C19.2782 48.5943 19.1042 48.4818 18.9131 48.4077C18.7219 48.3335 18.5177 48.2993 18.3128 48.3071H13.4105C11.2077 48.3243 9.56496 47.791 8.48223 46.7073ZM25.225 37.9942C26.0971 37.9942 26.848 37.5581 27.3567 36.783L38.1848 19.8505C38.4997 19.3418 38.7904 18.7604 38.7904 18.2033C38.7904 17.0163 37.7245 16.2169 36.586 16.2169C35.8593 16.2169 35.2052 16.6045 34.6965 17.4281L25.1281 32.786L20.6225 27.045C20.0411 26.294 19.4597 26.0276 18.733 26.0276C17.5461 26.0276 16.6255 26.9723 16.6255 28.1835C16.6255 28.7649 16.8436 29.2978 17.2554 29.8065L22.9722 36.783C23.6262 37.6308 24.3287 37.9942 25.225 37.9942Z" fill="white"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 2.1 KiB |
@@ -1,47 +0,0 @@
|
||||
import {
|
||||
sendAnalyticsEvent as sendAnalyticsTraceEvent,
|
||||
Trace as AnalyticsTrace,
|
||||
TraceEvent as AnalyticsEvent,
|
||||
} from '@uniswap/analytics'
|
||||
import { atomWithStorage, useAtomValue } from 'jotai/utils'
|
||||
import { memo } from 'react'
|
||||
|
||||
export { getDeviceId, initializeAnalytics, OriginApplication, user, useTrace } from '@uniswap/analytics'
|
||||
|
||||
const allowAnalyticsAtomKey = 'allow_analytics'
|
||||
export const allowAnalyticsAtom = atomWithStorage<boolean>(allowAnalyticsAtomKey, true)
|
||||
|
||||
export const Trace = memo((props: React.ComponentProps<typeof AnalyticsTrace>) => {
|
||||
const allowAnalytics = useAtomValue(allowAnalyticsAtom)
|
||||
const shouldLogImpression = allowAnalytics ? props.shouldLogImpression : false
|
||||
|
||||
return <AnalyticsTrace {...props} shouldLogImpression={shouldLogImpression} />
|
||||
})
|
||||
|
||||
Trace.displayName = 'Trace'
|
||||
|
||||
export const TraceEvent = memo((props: React.ComponentProps<typeof AnalyticsEvent>) => {
|
||||
const allowAnalytics = useAtomValue(allowAnalyticsAtom)
|
||||
const shouldLogImpression = allowAnalytics ? props.shouldLogImpression : false
|
||||
|
||||
return <AnalyticsEvent {...props} shouldLogImpression={shouldLogImpression} />
|
||||
})
|
||||
|
||||
TraceEvent.displayName = 'TraceEvent'
|
||||
|
||||
export const sendAnalyticsEvent: typeof sendAnalyticsTraceEvent = (event, properties) => {
|
||||
let allowAnalytics = true
|
||||
|
||||
try {
|
||||
const value = localStorage.getItem(allowAnalyticsAtomKey)
|
||||
|
||||
if (typeof value === 'string') {
|
||||
allowAnalytics = JSON.parse(value)
|
||||
}
|
||||
// eslint-disable-next-line no-empty
|
||||
} catch {}
|
||||
|
||||
if (allowAnalytics) {
|
||||
sendAnalyticsTraceEvent(event, properties)
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
<svg width="28" height="28" viewBox="0 0 28 28" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M0 14C0 6.26801 6.26801 0 14 0V0C21.732 0 28 6.26801 28 14V14C28 21.732 21.732 28 14 28V28C6.26801 28 0 21.732 0 14V14Z" fill="#0052FF"/>
|
||||
<g clip-path="url(#clip0_13924_33076)">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M23.3332 14.0003C23.3332 19.155 19.1472 23.3337 13.9836 23.3337C9.08459 23.3337 5.06565 19.5724 4.6665 14.7849H17.0245V13.2158H4.6665C5.06565 8.42825 9.08459 4.66699 13.9836 4.66699C19.1472 4.66699 23.3332 8.84566 23.3332 14.0003Z" fill="white"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_13924_33076">
|
||||
<rect width="18.6667" height="18.6667" fill="white" transform="translate(4.66675 4.66699)"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 745 B |
@@ -1,11 +0,0 @@
|
||||
<svg width="28" height="28" viewBox="0 0 28 28" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="28" height="28" rx="8" fill="#0052FF"/>
|
||||
<g clip-path="url(#clip0_13921_13252)">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M23.3332 14.0003C23.3332 19.155 19.1472 23.3337 13.9836 23.3337C9.08459 23.3337 5.06565 19.5724 4.6665 14.7849H17.0245V13.2158H4.6665C5.06565 8.42825 9.08459 4.66699 13.9836 4.66699C19.1472 4.66699 23.3332 8.84566 23.3332 14.0003Z" fill="white"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_13921_13252">
|
||||
<rect width="18.6667" height="18.6667" fill="white" transform="translate(4.66675 4.66699)"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 651 B |
@@ -1,5 +0,0 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M20.83 14.6C19.9 14.06 19.33 13.07 19.33 12C19.33 10.93 19.9 9.93999 20.83 9.39999C20.99 9.29999 21.05 9.1 20.95 8.94L19.28 6.06C19.22 5.95 19.11 5.89001 19 5.89001C18.94 5.89001 18.88 5.91 18.83 5.94C18.37 6.2 17.85 6.34 17.33 6.34C16.8 6.34 16.28 6.19999 15.81 5.92999C14.88 5.38999 14.31 4.41 14.31 3.34C14.31 3.15 14.16 3 13.98 3H10.02C9.83999 3 9.69 3.15 9.69 3.34C9.69 4.41 9.12 5.38999 8.19 5.92999C7.72 6.19999 7.20001 6.34 6.67001 6.34C6.15001 6.34 5.63001 6.2 5.17001 5.94C5.01001 5.84 4.81 5.9 4.72 6.06L3.04001 8.94C3.01001 8.99 3 9.05001 3 9.10001C3 9.22001 3.06001 9.32999 3.17001 9.39999C4.10001 9.93999 4.67001 10.92 4.67001 11.99C4.67001 13.07 4.09999 14.06 3.17999 14.6H3.17001C3.01001 14.7 2.94999 14.9 3.04999 15.06L4.72 17.94C4.78 18.05 4.89 18.11 5 18.11C5.06 18.11 5.12001 18.09 5.17001 18.06C6.11001 17.53 7.26 17.53 8.19 18.07C9.11 18.61 9.67999 19.59 9.67999 20.66C9.67999 20.85 9.82999 21 10.02 21H13.98C14.16 21 14.31 20.85 14.31 20.66C14.31 19.59 14.88 18.61 15.81 18.07C16.28 17.8 16.8 17.66 17.33 17.66C17.85 17.66 18.37 17.8 18.83 18.06C18.99 18.16 19.19 18.1 19.28 17.94L20.96 15.06C20.99 15.01 21 14.95 21 14.9C21 14.78 20.94 14.67 20.83 14.6ZM12 15C10.34 15 9 13.66 9 12C9 10.34 10.34 9 12 9C13.66 9 15 10.34 15 12C15 13.66 13.66 15 12 15Z"
|
||||
fill="currentColor" />
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.4 KiB |
@@ -1,5 +1,5 @@
|
||||
import { TraceEvent } from '@uniswap/analytics'
|
||||
import { BrowserEvent, InterfaceElementName, SharedEventName } from '@uniswap/analytics-events'
|
||||
import { TraceEvent } from 'analytics'
|
||||
import { useDisableNFTRoutes } from 'hooks/useDisableNFTRoutes'
|
||||
import styled from 'styled-components/macro'
|
||||
import { BREAKPOINTS, ExternalLink, StyledRouterLink } from 'theme'
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { TraceEvent } from '@uniswap/analytics'
|
||||
import { BrowserEvent, SharedEventName } from '@uniswap/analytics-events'
|
||||
import { TraceEvent } from 'analytics'
|
||||
import { Link } from 'react-router-dom'
|
||||
import styled, { DefaultTheme } from 'styled-components/macro'
|
||||
import { BREAKPOINTS } from 'theme'
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
import { t } from '@lingui/macro'
|
||||
import { allowAnalyticsAtom } from 'analytics'
|
||||
import { useAtom } from 'jotai'
|
||||
|
||||
import { SettingsToggle } from './SettingsToggle'
|
||||
|
||||
export function AnalyticsToggle() {
|
||||
const [allowAnalytics, updateAllowAnalytics] = useAtom(allowAnalyticsAtom)
|
||||
|
||||
return (
|
||||
<SettingsToggle
|
||||
title={t`Allow analytics`}
|
||||
description={t`We use anonymized data to enhance your experience with Uniswap Labs products.`}
|
||||
isActive={allowAnalytics}
|
||||
toggle={() => void updateAllowAnalytics((value) => !value)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import { sendAnalyticsEvent, TraceEvent } from '@uniswap/analytics'
|
||||
import { BrowserEvent, InterfaceElementName, InterfaceEventName, SharedEventName } from '@uniswap/analytics-events'
|
||||
import { formatNumber, NumberType } from '@uniswap/conedison/format'
|
||||
import { CurrencyAmount, Token } from '@uniswap/sdk-core'
|
||||
import { useWeb3React } from '@web3-react/core'
|
||||
import { sendAnalyticsEvent, TraceEvent } from 'analytics'
|
||||
import { ButtonEmphasis, ButtonSize, LoadingButtonSpinner, ThemeButton } from 'components/Button'
|
||||
import Column from 'components/Column'
|
||||
import { AutoRow } from 'components/Row'
|
||||
@@ -11,8 +11,9 @@ import { LoadingBubble } from 'components/Tokens/loading'
|
||||
import { formatDelta } from 'components/Tokens/TokenDetails/PriceChart'
|
||||
import Tooltip from 'components/Tooltip'
|
||||
import { getConnection } from 'connection'
|
||||
import { usePortfolioBalancesQuery } from 'graphql/data/__generated__/types-and-hooks'
|
||||
import { GQL_MAINNET_CHAINS } from 'graphql/data/util'
|
||||
import { useDisableNFTRoutes } from 'hooks/useDisableNFTRoutes'
|
||||
import useENSName from 'hooks/useENSName'
|
||||
import { useProfilePageState, useSellAsset, useWalletCollections } from 'nft/hooks'
|
||||
import { useIsNftClaimAvailable } from 'nft/hooks/useIsNftClaimAvailable'
|
||||
import { ProfilePageStateType } from 'nft/types'
|
||||
@@ -33,13 +34,13 @@ import { useToggleAccountDrawer } from '.'
|
||||
import IconButton, { IconHoverText, IconWithConfirmTextButton } from './IconButton'
|
||||
import MiniPortfolio from './MiniPortfolio'
|
||||
import { portfolioFadeInAnimation } from './MiniPortfolio/PortfolioRow'
|
||||
import { useCachedPortfolioBalancesQuery } from './PrefetchBalancesWrapper'
|
||||
|
||||
const AuthenticatedHeaderWrapper = styled.div`
|
||||
padding: 20px 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
`
|
||||
|
||||
const HeaderButton = styled(ThemeButton)`
|
||||
@@ -104,7 +105,7 @@ const StatusWrapper = styled.div`
|
||||
display: inline-block;
|
||||
width: 70%;
|
||||
max-width: 70%;
|
||||
padding-right: 8px;
|
||||
padding-right: 14px;
|
||||
display: inline-flex;
|
||||
`
|
||||
|
||||
@@ -161,8 +162,7 @@ const LogOutCentered = styled(LogOut)`
|
||||
`
|
||||
|
||||
export default function AuthenticatedHeader({ account, openSettings }: { account: string; openSettings: () => void }) {
|
||||
const { connector } = useWeb3React()
|
||||
const { ENSName } = useENSName(account)
|
||||
const { connector, ENSName } = useWeb3React()
|
||||
const dispatch = useAppDispatch()
|
||||
const navigate = useNavigate()
|
||||
const closeModal = useCloseModal()
|
||||
@@ -226,7 +226,11 @@ export default function AuthenticatedHeader({ account, openSettings }: { account
|
||||
const openFiatOnrampUnavailableTooltip = useCallback(() => setShow(true), [setShow])
|
||||
const closeFiatOnrampUnavailableTooltip = useCallback(() => setShow(false), [setShow])
|
||||
|
||||
const { data: portfolioBalances } = useCachedPortfolioBalancesQuery({ account })
|
||||
const { data: portfolioBalances } = usePortfolioBalancesQuery({
|
||||
variables: { ownerAddress: account ?? '', chains: GQL_MAINNET_CHAINS },
|
||||
fetchPolicy: 'cache-only', // PrefetchBalancesWrapper handles balance fetching/staleness; this component only reads from cache
|
||||
})
|
||||
|
||||
const portfolio = portfolioBalances?.portfolios?.[0]
|
||||
const totalBalance = portfolio?.tokensTotalDenominatedValue?.value
|
||||
const absoluteChange = portfolio?.tokensTotalDenominatedValueChange?.absolute?.value
|
||||
@@ -253,12 +257,9 @@ export default function AuthenticatedHeader({ account, openSettings }: { account
|
||||
)}
|
||||
</StatusWrapper>
|
||||
<IconContainer>
|
||||
<IconButton
|
||||
hideHorizontal={showDisconnectConfirm}
|
||||
data-testid="wallet-settings"
|
||||
onClick={openSettings}
|
||||
Icon={Settings}
|
||||
/>
|
||||
{!showDisconnectConfirm && (
|
||||
<IconButton data-testid="wallet-settings" onClick={openSettings} Icon={Settings} />
|
||||
)}
|
||||
<TraceEvent
|
||||
events={[BrowserEvent.onClick]}
|
||||
name={SharedEventName.ELEMENT_CLICKED}
|
||||
@@ -270,7 +271,6 @@ export default function AuthenticatedHeader({ account, openSettings }: { account
|
||||
onShowConfirm={setShowDisconnectConfirm}
|
||||
Icon={LogOutCentered}
|
||||
text="Disconnect"
|
||||
dismissOnHoverOut
|
||||
/>
|
||||
</TraceEvent>
|
||||
</IconContainer>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useWeb3React } from '@web3-react/core'
|
||||
import Column from 'components/Column'
|
||||
import WalletModal from 'components/WalletModal'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { useCallback, useState } from 'react'
|
||||
import styled from 'styled-components/macro'
|
||||
|
||||
import AuthenticatedHeader from './AuthenticatedHeader'
|
||||
@@ -17,7 +17,7 @@ enum MenuState {
|
||||
SETTINGS,
|
||||
}
|
||||
|
||||
function DefaultMenu({ drawerOpen }: { drawerOpen: boolean }) {
|
||||
function DefaultMenu() {
|
||||
const { account } = useWeb3React()
|
||||
const isAuthenticated = !!account
|
||||
|
||||
@@ -25,17 +25,6 @@ function DefaultMenu({ drawerOpen }: { drawerOpen: boolean }) {
|
||||
const openSettings = useCallback(() => setMenu(MenuState.SETTINGS), [])
|
||||
const closeSettings = useCallback(() => setMenu(MenuState.DEFAULT), [])
|
||||
|
||||
useEffect(() => {
|
||||
if (!drawerOpen && menu === MenuState.SETTINGS) {
|
||||
// wait for the drawer to close before resetting the menu
|
||||
const timer = setTimeout(() => {
|
||||
closeSettings()
|
||||
}, 250)
|
||||
return () => clearTimeout(timer)
|
||||
}
|
||||
return
|
||||
}, [drawerOpen, menu, closeSettings])
|
||||
|
||||
return (
|
||||
<DefaultMenuWrap>
|
||||
{menu === MenuState.DEFAULT &&
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { InterfaceElementName } from '@uniswap/analytics-events'
|
||||
import { sendAnalyticsEvent } from '@uniswap/analytics'
|
||||
import { InterfaceElementName, InterfaceEventName, SharedEventName } from '@uniswap/analytics-events'
|
||||
import { PropsWithChildren, useCallback } from 'react'
|
||||
import styled from 'styled-components/macro'
|
||||
import { ClickableStyle } from 'theme'
|
||||
import { openDownloadApp } from 'utils/openDownloadApp'
|
||||
import { isIOS } from 'utils/userAgent'
|
||||
|
||||
const StyledButton = styled.button<{ padded?: boolean; branded?: boolean }>`
|
||||
${ClickableStyle}
|
||||
@@ -31,6 +32,23 @@ function BaseButton({ onClick, branded, children }: PropsWithChildren<{ onClick?
|
||||
)
|
||||
}
|
||||
|
||||
const APP_STORE_LINK = 'https://apps.apple.com/app/apple-store/id6443944476?pt=123625782&ct=In-App-Banners&mt=8'
|
||||
const MICROSITE_LINK = 'https://wallet.uniswap.org/'
|
||||
|
||||
const openAppStore = () => {
|
||||
window.open(APP_STORE_LINK, /* target = */ 'uniswap_wallet_appstore')
|
||||
}
|
||||
export const openWalletMicrosite = () => {
|
||||
sendAnalyticsEvent(InterfaceEventName.UNISWAP_WALLET_MICROSITE_OPENED)
|
||||
window.open(MICROSITE_LINK, /* target = */ 'uniswap_wallet_microsite')
|
||||
}
|
||||
|
||||
export function openDownloadApp(element: InterfaceElementName) {
|
||||
sendAnalyticsEvent(SharedEventName.ELEMENT_CLICKED, { element })
|
||||
if (isIOS) openAppStore()
|
||||
else openWalletMicrosite()
|
||||
}
|
||||
|
||||
// Launches App Store if on an iOS device, else navigates to Uniswap Wallet microsite
|
||||
export function DownloadButton({
|
||||
onClick,
|
||||
@@ -44,7 +62,7 @@ export function DownloadButton({
|
||||
const onButtonClick = useCallback(() => {
|
||||
// handles any actions required by the parent, i.e. cancelling wallet connection attempt or dismissing an ad
|
||||
onClick?.()
|
||||
openDownloadApp({ element })
|
||||
openDownloadApp(element)
|
||||
}, [element, onClick])
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import React, { forwardRef, useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { Icon } from 'react-feather'
|
||||
import styled, { css, DefaultTheme } from 'styled-components/macro'
|
||||
import styled, { css } from 'styled-components/macro'
|
||||
import useResizeObserver from 'use-resize-observer'
|
||||
|
||||
import { TRANSITION_DURATIONS } from '../../theme/styles'
|
||||
import Row from '../Row'
|
||||
|
||||
export const IconHoverText = styled.span`
|
||||
@@ -18,12 +17,11 @@ export const IconHoverText = styled.span`
|
||||
left: 10px;
|
||||
`
|
||||
|
||||
const getWidthTransition = ({ theme }: { theme: DefaultTheme }) =>
|
||||
`width ${theme.transition.timing.inOut} ${theme.transition.duration.fast}`
|
||||
const widthTransition = `width ease-in 80ms`
|
||||
|
||||
const IconStyles = css<{ hideHorizontal?: boolean }>`
|
||||
const IconStyles = css`
|
||||
background-color: ${({ theme }) => theme.backgroundInteractive};
|
||||
transition: ${getWidthTransition};
|
||||
transition: ${widthTransition};
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
padding: 0;
|
||||
@@ -31,7 +29,7 @@ const IconStyles = css<{ hideHorizontal?: boolean }>`
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
height: 32px;
|
||||
width: ${({ hideHorizontal }) => (hideHorizontal ? '0px' : '32px')};
|
||||
width: 32px;
|
||||
color: ${({ theme }) => theme.textPrimary};
|
||||
:hover {
|
||||
background-color: ${({ theme }) => theme.hoverState};
|
||||
@@ -39,7 +37,7 @@ const IconStyles = css<{ hideHorizontal?: boolean }>`
|
||||
theme: {
|
||||
transition: { duration, timing },
|
||||
},
|
||||
}) => `${duration.fast} background-color ${timing.in}, ${getWidthTransition}`};
|
||||
}) => `${duration.fast} background-color ${timing.in}, ${widthTransition}`};
|
||||
|
||||
${IconHoverText} {
|
||||
opacity: 1;
|
||||
@@ -47,7 +45,7 @@ const IconStyles = css<{ hideHorizontal?: boolean }>`
|
||||
}
|
||||
:active {
|
||||
background-color: ${({ theme }) => theme.backgroundSurface};
|
||||
transition: background-color ${({ theme }) => theme.transition.duration.fast} linear, ${getWidthTransition};
|
||||
transition: background-color 50ms linear, ${widthTransition};
|
||||
}
|
||||
`
|
||||
|
||||
@@ -69,7 +67,6 @@ const IconWrapper = styled.span`
|
||||
`
|
||||
interface BaseProps {
|
||||
Icon: Icon
|
||||
hideHorizontal?: boolean
|
||||
children?: React.ReactNode
|
||||
}
|
||||
|
||||
@@ -99,8 +96,6 @@ type IconWithTextProps = (IconButtonProps | IconLinkProps) & {
|
||||
text: string
|
||||
onConfirm?: () => void
|
||||
onShowConfirm?: (on: boolean) => void
|
||||
dismissOnHoverOut?: boolean
|
||||
dismissOnHoverDurationMs?: number
|
||||
}
|
||||
|
||||
const TextWrapper = styled.div`
|
||||
@@ -112,8 +107,6 @@ const TextWrapper = styled.div`
|
||||
|
||||
const TextHide = styled.div`
|
||||
overflow: hidden;
|
||||
transition: width ${({ theme }) => theme.transition.timing.inOut} ${({ theme }) => theme.transition.duration.fast},
|
||||
max-width ${({ theme }) => theme.transition.timing.inOut} ${({ theme }) => theme.transition.duration.fast};
|
||||
`
|
||||
|
||||
/**
|
||||
@@ -127,12 +120,9 @@ export const IconWithConfirmTextButton = ({
|
||||
onConfirm,
|
||||
onShowConfirm,
|
||||
onClick,
|
||||
dismissOnHoverOut,
|
||||
dismissOnHoverDurationMs = TRANSITION_DURATIONS.slow,
|
||||
...rest
|
||||
}: IconWithTextProps) => {
|
||||
const [showText, setShowTextWithoutCallback] = useState(false)
|
||||
const [frame, setFrame] = useState<HTMLElement | null>()
|
||||
const frameObserver = useResizeObserver<HTMLElement>()
|
||||
const hiddenObserver = useResizeObserver<HTMLElement>()
|
||||
|
||||
@@ -146,60 +136,41 @@ export const IconWithConfirmTextButton = ({
|
||||
|
||||
const dimensionsRef = useRef({
|
||||
frame: 0,
|
||||
innerText: 0,
|
||||
hidden: 0,
|
||||
})
|
||||
const dimensions = (() => {
|
||||
// once opened, we avoid updating it to prevent constant resize loop
|
||||
if (!showText) {
|
||||
dimensionsRef.current = { frame: frameObserver.width || 0, innerText: hiddenObserver.width || 0 }
|
||||
dimensionsRef.current = { frame: frameObserver.width || 0, hidden: hiddenObserver.width || 0 }
|
||||
}
|
||||
return dimensionsRef.current
|
||||
})()
|
||||
|
||||
// keyboard action to cancel
|
||||
useEffect(() => {
|
||||
if (typeof window === 'undefined') return
|
||||
if (!showText || !frame) return
|
||||
|
||||
const closeAndPrevent = (e: Event) => {
|
||||
setShowText(false)
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
}
|
||||
|
||||
const clickHandler = (e: MouseEvent) => {
|
||||
const { target } = e
|
||||
const shouldClose = !(target instanceof HTMLElement) || !frame.contains(target)
|
||||
if (shouldClose) {
|
||||
closeAndPrevent(e)
|
||||
}
|
||||
}
|
||||
|
||||
if (!showText) return
|
||||
const isClient = typeof window !== 'undefined'
|
||||
if (!isClient) return
|
||||
if (!showText) return
|
||||
const keyHandler = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Escape') {
|
||||
closeAndPrevent(e)
|
||||
setShowText(false)
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('click', clickHandler, { capture: true })
|
||||
window.addEventListener('keydown', keyHandler, { capture: true })
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('click', clickHandler, { capture: true })
|
||||
window.removeEventListener('keydown', keyHandler, { capture: true })
|
||||
}
|
||||
}, [frame, setShowText, showText])
|
||||
}, [setShowText, showText])
|
||||
|
||||
const xPad = showText ? 8 : 0
|
||||
const width = showText ? dimensions.frame + dimensions.innerText + xPad : 32
|
||||
const mouseLeaveTimeout = useRef<NodeJS.Timeout>()
|
||||
const xPad = showText ? 12 : 0
|
||||
const width = showText ? dimensions.frame + dimensions.hidden + xPad : 32
|
||||
|
||||
return (
|
||||
<IconBlock
|
||||
ref={(node) => {
|
||||
frameObserver.ref(node)
|
||||
setFrame(node)
|
||||
}}
|
||||
ref={frameObserver.ref}
|
||||
{...rest}
|
||||
style={{
|
||||
width,
|
||||
@@ -216,18 +187,6 @@ export const IconWithConfirmTextButton = ({
|
||||
setShowText(!showText)
|
||||
}
|
||||
}}
|
||||
{...(dismissOnHoverOut && {
|
||||
onMouseLeave() {
|
||||
mouseLeaveTimeout.current = setTimeout(() => {
|
||||
setShowText(false)
|
||||
}, dismissOnHoverDurationMs)
|
||||
},
|
||||
onMouseEnter() {
|
||||
if (mouseLeaveTimeout.current) {
|
||||
clearTimeout(mouseLeaveTimeout.current)
|
||||
}
|
||||
},
|
||||
})}
|
||||
>
|
||||
<Row height="100%" gap="xs">
|
||||
<IconWrapper>
|
||||
@@ -237,11 +196,8 @@ export const IconWithConfirmTextButton = ({
|
||||
{/* this outer div is so we can cut it off but keep the inner text width full-width so we can measure it */}
|
||||
<TextHide
|
||||
style={{
|
||||
maxWidth: showText ? dimensions.innerText : 0,
|
||||
width: showText ? dimensions.innerText : 0,
|
||||
// this negative transform offsets for the shift it does due to being 0 width
|
||||
transform: showText ? undefined : `translateX(-8px)`,
|
||||
minWidth: showText ? dimensions.innerText : 0,
|
||||
maxWidth: showText ? dimensions.hidden : 0,
|
||||
minWidth: showText ? dimensions.hidden : 0,
|
||||
}}
|
||||
>
|
||||
<TextWrapper ref={hiddenObserver.ref}>{text}</TextWrapper>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { TraceEvent } from '@uniswap/analytics'
|
||||
import { BrowserEvent, InterfaceElementName, SharedEventName } from '@uniswap/analytics-events'
|
||||
import { TraceEvent } from 'analytics'
|
||||
import Column from 'components/Column'
|
||||
import AlertTriangleFilled from 'components/Icons/AlertTriangleFilled'
|
||||
import { LoaderV2 } from 'components/Icons/LoadingSpinner'
|
||||
|
||||
@@ -77,7 +77,6 @@ function Loader() {
|
||||
}
|
||||
|
||||
const Success = styled(AnimatedEntranceConfirmationIcon)`
|
||||
position: relative;
|
||||
margin-bottom: 10px;
|
||||
`
|
||||
|
||||
|
||||
@@ -1,97 +0,0 @@
|
||||
import { TransactionStatus, useActivityQuery } from 'graphql/data/__generated__/types-and-hooks'
|
||||
import { useEffect, useMemo } from 'react'
|
||||
import { usePendingOrders } from 'state/signatures/hooks'
|
||||
import { usePendingTransactions, useTransactionCanceller } from 'state/transactions/hooks'
|
||||
|
||||
import { useLocalActivities } from './parseLocal'
|
||||
import { parseRemoteActivities } from './parseRemote'
|
||||
import { Activity, ActivityMap } from './types'
|
||||
|
||||
/** Detects transactions from same account with the same nonce and different hash */
|
||||
function findCancelTx(localActivity: Activity, remoteMap: ActivityMap, account: string): string | undefined {
|
||||
// handles locally cached tx's that were stored before we started tracking nonces
|
||||
if (!localActivity.nonce || localActivity.status !== TransactionStatus.Pending) return undefined
|
||||
|
||||
for (const remoteTx of Object.values(remoteMap)) {
|
||||
if (!remoteTx) continue
|
||||
|
||||
// A pending tx is 'cancelled' when another tx with the same account & nonce but different hash makes it on chain
|
||||
if (
|
||||
remoteTx.nonce === localActivity.nonce &&
|
||||
remoteTx.from.toLowerCase() === account.toLowerCase() &&
|
||||
remoteTx.hash.toLowerCase() !== localActivity.hash.toLowerCase() &&
|
||||
remoteTx.chainId === localActivity.chainId
|
||||
) {
|
||||
return remoteTx.hash
|
||||
}
|
||||
}
|
||||
|
||||
return undefined
|
||||
}
|
||||
|
||||
/** Deduplicates local and remote activities */
|
||||
function combineActivities(localMap: ActivityMap = {}, remoteMap: ActivityMap = {}): Array<Activity> {
|
||||
const txHashes = [...new Set([...Object.keys(localMap), ...Object.keys(remoteMap)])]
|
||||
|
||||
return txHashes.reduce((acc: Array<Activity>, hash) => {
|
||||
const localActivity = (localMap?.[hash] ?? {}) as Activity
|
||||
const remoteActivity = (remoteMap?.[hash] ?? {}) as Activity
|
||||
|
||||
if (localActivity.cancelled) {
|
||||
// Hides misleading activities caused by cross-chain nonce collisions previously being incorrectly labelled as cancelled txs in redux
|
||||
if (localActivity.chainId !== remoteActivity.chainId) {
|
||||
acc.push(remoteActivity)
|
||||
return acc
|
||||
}
|
||||
// Remote data only contains data of the cancel tx, rather than the original tx, so we prefer local data here
|
||||
acc.push(localActivity)
|
||||
} else {
|
||||
// Generally prefer remote values to local value because i.e. remote swap amounts are on-chain rather than client-estimated
|
||||
acc.push({ ...localActivity, ...remoteActivity } as Activity)
|
||||
}
|
||||
|
||||
return acc
|
||||
}, [])
|
||||
}
|
||||
|
||||
export function useAllActivities(account: string) {
|
||||
const { data, loading, refetch } = useActivityQuery({
|
||||
variables: { account },
|
||||
errorPolicy: 'all',
|
||||
fetchPolicy: 'cache-first',
|
||||
})
|
||||
|
||||
const localMap = useLocalActivities(account)
|
||||
const remoteMap = useMemo(() => parseRemoteActivities(data?.portfolios?.[0].assetActivities), [data?.portfolios])
|
||||
const updateCancelledTx = useTransactionCanceller()
|
||||
|
||||
/* Updates locally stored pendings tx's when remote data contains a conflicting cancellation tx */
|
||||
useEffect(() => {
|
||||
if (!remoteMap) return
|
||||
|
||||
Object.values(localMap).forEach((localActivity) => {
|
||||
if (!localActivity) return
|
||||
|
||||
const cancelHash = findCancelTx(localActivity, remoteMap, account)
|
||||
|
||||
if (cancelHash) updateCancelledTx(localActivity.hash, localActivity.chainId, cancelHash)
|
||||
})
|
||||
}, [account, localMap, remoteMap, updateCancelledTx])
|
||||
|
||||
const combinedActivities = useMemo(
|
||||
() => (remoteMap ? combineActivities(localMap, remoteMap) : undefined),
|
||||
[localMap, remoteMap]
|
||||
)
|
||||
|
||||
return { loading, activities: combinedActivities, refetch }
|
||||
}
|
||||
|
||||
export function useHasPendingActivity() {
|
||||
const pendingTransactions = usePendingTransactions()
|
||||
const pendingOrders = usePendingOrders()
|
||||
|
||||
const hasPendingActivity = pendingTransactions.length > 0 || pendingOrders.length > 0
|
||||
const pendingActivityCount = pendingTransactions.length + pendingOrders.length
|
||||
|
||||
return { hasPendingActivity, pendingActivityCount }
|
||||
}
|
||||
@@ -3,7 +3,7 @@ import { useAccountDrawer } from 'components/AccountDrawer'
|
||||
import Column from 'components/Column'
|
||||
import { LoadingBubble } from 'components/Tokens/loading'
|
||||
import { getYear, isSameDay, isSameMonth, isSameWeek, isSameYear } from 'date-fns'
|
||||
import { TransactionStatus } from 'graphql/data/__generated__/types-and-hooks'
|
||||
import { TransactionStatus, useActivityQuery } from 'graphql/data/__generated__/types-and-hooks'
|
||||
import { PollingInterval } from 'graphql/data/util'
|
||||
import { atom, useAtom } from 'jotai'
|
||||
import { EmptyWalletModule } from 'nft/components/profile/view/EmptyWalletContent'
|
||||
@@ -13,8 +13,9 @@ import { ThemedText } from 'theme'
|
||||
|
||||
import { PortfolioSkeleton, PortfolioTabWrapper } from '../PortfolioRow'
|
||||
import { ActivityRow } from './ActivityRow'
|
||||
import { useAllActivities } from './hooks'
|
||||
import { Activity } from './types'
|
||||
import { useLocalActivities } from './parseLocal'
|
||||
import { parseRemoteActivities } from './parseRemote'
|
||||
import { Activity, ActivityMap } from './types'
|
||||
|
||||
interface ActivityGroup {
|
||||
title: string
|
||||
@@ -24,7 +25,7 @@ interface ActivityGroup {
|
||||
const sortActivities = (a: Activity, b: Activity) => b.timestamp - a.timestamp
|
||||
|
||||
const createGroups = (activities?: Array<Activity>) => {
|
||||
if (!activities) return undefined
|
||||
if (!activities || !activities.length) return []
|
||||
const now = Date.now()
|
||||
|
||||
const pending: Array<Activity> = []
|
||||
@@ -81,13 +82,51 @@ const ActivityGroupWrapper = styled(Column)`
|
||||
gap: 8px;
|
||||
`
|
||||
|
||||
/* Detects transactions from same account with the same nonce and different hash */
|
||||
function wasTxCancelled(localActivity: Activity, remoteMap: ActivityMap, account: string): boolean {
|
||||
// handles locally cached tx's that were stored before we started tracking nonces
|
||||
if (!localActivity.nonce || localActivity.status !== TransactionStatus.Pending) return false
|
||||
|
||||
return Object.values(remoteMap).some((remoteTx) => {
|
||||
if (!remoteTx) return false
|
||||
|
||||
// Cancellations are only possible when both nonce and tx.from are the same
|
||||
if (remoteTx.nonce === localActivity.nonce && remoteTx.from.toLowerCase() === account.toLowerCase()) {
|
||||
// If the remote tx has a different hash than the local tx, the local tx was cancelled
|
||||
return remoteTx.hash.toLowerCase() !== localActivity.hash.toLowerCase()
|
||||
}
|
||||
return false
|
||||
})
|
||||
}
|
||||
|
||||
function combineActivities(localMap: ActivityMap = {}, remoteMap: ActivityMap = {}, account: string): Array<Activity> {
|
||||
const txHashes = [...new Set([...Object.keys(localMap), ...Object.keys(remoteMap)])]
|
||||
|
||||
// Merges local and remote activities w/ same hash, preferring remote data
|
||||
return txHashes.reduce((acc: Array<Activity>, hash) => {
|
||||
const localActivity = (localMap?.[hash] ?? {}) as Activity
|
||||
const remoteActivity = (remoteMap?.[hash] ?? {}) as Activity
|
||||
|
||||
// TODO(WEB-2064): Display cancelled status in UI rather than completely hiding cancelled TXs
|
||||
if (wasTxCancelled(localActivity, remoteMap, account)) return acc
|
||||
|
||||
// TODO(cartcrom): determine best logic for which fields to prefer from which sources
|
||||
// i.e.prefer remote exact swap output instead of local estimated output
|
||||
acc.push({ ...localActivity, ...remoteActivity } as Activity)
|
||||
|
||||
return acc
|
||||
}, [])
|
||||
}
|
||||
|
||||
const lastFetchedAtom = atom<number | undefined>(0)
|
||||
|
||||
export function ActivityTab({ account }: { account: string }) {
|
||||
const [drawerOpen, toggleWalletDrawer] = useAccountDrawer()
|
||||
const [lastFetched, setLastFetched] = useAtom(lastFetchedAtom)
|
||||
|
||||
const { activities, loading, refetch } = useAllActivities(account)
|
||||
const localMap = useLocalActivities(account)
|
||||
|
||||
const { data, loading, refetch } = useActivityQuery({ variables: { account } })
|
||||
|
||||
// We only refetch remote activity if the user renavigates to the activity tab by changing tabs or opening the drawer
|
||||
useEffect(() => {
|
||||
@@ -100,16 +139,20 @@ export function ActivityTab({ account }: { account: string }) {
|
||||
}
|
||||
}, [drawerOpen, lastFetched, refetch, setLastFetched])
|
||||
|
||||
const activityGroups = useMemo(() => createGroups(activities), [activities])
|
||||
const activityGroups = useMemo(() => {
|
||||
const remoteMap = parseRemoteActivities(data?.portfolios?.[0].assetActivities)
|
||||
const allActivities = combineActivities(localMap, remoteMap, account)
|
||||
return createGroups(allActivities)
|
||||
}, [data?.portfolios, localMap, account])
|
||||
|
||||
if (!activityGroups && loading) {
|
||||
if (!data && loading)
|
||||
return (
|
||||
<>
|
||||
<LoadingBubble height="16px" width="80px" margin="16px 16px 8px" />
|
||||
<PortfolioSkeleton shrinkRight />
|
||||
</>
|
||||
)
|
||||
} else if (!activityGroups || activityGroups?.length === 0) {
|
||||
else if (activityGroups.length === 0) {
|
||||
return <EmptyWalletModule type="activity" onNavigateClick={toggleWalletDrawer} />
|
||||
} else {
|
||||
return (
|
||||
|
||||
@@ -25,7 +25,7 @@ import {
|
||||
WrapTransactionInfo,
|
||||
} from 'state/transactions/types'
|
||||
|
||||
import { CancelledTransactionTitleTable, getActivityTitle, OrderTextTable } from '../constants'
|
||||
import { getActivityTitle, OrderTextTable } from '../constants'
|
||||
import { Activity, ActivityMap } from './types'
|
||||
|
||||
function getCurrency(currencyId: string, chainId: ChainId, tokens: ChainTokenMap): Currency | undefined {
|
||||
@@ -138,21 +138,17 @@ function parseMigrateCreateV3(
|
||||
return { descriptor, currencies: [baseCurrency, quoteCurrency] }
|
||||
}
|
||||
|
||||
export function getTransactionStatus(details: TransactionDetails): TransactionStatus {
|
||||
return !details.receipt
|
||||
? TransactionStatus.Pending
|
||||
: details.receipt.status === 1 || details.receipt?.status === undefined
|
||||
? TransactionStatus.Confirmed
|
||||
: TransactionStatus.Failed
|
||||
}
|
||||
|
||||
export function transactionToActivity(
|
||||
details: TransactionDetails,
|
||||
chainId: ChainId,
|
||||
tokens: ChainTokenMap
|
||||
): Activity | undefined {
|
||||
try {
|
||||
const status = getTransactionStatus(details)
|
||||
const status = !details.receipt
|
||||
? TransactionStatus.Pending
|
||||
: details.receipt.status === 1 || details.receipt?.status === undefined
|
||||
? TransactionStatus.Confirmed
|
||||
: TransactionStatus.Failed
|
||||
|
||||
const defaultFields = {
|
||||
hash: details.hash,
|
||||
@@ -162,7 +158,6 @@ export function transactionToActivity(
|
||||
timestamp: (details.confirmedTime ?? details.addedTime) / 1000,
|
||||
from: details.from,
|
||||
nonce: details.nonce,
|
||||
cancelled: details.cancelled,
|
||||
}
|
||||
|
||||
let additionalFields: Partial<Activity> = {}
|
||||
@@ -185,14 +180,7 @@ export function transactionToActivity(
|
||||
additionalFields = parseMigrateCreateV3(info, chainId, tokens)
|
||||
}
|
||||
|
||||
const activity = { ...defaultFields, ...additionalFields }
|
||||
|
||||
if (details.cancelled) {
|
||||
activity.title = CancelledTransactionTitleTable[details.info.type]
|
||||
activity.status = TransactionStatus.Confirmed
|
||||
}
|
||||
|
||||
return activity
|
||||
return { ...defaultFields, ...additionalFields }
|
||||
} catch (error) {
|
||||
console.debug(`Failed to parse transaction ${details.hash}`, error)
|
||||
return undefined
|
||||
|
||||
@@ -404,19 +404,15 @@ export function useTimeSince(timestamp: number) {
|
||||
const [timeSince, setTimeSince] = useState<string>(getTimeSince(timestamp))
|
||||
|
||||
useEffect(() => {
|
||||
const refreshTime = () =>
|
||||
setTimeout(() => {
|
||||
if (Math.floor(Date.now() - timestamp * 1000) / ms`61s` <= 1) {
|
||||
setTimeSince(getTimeSince(timestamp))
|
||||
timeout = refreshTime()
|
||||
}
|
||||
}, ms`1s`)
|
||||
|
||||
let timeout = refreshTime()
|
||||
|
||||
return () => {
|
||||
timeout && clearTimeout(timeout)
|
||||
const refreshTime = () => {
|
||||
if (Math.floor(Date.now() - timestamp * 1000) / ms`61s` <= 1) {
|
||||
setTimeSince(getTimeSince(timestamp))
|
||||
setTimeout(() => {
|
||||
refreshTime()
|
||||
}, ms`1s`)
|
||||
}
|
||||
}
|
||||
refreshTime()
|
||||
}, [timestamp])
|
||||
|
||||
return timeSince
|
||||
|
||||
@@ -18,7 +18,6 @@ export type Activity = {
|
||||
from: string
|
||||
nonce?: number | null
|
||||
prefixIconSrc?: string
|
||||
cancelled?: boolean
|
||||
}
|
||||
|
||||
export type ActivityMap = { [id: string]: Activity | undefined }
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { sendAnalyticsEvent, useTrace } from '@uniswap/analytics'
|
||||
import { InterfaceElementName, SharedEventName } from '@uniswap/analytics-events'
|
||||
import { sendAnalyticsEvent, useTrace } from 'analytics'
|
||||
import { useToggleAccountDrawer } from 'components/AccountDrawer'
|
||||
import Column from 'components/Column'
|
||||
import Row from 'components/Row'
|
||||
|
||||
@@ -11,7 +11,6 @@ import { useWeb3React } from '@web3-react/core'
|
||||
import { isSupportedChain } from 'constants/chains'
|
||||
import { RPC_PROVIDERS } from 'constants/providers'
|
||||
import { BaseContract } from 'ethers/lib/ethers'
|
||||
import { useBaseEnabledChains } from 'featureFlags/flags/baseEnabled'
|
||||
import { ContractInput, useUniswapPricesQuery } from 'graphql/data/__generated__/types-and-hooks'
|
||||
import { toContractInput } from 'graphql/data/util'
|
||||
import useStablecoinPrice from 'hooks/useStablecoinPrice'
|
||||
@@ -31,14 +30,13 @@ function useContractMultichain<T extends BaseContract>(
|
||||
chainIds?: ChainId[]
|
||||
): ContractMap<T> {
|
||||
const { chainId: walletChainId, provider: walletProvider } = useWeb3React()
|
||||
const baseEnabledChains = useBaseEnabledChains()
|
||||
|
||||
return useMemo(() => {
|
||||
const relevantChains =
|
||||
chainIds ??
|
||||
Object.keys(addressMap)
|
||||
.map((chainId) => parseInt(chainId))
|
||||
.filter((chainId) => isSupportedChain(chainId, baseEnabledChains))
|
||||
.filter(isSupportedChain)
|
||||
|
||||
return relevantChains.reduce((acc: ContractMap<T>, chainId) => {
|
||||
const provider =
|
||||
@@ -52,7 +50,7 @@ function useContractMultichain<T extends BaseContract>(
|
||||
}
|
||||
return acc
|
||||
}, {})
|
||||
}, [ABI, addressMap, baseEnabledChains, chainIds, walletChainId, walletProvider])
|
||||
}, [ABI, addressMap, chainIds, walletChainId, walletProvider])
|
||||
}
|
||||
|
||||
export function useV3ManagerContracts(chainIds: ChainId[]): ContractMap<NonfungiblePositionManager> {
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { t } from '@lingui/macro'
|
||||
import { TraceEvent } from '@uniswap/analytics'
|
||||
import { BrowserEvent, InterfaceElementName, SharedEventName } from '@uniswap/analytics-events'
|
||||
import { formatNumber, NumberType } from '@uniswap/conedison/format'
|
||||
import { Position } from '@uniswap/v3-sdk'
|
||||
import { useWeb3React } from '@web3-react/core'
|
||||
import { TraceEvent } from 'analytics'
|
||||
import { useToggleAccountDrawer } from 'components/AccountDrawer'
|
||||
import Row from 'components/Row'
|
||||
import { MouseoverTooltip } from 'components/Tooltip'
|
||||
|
||||
@@ -47,7 +47,6 @@ const DEFAULT_CHAINS = [
|
||||
ChainId.CELO,
|
||||
ChainId.BNB,
|
||||
ChainId.AVALANCHE,
|
||||
ChainId.BASE,
|
||||
]
|
||||
|
||||
type UseMultiChainPositionsData = { positions?: PositionInfo[]; loading: boolean }
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
import { ChainId, Currency } from '@uniswap/sdk-core'
|
||||
import blankTokenUrl from 'assets/svg/blank_token.svg'
|
||||
import { ReactComponent as UnknownStatus } from 'assets/svg/contract-interaction.svg'
|
||||
import { MissingImageLogo } from 'components/Logo/AssetLogo'
|
||||
import CurrencyLogo from 'components/Logo/CurrencyLogo'
|
||||
import { LogoImage, MissingImageLogo } from 'components/Logo/AssetLogo'
|
||||
import { Unicon } from 'components/Unicon'
|
||||
import { getChainInfo } from 'constants/chainInfo'
|
||||
import { useBaseEnabledChains } from 'featureFlags/flags/baseEnabled'
|
||||
import useTokenLogoSource from 'hooks/useAssetLogoSource'
|
||||
import useENSAvatar from 'hooks/useENSAvatar'
|
||||
import React from 'react'
|
||||
@@ -22,16 +20,16 @@ const DoubleLogoContainer = styled.div`
|
||||
position: relative;
|
||||
top: 0;
|
||||
left: 0;
|
||||
img:nth-child(n) {
|
||||
${LogoImage}:nth-child(n) {
|
||||
width: 19px;
|
||||
height: 40px;
|
||||
object-fit: cover;
|
||||
}
|
||||
img:nth-child(1) {
|
||||
${LogoImage}:nth-child(1) {
|
||||
border-radius: 20px 0 0 20px;
|
||||
object-position: 0 0;
|
||||
}
|
||||
img:nth-child(2) {
|
||||
${LogoImage}:nth-child(2) {
|
||||
border-radius: 0 20px 20px 0;
|
||||
object-position: 100% 0;
|
||||
}
|
||||
@@ -68,12 +66,6 @@ const SquareChainLogo = styled.img`
|
||||
width: 100%;
|
||||
`
|
||||
|
||||
const CircleLogoImage = styled.img<{ size: string }>`
|
||||
width: ${({ size }) => size};
|
||||
height: ${({ size }) => size};
|
||||
border-radius: 50%;
|
||||
`
|
||||
|
||||
const L2LogoContainer = styled.div<{ $backgroundColor?: string }>`
|
||||
background-color: ${({ $backgroundColor }) => $backgroundColor};
|
||||
border-radius: 2px;
|
||||
@@ -99,10 +91,7 @@ export function PortfolioLogo({
|
||||
size = '40px',
|
||||
style,
|
||||
}: MultiLogoProps) {
|
||||
const baseEnabledChains = useBaseEnabledChains()
|
||||
const chainInfo = getChainInfo(chainId, baseEnabledChains)
|
||||
const squareLogoUrl = chainInfo?.squareLogoUrl
|
||||
const logoUrl = chainInfo?.logoUrl
|
||||
const { squareLogoUrl, logoUrl } = getChainInfo(chainId)
|
||||
const chainLogo = squareLogoUrl ?? logoUrl
|
||||
const { avatar, loading } = useENSAvatar(accountAddress, false)
|
||||
const theme = useTheme()
|
||||
@@ -120,16 +109,18 @@ export function PortfolioLogo({
|
||||
<Unicon size={40} address={accountAddress} />
|
||||
)
|
||||
} else if (currencies && currencies.length) {
|
||||
const logo1 = <CircleLogoImage size={size} src={src ?? blankTokenUrl} onError={nextSrc} />
|
||||
const logo2 = <CircleLogoImage size={size} src={src2 ?? blankTokenUrl} onError={nextSrc2} />
|
||||
const logo1 = <LogoImage size={size} src={src ?? blankTokenUrl} onError={nextSrc} />
|
||||
|
||||
const logo2 = <LogoImage size={size} src={src2 ?? blankTokenUrl} onError={nextSrc2} />
|
||||
|
||||
component =
|
||||
currencies.length > 1 ? (
|
||||
<DoubleLogoContainer style={style}>
|
||||
{logo1}
|
||||
{logo2}
|
||||
</DoubleLogoContainer>
|
||||
) : currencies.length === 1 ? (
|
||||
<CurrencyLogo currency={currencies[0]} size={size} />
|
||||
) : src ? (
|
||||
logo1
|
||||
) : (
|
||||
<MissingImageLogo size={size}>
|
||||
{currencies[0]?.symbol?.toUpperCase().replace('$', '').replace(/\s+/g, '').slice(0, 3)}
|
||||
@@ -139,11 +130,11 @@ export function PortfolioLogo({
|
||||
component =
|
||||
images.length > 1 ? (
|
||||
<DoubleLogoContainer style={style}>
|
||||
<CircleLogoImage size={size} src={images[0]} />
|
||||
<CircleLogoImage size={size} src={images[images.length - 1]} />
|
||||
<LogoImage size={size} src={images[0]} />
|
||||
<LogoImage size={size} src={images[images.length - 1]} />
|
||||
</DoubleLogoContainer>
|
||||
) : (
|
||||
<CircleLogoImage size={size} src={images[0]} />
|
||||
<LogoImage size={size} src={images[0]} />
|
||||
)
|
||||
} else {
|
||||
return <UnknownContract width={size} height={size} />
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
import { TraceEvent } from '@uniswap/analytics'
|
||||
import { BrowserEvent, InterfaceElementName, SharedEventName } from '@uniswap/analytics-events'
|
||||
import { formatNumber, NumberType } from '@uniswap/conedison/format'
|
||||
import { TraceEvent } from 'analytics'
|
||||
import { useCachedPortfolioBalancesQuery } from 'components/AccountDrawer/PrefetchBalancesWrapper'
|
||||
import Row from 'components/Row'
|
||||
import { formatDelta } from 'components/Tokens/TokenDetails/PriceChart'
|
||||
import { PortfolioBalancesQuery } from 'graphql/data/__generated__/types-and-hooks'
|
||||
import { getTokenDetailsURL, gqlToCurrency, logSentryErrorForUnsupportedChain } from 'graphql/data/util'
|
||||
import { PortfolioBalancesQuery, usePortfolioBalancesQuery } from 'graphql/data/__generated__/types-and-hooks'
|
||||
import {
|
||||
getTokenDetailsURL,
|
||||
GQL_MAINNET_CHAINS,
|
||||
gqlToCurrency,
|
||||
logSentryErrorForUnsupportedChain,
|
||||
} from 'graphql/data/util'
|
||||
import { useAtomValue } from 'jotai/utils'
|
||||
import { EmptyWalletModule } from 'nft/components/profile/view/EmptyWalletContent'
|
||||
import { useCallback, useMemo, useState } from 'react'
|
||||
@@ -31,7 +35,11 @@ export default function Tokens({ account }: { account: string }) {
|
||||
const hideSmallBalances = useAtomValue(hideSmallBalancesAtom)
|
||||
const [showHiddenTokens, setShowHiddenTokens] = useState(false)
|
||||
|
||||
const { data } = useCachedPortfolioBalancesQuery({ account })
|
||||
const { data } = usePortfolioBalancesQuery({
|
||||
variables: { ownerAddress: account, chains: GQL_MAINNET_CHAINS },
|
||||
fetchPolicy: 'cache-only', // PrefetchBalancesWrapper handles balance fetching/staleness; this component only reads from cache
|
||||
errorPolicy: 'all',
|
||||
})
|
||||
|
||||
const visibleTokens = useMemo(() => {
|
||||
return !hideSmallBalances
|
||||
@@ -82,9 +90,6 @@ export default function Tokens({ account }: { account: string }) {
|
||||
const TokenBalanceText = styled(ThemedText.BodySecondary)`
|
||||
${EllipsisStyle}
|
||||
`
|
||||
const TokenNameText = styled(ThemedText.SubHeader)`
|
||||
${EllipsisStyle}
|
||||
`
|
||||
|
||||
type TokenBalance = NonNullable<
|
||||
NonNullable<NonNullable<PortfolioBalancesQuery['portfolios']>[number]>['tokenBalances']
|
||||
@@ -119,7 +124,7 @@ function TokenRow({ token, quantity, denominatedValue, tokenProjectMarket }: Tok
|
||||
>
|
||||
<PortfolioRow
|
||||
left={<PortfolioLogo chainId={currency.chainId} currencies={[currency]} size="40px" />}
|
||||
title={<TokenNameText>{token?.name}</TokenNameText>}
|
||||
title={<ThemedText.SubHeader>{token?.name}</ThemedText.SubHeader>}
|
||||
descriptor={
|
||||
<TokenBalanceText>
|
||||
{formatNumber(quantity, NumberType.TokenNonTx)} {token?.symbol}
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`PortfolioLogo renders with L2 icon 1`] = `
|
||||
.c3 {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.c1 {
|
||||
display: -webkit-box;
|
||||
display: -webkit-flex;
|
||||
@@ -15,18 +21,18 @@ exports[`PortfolioLogo renders with L2 icon 1`] = `
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.c1 img:nth-child(n) {
|
||||
.c1 .c2:nth-child(n) {
|
||||
width: 19px;
|
||||
height: 40px;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.c1 img:nth-child(1) {
|
||||
.c1 .c2:nth-child(1) {
|
||||
border-radius: 20px 0 0 20px;
|
||||
object-position: 0 0;
|
||||
}
|
||||
|
||||
.c1 img:nth-child(2) {
|
||||
.c1 .c2:nth-child(2) {
|
||||
border-radius: 0 20px 20px 0;
|
||||
object-position: 100% 0;
|
||||
}
|
||||
@@ -37,18 +43,12 @@ exports[`PortfolioLogo renders with L2 icon 1`] = `
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.c4 {
|
||||
.c5 {
|
||||
height: 14px;
|
||||
width: 14px;
|
||||
}
|
||||
|
||||
.c2 {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.c3 {
|
||||
.c4 {
|
||||
background-color: #0D111C;
|
||||
border-radius: 2px;
|
||||
height: 16px;
|
||||
@@ -79,20 +79,20 @@ exports[`PortfolioLogo renders with L2 icon 1`] = `
|
||||
class="c1"
|
||||
>
|
||||
<img
|
||||
class="c2"
|
||||
class="c2 c3"
|
||||
src="https://raw.githubusercontent.com/Uniswap/assets/master/blockchains/arbitrum/assets/0xDA10009cBd5D07dd0CeCc66161FC93D7c9000da1/logo.png"
|
||||
/>
|
||||
<img
|
||||
class="c2"
|
||||
src="https://raw.githubusercontent.com/Uniswap/assets/master/blockchains/arbitrum/assets/0xFF970A61A04b1cA14834A43f5dE4533eBDDB5CC8/logo.png"
|
||||
class="c2 c3"
|
||||
src="blank_token.svg"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="c3"
|
||||
class="c4"
|
||||
>
|
||||
<img
|
||||
alt="chainLogo"
|
||||
class="c4"
|
||||
class="c5"
|
||||
src="arbitrum_logo.svg"
|
||||
/>
|
||||
</div>
|
||||
@@ -101,6 +101,12 @@ exports[`PortfolioLogo renders with L2 icon 1`] = `
|
||||
`;
|
||||
|
||||
exports[`PortfolioLogo renders without L2 icon 1`] = `
|
||||
.c3 {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.c1 {
|
||||
display: -webkit-box;
|
||||
display: -webkit-flex;
|
||||
@@ -115,18 +121,18 @@ exports[`PortfolioLogo renders without L2 icon 1`] = `
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.c1 img:nth-child(n) {
|
||||
.c1 .c2:nth-child(n) {
|
||||
width: 19px;
|
||||
height: 40px;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.c1 img:nth-child(1) {
|
||||
.c1 .c2:nth-child(1) {
|
||||
border-radius: 20px 0 0 20px;
|
||||
object-position: 0 0;
|
||||
}
|
||||
|
||||
.c1 img:nth-child(2) {
|
||||
.c1 .c2:nth-child(2) {
|
||||
border-radius: 0 20px 20px 0;
|
||||
object-position: 100% 0;
|
||||
}
|
||||
@@ -137,12 +143,6 @@ exports[`PortfolioLogo renders without L2 icon 1`] = `
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.c2 {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
<div>
|
||||
<div
|
||||
class="c0"
|
||||
@@ -151,11 +151,11 @@ exports[`PortfolioLogo renders without L2 icon 1`] = `
|
||||
class="c1"
|
||||
>
|
||||
<img
|
||||
class="c2"
|
||||
class="c2 c3"
|
||||
src="https://raw.githubusercontent.com/Uniswap/assets/master/blockchains/ethereum/assets/0x6B175474E89094C44Da98b954EedeAC495271d0F/logo.png"
|
||||
/>
|
||||
<img
|
||||
class="c2"
|
||||
class="c2 c3"
|
||||
src="https://raw.githubusercontent.com/Uniswap/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -139,35 +139,6 @@ const TransactionTitleTable: { [key in TransactionType]: { [state in Transaction
|
||||
},
|
||||
}
|
||||
|
||||
export const CancelledTransactionTitleTable: { [key in TransactionType]: string } = {
|
||||
[TransactionType.SWAP]: t`Swap cancelled`,
|
||||
[TransactionType.WRAP]: t`Wrap cancelled`,
|
||||
[TransactionType.ADD_LIQUIDITY_V3_POOL]: t`Add liquidity cancelled`,
|
||||
[TransactionType.REMOVE_LIQUIDITY_V3]: t`Remove liquidity cancelled`,
|
||||
[TransactionType.CREATE_V3_POOL]: t`Create pool cancelled`,
|
||||
[TransactionType.COLLECT_FEES]: t`Collect fees cancelled`,
|
||||
[TransactionType.APPROVAL]: t`Approval cancelled`,
|
||||
[TransactionType.CLAIM]: t`Claim cancelled`,
|
||||
[TransactionType.BUY]: t`Buy cancelled`,
|
||||
[TransactionType.SEND]: t`Send cancelled`,
|
||||
[TransactionType.RECEIVE]: t`Receive cancelled`,
|
||||
[TransactionType.MINT]: t`Mint cancelled`,
|
||||
[TransactionType.BURN]: t`Burn cancelled`,
|
||||
[TransactionType.VOTE]: t`Vote cancelled`,
|
||||
[TransactionType.QUEUE]: t`Queue cancelled`,
|
||||
[TransactionType.EXECUTE]: t`Execute cancelled`,
|
||||
[TransactionType.BORROW]: t`Borrow cancelled`,
|
||||
[TransactionType.REPAY]: t`Repay cancelled`,
|
||||
[TransactionType.DEPLOY]: t`Deploy cancelled`,
|
||||
[TransactionType.CANCEL]: t`Cancellation cancelled`,
|
||||
[TransactionType.DELEGATE]: t`Delegate cancelled`,
|
||||
[TransactionType.DEPOSIT_LIQUIDITY_STAKING]: t`Deposit cancelled`,
|
||||
[TransactionType.WITHDRAW_LIQUIDITY_STAKING]: t`Withdrawal cancelled`,
|
||||
[TransactionType.ADD_LIQUIDITY_V2_POOL]: t`Add V2 liquidity cancelled`,
|
||||
[TransactionType.MIGRATE_LIQUIDITY_V3]: t`Migrate liquidity cancelled`,
|
||||
[TransactionType.SUBMIT_PROPOSAL]: t`Submit proposal cancelled`,
|
||||
}
|
||||
|
||||
const AlternateTransactionTitleTable: { [key in TransactionType]?: { [state in TransactionStatus]: string } } = {
|
||||
[TransactionType.WRAP]: {
|
||||
[TransactionStatus.Pending]: t`Unwrapping`,
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import { Trace, TraceEvent } from '@uniswap/analytics'
|
||||
import { BrowserEvent, InterfaceElementName, InterfaceSectionName, SharedEventName } from '@uniswap/analytics-events'
|
||||
import { Trace, TraceEvent } from 'analytics'
|
||||
import Column from 'components/Column'
|
||||
import { LoaderV2 } from 'components/Icons/LoadingSpinner'
|
||||
import { AutoRow } from 'components/Row'
|
||||
import { useDisableNFTRoutes } from 'hooks/useDisableNFTRoutes'
|
||||
import { useIsNftPage } from 'hooks/useIsNftPage'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useHasPendingTransactions } from 'state/transactions/hooks'
|
||||
import styled, { useTheme } from 'styled-components/macro'
|
||||
import { BREAKPOINTS, ThemedText } from 'theme'
|
||||
|
||||
import { ActivityTab } from './Activity'
|
||||
import { useHasPendingActivity } from './Activity/hooks'
|
||||
import NFTs from './NFTs'
|
||||
import Pools from './Pools'
|
||||
import { PortfolioRowWrapper } from './PortfolioRow'
|
||||
@@ -103,11 +103,11 @@ export default function MiniPortfolio({ account }: { account: string }) {
|
||||
|
||||
const { component: Page, key: currentKey } = Pages[currentPage]
|
||||
|
||||
const { hasPendingActivity } = useHasPendingActivity()
|
||||
const hasPendingTransactions = useHasPendingTransactions()
|
||||
|
||||
useEffect(() => {
|
||||
if (hasPendingActivity && currentKey !== 'activity') setActivityUnread(true)
|
||||
}, [currentKey, hasPendingActivity])
|
||||
if (hasPendingTransactions && currentKey !== 'activity') setActivityUnread(true)
|
||||
}, [currentKey, hasPendingTransactions])
|
||||
|
||||
return (
|
||||
<Trace section={InterfaceSectionName.MINI_PORTFOLIO}>
|
||||
@@ -116,7 +116,7 @@ export default function MiniPortfolio({ account }: { account: string }) {
|
||||
{Pages.map(({ title, loggingElementName, key }, index) => {
|
||||
if (shouldDisableNFTRoutes && loggingElementName.includes('nft')) return null
|
||||
const isUnselectedActivity = key === 'activity' && currentKey !== 'activity'
|
||||
const showActivityIndicator = isUnselectedActivity && (hasPendingActivity || activityUnread)
|
||||
const showActivityIndicator = isUnselectedActivity && (hasPendingTransactions || activityUnread)
|
||||
const handleNavItemClick = () => {
|
||||
setCurrentPage(index)
|
||||
if (key === 'activity') setActivityUnread(false)
|
||||
@@ -133,7 +133,7 @@ export default function MiniPortfolio({ account }: { account: string }) {
|
||||
{showActivityIndicator && (
|
||||
<>
|
||||
|
||||
{hasPendingActivity ? (
|
||||
{hasPendingTransactions ? (
|
||||
<LoaderV2 />
|
||||
) : (
|
||||
<svg width="8" height="8" viewBox="0 0 8 8" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import { useWeb3React } from '@web3-react/core'
|
||||
import { usePortfolioBalancesLazyQuery, usePortfolioBalancesQuery } from 'graphql/data/__generated__/types-and-hooks'
|
||||
import { usePortfolioBalancesLazyQuery } from 'graphql/data/__generated__/types-and-hooks'
|
||||
import { GQL_MAINNET_CHAINS } from 'graphql/data/util'
|
||||
import usePrevious from 'hooks/usePrevious'
|
||||
import { atom, useAtom } from 'jotai'
|
||||
import { PropsWithChildren, useCallback, useEffect, useMemo } from 'react'
|
||||
import { PropsWithChildren, useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import { useAllTransactions } from 'state/transactions/hooks'
|
||||
import { TransactionDetails } from 'state/transactions/types'
|
||||
|
||||
import { useAccountDrawer } from '.'
|
||||
|
||||
const isTxPending = (tx: TransactionDetails) => !tx.receipt
|
||||
function wasPending(previousTxs: { [hash: string]: TransactionDetails | undefined }, current: TransactionDetails) {
|
||||
const previousTx = previousTxs[current.hash]
|
||||
@@ -35,50 +36,36 @@ function useHasUpdatedTx(account: string | undefined) {
|
||||
}, [account, currentChainTxs, previousPendingTxs])
|
||||
}
|
||||
|
||||
export function useCachedPortfolioBalancesQuery({ account }: { account?: string }) {
|
||||
return usePortfolioBalancesQuery({
|
||||
skip: !account,
|
||||
variables: { ownerAddress: account ?? '', chains: GQL_MAINNET_CHAINS },
|
||||
fetchPolicy: 'cache-only', // PrefetchBalancesWrapper handles balance fetching/staleness; this component only reads from cache
|
||||
errorPolicy: 'all',
|
||||
})
|
||||
}
|
||||
|
||||
const hasUnfetchedBalancesAtom = atom<boolean>(true)
|
||||
|
||||
/* Prefetches & caches portfolio balances when the wrapped component is hovered or the user completes a transaction */
|
||||
export default function PrefetchBalancesWrapper({
|
||||
children,
|
||||
shouldFetchOnAccountUpdate,
|
||||
}: PropsWithChildren<{ shouldFetchOnAccountUpdate: boolean }>) {
|
||||
export default function PrefetchBalancesWrapper({ children }: PropsWithChildren) {
|
||||
const { account } = useWeb3React()
|
||||
const [prefetchPortfolioBalances] = usePortfolioBalancesLazyQuery()
|
||||
const [drawerOpen] = useAccountDrawer()
|
||||
|
||||
// Use an atom to track unfetched state to avoid duplicating fetches if this component appears multiple times on the page.
|
||||
const [hasUnfetchedBalances, setHasUnfetchedBalances] = useAtom(hasUnfetchedBalancesAtom)
|
||||
const [hasUnfetchedBalances, setHasUnfetchedBalances] = useState(true)
|
||||
const fetchBalances = useCallback(() => {
|
||||
if (account) {
|
||||
prefetchPortfolioBalances({ variables: { ownerAddress: account, chains: GQL_MAINNET_CHAINS } })
|
||||
setHasUnfetchedBalances(false)
|
||||
}
|
||||
}, [account, prefetchPortfolioBalances, setHasUnfetchedBalances])
|
||||
}, [account, prefetchPortfolioBalances])
|
||||
|
||||
const prevAccount = usePrevious(account)
|
||||
|
||||
// TODO(cartcrom): add delay for refetching on optimism, as there is high latency in new balances being available
|
||||
const hasUpdatedTx = useHasUpdatedTx(account)
|
||||
// Listens for account changes & recently updated transactions to keep portfolio balances fresh in apollo cache
|
||||
useEffect(() => {
|
||||
const accountChanged = prevAccount !== undefined && prevAccount !== account
|
||||
if (hasUpdatedTx || accountChanged) {
|
||||
// The parent configures whether these conditions should trigger an immediate fetch,
|
||||
// if not, we set a flag to fetch on next hover.
|
||||
if (shouldFetchOnAccountUpdate) {
|
||||
// If the drawer is open, fetch balances immediately, else set a flag to fetch on next hover
|
||||
if (drawerOpen) {
|
||||
fetchBalances()
|
||||
} else {
|
||||
setHasUnfetchedBalances(true)
|
||||
}
|
||||
}
|
||||
}, [account, prevAccount, shouldFetchOnAccountUpdate, fetchBalances, hasUpdatedTx, setHasUnfetchedBalances])
|
||||
}, [account, prevAccount, drawerOpen, fetchBalances, hasUpdatedTx])
|
||||
|
||||
const onHover = useCallback(() => {
|
||||
if (hasUnfetchedBalances) fetchBalances()
|
||||
|
||||
@@ -8,7 +8,6 @@ import styled, { useTheme } from 'styled-components/macro'
|
||||
import { ClickableStyle, ThemedText } from 'theme'
|
||||
import ThemeToggle from 'theme/components/ThemeToggle'
|
||||
|
||||
import { AnalyticsToggle } from './AnalyticsToggle'
|
||||
import { GitVersionRow } from './GitVersionRow'
|
||||
import { SlideOutMenu } from './SlideOutMenu'
|
||||
import { SmallBalanceToggle } from './SmallBalanceToggle'
|
||||
@@ -64,7 +63,6 @@ export default function SettingsMenu({ onClose }: { onClose: () => void }) {
|
||||
<ToggleWrapper>
|
||||
<ThemeToggle />
|
||||
<SmallBalanceToggle />
|
||||
<AnalyticsToggle />
|
||||
<TestnetsToggle />
|
||||
</ToggleWrapper>
|
||||
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
import Column from 'components/Column'
|
||||
import Row from 'components/Row'
|
||||
import Toggle from 'components/Toggle'
|
||||
import styled from 'styled-components/macro'
|
||||
import { ThemedText } from 'theme'
|
||||
|
||||
const StyledColumn = styled(Column)`
|
||||
width: 100%;
|
||||
`
|
||||
|
||||
interface SettingsToggleProps {
|
||||
title: string
|
||||
description?: string
|
||||
dataid?: string
|
||||
isActive: boolean
|
||||
toggle: () => void
|
||||
}
|
||||
|
||||
export function SettingsToggle({ title, description, dataid, isActive, toggle }: SettingsToggleProps) {
|
||||
return (
|
||||
<Row align="center">
|
||||
<StyledColumn>
|
||||
<Row>
|
||||
<ThemedText.SubHeaderSmall color="textPrimary">{title}</ThemedText.SubHeaderSmall>
|
||||
</Row>
|
||||
{description && (
|
||||
<Row>
|
||||
<ThemedText.Caption color="textSecondary" lineHeight="16px">
|
||||
{description}
|
||||
</ThemedText.Caption>
|
||||
</Row>
|
||||
)}
|
||||
</StyledColumn>
|
||||
<Toggle id={dataid} isActive={isActive} toggle={toggle} />
|
||||
</Row>
|
||||
)
|
||||
}
|
||||
@@ -1,8 +1,9 @@
|
||||
import { t } from '@lingui/macro'
|
||||
import { Trans } from '@lingui/macro'
|
||||
import Row from 'components/Row'
|
||||
import Toggle from 'components/Toggle'
|
||||
import { useAtom } from 'jotai'
|
||||
import { atomWithStorage } from 'jotai/utils'
|
||||
|
||||
import { SettingsToggle } from './SettingsToggle'
|
||||
import { ThemedText } from 'theme'
|
||||
|
||||
export const hideSmallBalancesAtom = atomWithStorage<boolean>('hideSmallBalances', true)
|
||||
|
||||
@@ -10,10 +11,20 @@ export function SmallBalanceToggle() {
|
||||
const [hideSmallBalances, updateHideSmallBalances] = useAtom(hideSmallBalancesAtom)
|
||||
|
||||
return (
|
||||
<SettingsToggle
|
||||
title={t`Hide small balances`}
|
||||
isActive={hideSmallBalances}
|
||||
toggle={() => void updateHideSmallBalances((value) => !value)}
|
||||
/>
|
||||
<Row align="center">
|
||||
<Row width="50%">
|
||||
<ThemedText.SubHeaderSmall color="primary">
|
||||
<Trans>Hide small balances</Trans>
|
||||
</ThemedText.SubHeaderSmall>
|
||||
</Row>
|
||||
<Row width="50%" justify="flex-end">
|
||||
<Toggle
|
||||
isActive={hideSmallBalances}
|
||||
toggle={() => {
|
||||
updateHideSmallBalances(!hideSmallBalances)
|
||||
}}
|
||||
/>
|
||||
</Row>
|
||||
</Row>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { t } from '@lingui/macro'
|
||||
import { Trans } from '@lingui/macro'
|
||||
import Row from 'components/Row'
|
||||
import Toggle from 'components/Toggle'
|
||||
import { useAtom } from 'jotai'
|
||||
import { atomWithStorage } from 'jotai/utils'
|
||||
|
||||
import { SettingsToggle } from './SettingsToggle'
|
||||
import { ThemedText } from 'theme'
|
||||
|
||||
export const showTestnetsAtom = atomWithStorage<boolean>('showTestnets', false)
|
||||
|
||||
@@ -10,11 +11,21 @@ export function TestnetsToggle() {
|
||||
const [showTestnets, updateShowTestnets] = useAtom(showTestnetsAtom)
|
||||
|
||||
return (
|
||||
<SettingsToggle
|
||||
title={t`Show testnets`}
|
||||
dataid="testnets-toggle"
|
||||
isActive={showTestnets}
|
||||
toggle={() => void updateShowTestnets((value) => !value)}
|
||||
/>
|
||||
<Row align="center">
|
||||
<Row width="50%">
|
||||
<ThemedText.SubHeaderSmall color="primary">
|
||||
<Trans>Show testnets</Trans>
|
||||
</ThemedText.SubHeaderSmall>
|
||||
</Row>
|
||||
<Row width="50%" justify="flex-end">
|
||||
<Toggle
|
||||
id="testnets-toggle"
|
||||
isActive={showTestnets}
|
||||
toggle={() => {
|
||||
updateShowTestnets(!showTestnets)
|
||||
}}
|
||||
/>
|
||||
</Row>
|
||||
</Row>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import { sendAnalyticsEvent } from '@uniswap/analytics'
|
||||
import { InterfaceElementName } from '@uniswap/analytics-events'
|
||||
import { WalletConnect as WalletConnectv2 } from '@web3-react/walletconnect-v2'
|
||||
import { sendAnalyticsEvent } from 'analytics'
|
||||
import Column, { AutoColumn } from 'components/Column'
|
||||
import Modal from 'components/Modal'
|
||||
import { RowBetween } from 'components/Row'
|
||||
@@ -13,7 +13,6 @@ import { QRCodeSVG } from 'qrcode.react'
|
||||
import { useEffect, useState } from 'react'
|
||||
import styled, { useTheme } from 'styled-components/macro'
|
||||
import { CloseIcon, ThemedText } from 'theme'
|
||||
import { isIOS } from 'utils/userAgent'
|
||||
|
||||
import uniPng from '../../assets/images/uniwallet_modal_icon.png'
|
||||
import { DownloadButton } from './DownloadButton'
|
||||
@@ -42,9 +41,8 @@ export default function UniwalletModal() {
|
||||
const { activationState, cancelActivation } = useActivationState()
|
||||
const [uri, setUri] = useState<string>()
|
||||
|
||||
// Displays the modal if not on iOS, a Uniswap Wallet Connection is pending, & qrcode URI is available
|
||||
// Displays the modal if a Uniswap Wallet Connection is pending & qrcode URI is available
|
||||
const open =
|
||||
!isIOS &&
|
||||
activationState.status === ActivationStatus.PENDING &&
|
||||
activationState.connection.type === ConnectionType.UNISWAP_WALLET_V2 &&
|
||||
!!uri
|
||||
|
||||
@@ -1,17 +1,14 @@
|
||||
import { TraceEvent } from '@uniswap/analytics'
|
||||
import { BrowserEvent, InterfaceEventName } from '@uniswap/analytics-events'
|
||||
import { TraceEvent } from 'analytics'
|
||||
import { ScrollBarStyles } from 'components/Common'
|
||||
import useDisableScrolling from 'hooks/useDisableScrolling'
|
||||
import { useWindowSize } from 'hooks/useWindowSize'
|
||||
import { atom } from 'jotai'
|
||||
import { useAtomValue, useUpdateAtom } from 'jotai/utils'
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { useCallback, useEffect, useRef } from 'react'
|
||||
import { ChevronsRight } from 'react-feather'
|
||||
import { useGesture } from 'react-use-gesture'
|
||||
import styled from 'styled-components/macro'
|
||||
import { BREAKPOINTS, ClickableStyle } from 'theme'
|
||||
import { Z_INDEX } from 'theme/zIndex'
|
||||
import { isMobile } from 'utils/userAgent'
|
||||
|
||||
import DefaultMenu from './DefaultMenu'
|
||||
|
||||
@@ -95,7 +92,7 @@ const Container = styled.div`
|
||||
z-index: ${Z_INDEX.fixed};
|
||||
|
||||
@media only screen and (max-width: ${({ theme }) => `${theme.breakpoint.sm}px`}) {
|
||||
top: 100vh;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
width: 100%;
|
||||
@@ -184,53 +181,21 @@ function AccountDrawer() {
|
||||
}
|
||||
}, [walletDrawerOpen, toggleWalletDrawer])
|
||||
|
||||
// useStates for detecting swipe gestures
|
||||
const [yPosition, setYPosition] = useState(0)
|
||||
const [dragStartTop, setDragStartTop] = useState(true)
|
||||
useDisableScrolling(walletDrawerOpen)
|
||||
|
||||
// useGesture hook for detecting swipe gestures
|
||||
const bind = useGesture({
|
||||
// if the drawer is open and the user is dragging down, close the drawer
|
||||
onDrag: (state) => {
|
||||
// if the user is dragging up, set dragStartTop to false
|
||||
if (state.movement[1] < 0) {
|
||||
setDragStartTop(false)
|
||||
if (scrollRef.current) {
|
||||
scrollRef.current.style.overflowY = 'auto'
|
||||
}
|
||||
} else if (
|
||||
(state.movement[1] > 300 || (state.velocity > 3 && state.direction[1] > 0)) &&
|
||||
walletDrawerOpen &&
|
||||
dragStartTop
|
||||
) {
|
||||
// close on escape keypress
|
||||
useEffect(() => {
|
||||
const escapeKeyDownHandler = (event: KeyboardEvent) => {
|
||||
if (event.key === 'Escape' && walletDrawerOpen) {
|
||||
event.preventDefault()
|
||||
toggleWalletDrawer()
|
||||
} else if (walletDrawerOpen && dragStartTop && state.movement[1] > 0) {
|
||||
setYPosition(state.movement[1])
|
||||
if (scrollRef.current) {
|
||||
scrollRef.current.style.overflowY = 'hidden'
|
||||
}
|
||||
}
|
||||
},
|
||||
// reset the yPosition when the user stops dragging
|
||||
onDragEnd: () => {
|
||||
setYPosition(0)
|
||||
if (scrollRef.current) {
|
||||
scrollRef.current.style.overflowY = 'auto'
|
||||
}
|
||||
},
|
||||
// set dragStartTop to true if the user starts dragging from the top of the drawer
|
||||
onDragStart: () => {
|
||||
if (!scrollRef.current?.scrollTop || scrollRef.current?.scrollTop < 30) {
|
||||
setDragStartTop(true)
|
||||
} else {
|
||||
setDragStartTop(false)
|
||||
if (scrollRef.current) {
|
||||
scrollRef.current.style.overflowY = 'auto'
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
document.addEventListener('keydown', escapeKeyDownHandler)
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('keydown', escapeKeyDownHandler)
|
||||
}
|
||||
}, [walletDrawerOpen, toggleWalletDrawer])
|
||||
|
||||
return (
|
||||
<Container>
|
||||
@@ -246,18 +211,10 @@ function AccountDrawer() {
|
||||
</TraceEvent>
|
||||
)}
|
||||
<Scrim onClick={toggleWalletDrawer} open={walletDrawerOpen} />
|
||||
<AccountDrawerWrapper
|
||||
open={walletDrawerOpen}
|
||||
{...(isMobile
|
||||
? {
|
||||
...bind(),
|
||||
style: { transform: `translateY(${yPosition}px)` },
|
||||
}
|
||||
: {})}
|
||||
>
|
||||
<AccountDrawerWrapper open={walletDrawerOpen}>
|
||||
{/* id used for child InfiniteScrolls to reference when it has reached the bottom of the component */}
|
||||
<AccountDrawerScrollWrapper ref={scrollRef} id="wallet-dropdown-scroll-wrapper">
|
||||
<DefaultMenu drawerOpen={walletDrawerOpen} />
|
||||
<DefaultMenu />
|
||||
</AccountDrawerScrollWrapper>
|
||||
</AccountDrawerWrapper>
|
||||
</Container>
|
||||
|
||||
@@ -22,9 +22,7 @@ export default function AnimatedDropdown({ open, children }: React.PropsWithChil
|
||||
},
|
||||
})
|
||||
return (
|
||||
<animated.div
|
||||
style={{ ...props, overflow: 'hidden', width: '100%', minWidth: 'min-content', willChange: 'height' }}
|
||||
>
|
||||
<animated.div style={{ ...props, overflow: 'hidden', width: '100%', willChange: 'height' }}>
|
||||
<div ref={ref}>{children}</div>
|
||||
</animated.div>
|
||||
)
|
||||
|
||||
@@ -83,3 +83,8 @@ const Badge = styled.div<PropsWithChildren<BadgeProps>>`
|
||||
`
|
||||
|
||||
export default Badge
|
||||
|
||||
export const SmallBadge = styled(Badge)`
|
||||
border-radius: 5px;
|
||||
padding: 2px 4px;
|
||||
`
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Trans } from '@lingui/macro'
|
||||
import { InterfaceElementName } from '@uniswap/analytics-events'
|
||||
import walletBannerPhoneImageSrc from 'assets/images/wallet_banner_phone_image.png'
|
||||
import { ReactComponent as AppleLogo } from 'assets/svg/apple_logo.svg'
|
||||
import { openDownloadApp, openWalletMicrosite } from 'components/AccountDrawer/DownloadButton'
|
||||
import { BaseButton } from 'components/Button'
|
||||
import { AutoColumn } from 'components/Column'
|
||||
import { OpacityHoverState } from 'components/Common'
|
||||
@@ -13,7 +14,6 @@ import { useHideUniswapWalletBanner } from 'state/user/hooks'
|
||||
import styled from 'styled-components/macro'
|
||||
import { ThemedText } from 'theme'
|
||||
import { Z_INDEX } from 'theme/zIndex'
|
||||
import { openDownloadApp, openWalletMicrosite } from 'utils/openDownloadApp'
|
||||
import { isIOS, isMobileSafari } from 'utils/userAgent'
|
||||
|
||||
const PopupContainer = styled.div<{ show: boolean }>`
|
||||
@@ -119,11 +119,7 @@ export default function UniswapWalletBanner() {
|
||||
<>
|
||||
<BannerButton
|
||||
backgroundColor="white"
|
||||
onClick={() =>
|
||||
openDownloadApp({
|
||||
element: InterfaceElementName.UNISWAP_WALLET_BANNER_DOWNLOAD_BUTTON,
|
||||
})
|
||||
}
|
||||
onClick={() => openDownloadApp(InterfaceElementName.UNISWAP_WALLET_BANNER_DOWNLOAD_BUTTON)}
|
||||
>
|
||||
<AppleLogo width={14} height={14} />
|
||||
<ThemedText.LabelSmall color="black" marginLeft="5px">
|
||||
@@ -131,14 +127,14 @@ export default function UniswapWalletBanner() {
|
||||
</ThemedText.LabelSmall>
|
||||
</BannerButton>
|
||||
|
||||
<BannerButton backgroundColor="black" onClick={() => openWalletMicrosite()}>
|
||||
<BannerButton backgroundColor="black" onClick={openWalletMicrosite}>
|
||||
<ThemedText.LabelSmall color="white">
|
||||
<Trans>Learn more</Trans>
|
||||
</ThemedText.LabelSmall>
|
||||
</BannerButton>
|
||||
</>
|
||||
) : (
|
||||
<BannerButton backgroundColor="white" width="125px" onClick={() => openWalletMicrosite()}>
|
||||
<BannerButton backgroundColor="white" width="125px" onClick={openWalletMicrosite}>
|
||||
<ThemedText.LabelSmall color="black">
|
||||
<Trans>Learn more</Trans>
|
||||
</ThemedText.LabelSmall>
|
||||
|
||||
@@ -37,7 +37,7 @@ export function FiatValue({
|
||||
|
||||
return (
|
||||
<Row gap="sm">
|
||||
<ThemedText.BodySmall color="textSecondary">
|
||||
<ThemedText.BodySmall>
|
||||
{fiatValue.data ? (
|
||||
formatNumber(fiatValue.data, NumberType.FiatTokenPrice)
|
||||
) : (
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import { TraceEvent } from '@uniswap/analytics'
|
||||
import { BrowserEvent, InterfaceElementName, SwapEventName } from '@uniswap/analytics-events'
|
||||
import { formatCurrencyAmount, NumberType } from '@uniswap/conedison/format'
|
||||
import { Currency, CurrencyAmount, Percent } from '@uniswap/sdk-core'
|
||||
import { Pair } from '@uniswap/v2-sdk'
|
||||
import { useWeb3React } from '@web3-react/core'
|
||||
import { TraceEvent } from 'analytics'
|
||||
import PrefetchBalancesWrapper from 'components/AccountDrawer/PrefetchBalancesWrapper'
|
||||
import { AutoColumn } from 'components/Column'
|
||||
import { LoadingOpacityContainer, loadingOpacityMixin } from 'components/Loader/styled'
|
||||
import CurrencyLogo from 'components/Logo/CurrencyLogo'
|
||||
@@ -71,7 +70,7 @@ const CurrencySelect = styled(ButtonGray)<{
|
||||
user-select: none;
|
||||
border: none;
|
||||
font-size: 24px;
|
||||
font-weight: 500;
|
||||
font-weight: 400;
|
||||
width: ${({ hideInput }) => (hideInput ? '100%' : 'initial')};
|
||||
padding: ${({ selected }) => (selected ? '4px 8px 4px 4px' : '6px 6px 6px 8px')};
|
||||
gap: 8px;
|
||||
@@ -189,7 +188,7 @@ interface SwapCurrencyInputPanelProps {
|
||||
onUserInput: (value: string) => void
|
||||
onMax?: () => void
|
||||
showMaxButton: boolean
|
||||
label: ReactNode
|
||||
label?: ReactNode
|
||||
onCurrencySelect?: (currency: Currency) => void
|
||||
currency?: Currency | null
|
||||
hideBalance?: boolean
|
||||
@@ -229,7 +228,6 @@ export default function SwapCurrencyInputPanel({
|
||||
locked = false,
|
||||
loading = false,
|
||||
disabled = false,
|
||||
label,
|
||||
...rest
|
||||
}: SwapCurrencyInputPanelProps) {
|
||||
const [modalOpen, setModalOpen] = useState(false)
|
||||
@@ -256,7 +254,6 @@ export default function SwapCurrencyInputPanel({
|
||||
</FixedContainer>
|
||||
)}
|
||||
<Container hideInput={hideInput}>
|
||||
<ThemedText.SubHeaderSmall color="textTertiary">{label}</ThemedText.SubHeaderSmall>
|
||||
<InputRow style={hideInput ? { padding: '0', borderRadius: '8px' } : {}}>
|
||||
{!hideInput && (
|
||||
<StyledNumericalInput
|
||||
@@ -267,46 +264,45 @@ export default function SwapCurrencyInputPanel({
|
||||
$loading={loading}
|
||||
/>
|
||||
)}
|
||||
<PrefetchBalancesWrapper shouldFetchOnAccountUpdate={modalOpen}>
|
||||
<CurrencySelect
|
||||
disabled={!chainAllowed || disabled}
|
||||
visible={currency !== undefined}
|
||||
selected={!!currency}
|
||||
hideInput={hideInput}
|
||||
className="open-currency-select-button"
|
||||
onClick={() => {
|
||||
if (onCurrencySelect) {
|
||||
setModalOpen(true)
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Aligner>
|
||||
<RowFixed>
|
||||
{pair ? (
|
||||
<span style={{ marginRight: '0.5rem' }}>
|
||||
<DoubleCurrencyLogo currency0={pair.token0} currency1={pair.token1} size={24} margin={true} />
|
||||
</span>
|
||||
) : currency ? (
|
||||
<CurrencyLogo style={{ marginRight: '2px' }} currency={currency} size="24px" />
|
||||
) : null}
|
||||
{pair ? (
|
||||
<StyledTokenName className="pair-name-container">
|
||||
{pair?.token0.symbol}:{pair?.token1.symbol}
|
||||
</StyledTokenName>
|
||||
) : (
|
||||
<StyledTokenName className="token-symbol-container" active={Boolean(currency && currency.symbol)}>
|
||||
{(currency && currency.symbol && currency.symbol.length > 20
|
||||
? currency.symbol.slice(0, 4) +
|
||||
'...' +
|
||||
currency.symbol.slice(currency.symbol.length - 5, currency.symbol.length)
|
||||
: currency?.symbol) || <Trans>Select token</Trans>}
|
||||
</StyledTokenName>
|
||||
)}
|
||||
</RowFixed>
|
||||
{onCurrencySelect && <StyledDropDown selected={!!currency} />}
|
||||
</Aligner>
|
||||
</CurrencySelect>
|
||||
</PrefetchBalancesWrapper>
|
||||
|
||||
<CurrencySelect
|
||||
disabled={!chainAllowed || disabled}
|
||||
visible={currency !== undefined}
|
||||
selected={!!currency}
|
||||
hideInput={hideInput}
|
||||
className="open-currency-select-button"
|
||||
onClick={() => {
|
||||
if (onCurrencySelect) {
|
||||
setModalOpen(true)
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Aligner>
|
||||
<RowFixed>
|
||||
{pair ? (
|
||||
<span style={{ marginRight: '0.5rem' }}>
|
||||
<DoubleCurrencyLogo currency0={pair.token0} currency1={pair.token1} size={24} margin={true} />
|
||||
</span>
|
||||
) : currency ? (
|
||||
<CurrencyLogo style={{ marginRight: '2px' }} currency={currency} size="24px" />
|
||||
) : null}
|
||||
{pair ? (
|
||||
<StyledTokenName className="pair-name-container">
|
||||
{pair?.token0.symbol}:{pair?.token1.symbol}
|
||||
</StyledTokenName>
|
||||
) : (
|
||||
<StyledTokenName className="token-symbol-container" active={Boolean(currency && currency.symbol)}>
|
||||
{(currency && currency.symbol && currency.symbol.length > 20
|
||||
? currency.symbol.slice(0, 4) +
|
||||
'...' +
|
||||
currency.symbol.slice(currency.symbol.length - 5, currency.symbol.length)
|
||||
: currency?.symbol) || <Trans>Select token</Trans>}
|
||||
</StyledTokenName>
|
||||
)}
|
||||
</RowFixed>
|
||||
{onCurrencySelect && <StyledDropDown selected={!!currency} />}
|
||||
</Aligner>
|
||||
</CurrencySelect>
|
||||
</InputRow>
|
||||
{Boolean(!hideInput && !hideBalance) && (
|
||||
<FiatRow>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import { TraceEvent } from '@uniswap/analytics'
|
||||
import { BrowserEvent, InterfaceElementName, SwapEventName } from '@uniswap/analytics-events'
|
||||
import { Currency, CurrencyAmount } from '@uniswap/sdk-core'
|
||||
import { Pair } from '@uniswap/v2-sdk'
|
||||
import { useWeb3React } from '@web3-react/core'
|
||||
import { TraceEvent } from 'analytics'
|
||||
import { AutoColumn } from 'components/Column'
|
||||
import { LoadingOpacityContainer, loadingOpacityMixin } from 'components/Loader/styled'
|
||||
import { isSupportedChain } from 'constants/chains'
|
||||
@@ -40,7 +40,7 @@ const FixedContainer = styled.div`
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
border-radius: 16px;
|
||||
border-radius: 20px;
|
||||
background-color: ${({ theme }) => theme.backgroundInteractive};
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -68,11 +68,12 @@ const CurrencySelect = styled(ButtonGray)<{
|
||||
selected: boolean
|
||||
hideInput?: boolean
|
||||
disabled?: boolean
|
||||
pointerEvents?: string
|
||||
}>`
|
||||
align-items: center;
|
||||
background-color: ${({ selected, theme }) => (selected ? theme.backgroundInteractive : theme.accentAction)};
|
||||
opacity: ${({ disabled }) => (!disabled ? 1 : 0.4)};
|
||||
box-shadow: ${({ selected }) => (selected ? 'none' : '0px 6px 10px rgba(0, 0, 0, 0.075)')};
|
||||
box-shadow: 0px 6px 10px rgba(0, 0, 0, 0.075);
|
||||
color: ${({ selected, theme }) => (selected ? theme.textPrimary : theme.white)};
|
||||
cursor: pointer;
|
||||
border-radius: 16px;
|
||||
@@ -91,7 +92,6 @@ const CurrencySelect = styled(ButtonGray)<{
|
||||
background-color: ${({ selected, theme }) => (selected ? theme.deprecated_bg3 : darken(0.05, theme.accentAction))};
|
||||
}
|
||||
visibility: ${({ visible }) => (visible ? 'visible' : 'hidden')};
|
||||
${({ pointerEvents }) => pointerEvents && `pointer-events: none`}
|
||||
`
|
||||
|
||||
const InputRow = styled.div<{ selected: boolean }>`
|
||||
@@ -259,7 +259,6 @@ export default function CurrencyInputPanel({
|
||||
setModalOpen(true)
|
||||
}
|
||||
}}
|
||||
pointerEvents={!onCurrencySelect ? 'none' : undefined}
|
||||
>
|
||||
<Aligner>
|
||||
<RowFixed>
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import { BaseVariant, FeatureFlag, featureFlagSettings, useUpdateFlag } from 'featureFlags'
|
||||
import { useBaseEnabledFlag } from 'featureFlags/flags/baseEnabled'
|
||||
import { useForceUniswapXOnFlag } from 'featureFlags/flags/forceUniswapXOn'
|
||||
import { DetailsV2Variant, useDetailsV2Flag } from 'featureFlags/flags/nftDetails'
|
||||
import { useRoutingAPIForPriceFlag } from 'featureFlags/flags/priceRoutingApi'
|
||||
import { TraceJsonRpcVariant, useTraceJsonRpcFlag } from 'featureFlags/flags/traceJsonRpc'
|
||||
@@ -216,13 +214,7 @@ export default function FeatureFlagModal() {
|
||||
variant={UniswapXVariant}
|
||||
value={useUniswapXFlag()}
|
||||
featureFlag={FeatureFlag.uniswapXEnabled}
|
||||
label="Enable UniswapX on interface"
|
||||
/>
|
||||
<FeatureFlagOption
|
||||
variant={BaseVariant}
|
||||
value={useForceUniswapXOnFlag()}
|
||||
featureFlag={FeatureFlag.forceUniswapXOn}
|
||||
label="Force routing api to enable UniswapX"
|
||||
label="Enable UniswapX"
|
||||
/>
|
||||
<FeatureFlagOption
|
||||
variant={BaseVariant}
|
||||
@@ -236,12 +228,6 @@ export default function FeatureFlagModal() {
|
||||
featureFlag={FeatureFlag.routingAPIPrice}
|
||||
label="Use the routing-api v2 for price fetches"
|
||||
/>
|
||||
<FeatureFlagOption
|
||||
variant={BaseVariant}
|
||||
value={useBaseEnabledFlag()}
|
||||
featureFlag={FeatureFlag.baseEnabled}
|
||||
label="Enable Base"
|
||||
/>
|
||||
<FeatureFlagGroup name="Debug">
|
||||
<FeatureFlagOption
|
||||
variant={TraceJsonRpcVariant}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user