Compare commits
87 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7e1ab5fcd2 | ||
|
|
3f510aabcc | ||
|
|
b29968d014 | ||
|
|
a07556b87d | ||
|
|
5b551af25a | ||
|
|
69f6ca2635 | ||
|
|
2c7381ff47 | ||
|
|
6e4746a7fe | ||
|
|
48379c66ce | ||
|
|
1b7f0d11fd | ||
|
|
db1d264ad3 | ||
|
|
fd24cb890a | ||
|
|
932c4482d2 | ||
|
|
2d8dac5c15 | ||
|
|
0e3d188a9a | ||
|
|
1be62f0bec | ||
|
|
e6519a7dd1 | ||
|
|
3ced65b8a4 | ||
|
|
bab8506919 | ||
|
|
4a79280edc | ||
|
|
53f0ca9b7e | ||
|
|
0381200fec | ||
|
|
040ebb5475 | ||
|
|
0752314d87 | ||
|
|
9db5fd104a | ||
|
|
b9db195017 | ||
|
|
b6bdbcf587 | ||
|
|
cc325b2fbe | ||
|
|
2694379c97 | ||
|
|
82aaf0784a | ||
|
|
55a509cad8 | ||
|
|
463dd6fdfb | ||
|
|
3ad4fb6846 | ||
|
|
1c76277c46 | ||
|
|
f90f81b3d9 | ||
|
|
81accd1864 | ||
|
|
524ce49fcb | ||
|
|
cbec108172 | ||
|
|
3a4dc91e49 | ||
|
|
af80079957 | ||
|
|
c7a8e9e5a7 | ||
|
|
e6362212c6 | ||
|
|
d63bdf1887 | ||
|
|
3bb55c6b5d | ||
|
|
71212f7e32 | ||
|
|
731ff4a485 | ||
|
|
519ba8963a | ||
|
|
ec784ccb36 | ||
|
|
20d8404717 | ||
|
|
809841df0a | ||
|
|
2dc5a6efb4 | ||
|
|
7cd72a706d | ||
|
|
4f8956f79a | ||
|
|
beef7f2d86 | ||
|
|
b667662b49 | ||
|
|
ed87df6269 | ||
|
|
622c72d4a8 | ||
|
|
df6c44d2c4 | ||
|
|
59e7a2867a | ||
|
|
0a31428d7a | ||
|
|
fbc7e64032 | ||
|
|
4a8fb760d2 | ||
|
|
15c510b742 | ||
|
|
e81b0a4d1f | ||
|
|
b9fc65ec9a | ||
|
|
d73c368ee4 | ||
|
|
5e1c430657 | ||
|
|
d4f19e42f8 | ||
|
|
60593df077 | ||
|
|
aeef2c2356 | ||
|
|
54880d201a | ||
|
|
0f6581bf47 | ||
|
|
37c3330897 | ||
|
|
7a981923f6 | ||
|
|
9672c2db9a | ||
|
|
13f57d8d73 | ||
|
|
43f4d0f1b0 | ||
|
|
19c83c92ab | ||
|
|
91c2013522 | ||
|
|
cf09e80934 | ||
|
|
ad9879b4f9 | ||
|
|
c528c6169e | ||
|
|
33c93b5ded | ||
|
|
5ba046f111 | ||
|
|
5414a7c7ef | ||
|
|
22fd0cc7bb | ||
|
|
784fbfe7b1 |
1
.env
@@ -5,6 +5,7 @@ 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_INFURA_KEY="4bf032f2d38a4ed6bb975b80d6340847"
|
||||
REACT_APP_QUICKNODE_MAINNET_RPC_URL="https://magical-alien-tab.quiknode.pro/669e87e569a8277d3fbd9e202f9df93189f19f4c"
|
||||
REACT_APP_MOONPAY_API="https://api.moonpay.com"
|
||||
REACT_APP_MOONPAY_LINK="https://us-central1-uniswap-mobile.cloudfunctions.net/signMoonpayLinkV2?platform=web&env=staging"
|
||||
REACT_APP_MOONPAY_PUBLISHABLE_KEY="pk_test_DycfESRid31UaSxhI5yWKe1r5E5kKSz"
|
||||
|
||||
@@ -12,4 +12,5 @@ REACT_APP_MOONPAY_PUBLISHABLE_KEY="pk_live_uQG4BJC4w3cxnqpcSqAfohdBFDTsY6E"
|
||||
REACT_APP_SENTRY_ENABLED=true
|
||||
REACT_APP_SENTRY_TRACES_SAMPLE_RATE=0.00003
|
||||
REACT_APP_STATSIG_PROXY_URL="https://api.uniswap.org/v1/statsig-proxy"
|
||||
REACT_APP_QUICKNODE_MAINNET_RPC_URL="https://ultra-blue-flower.quiknode.pro/770b22d5f362c537bc8fe19b034c45b22958f880"
|
||||
THE_GRAPH_SCHEMA_ENDPOINT="https://api.thegraph.com/subgraphs/name/uniswap/uniswap-v3?source=uniswap"
|
||||
|
||||
@@ -70,6 +70,13 @@ module.exports = {
|
||||
],
|
||||
},
|
||||
],
|
||||
'no-restricted-syntax': [
|
||||
'error',
|
||||
{
|
||||
selector: ':matches(ExportAllDeclaration)',
|
||||
message: 'Barrel exports bloat the bundle size by preventing tree-shaking.',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
1
.github/workflows/test.yml
vendored
@@ -11,6 +11,7 @@ on:
|
||||
- main
|
||||
- releases/staging
|
||||
pull_request:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
|
||||
1
CODEOWNERS
Normal file
@@ -0,0 +1 @@
|
||||
* @uniswap/web-admins
|
||||
@@ -9,6 +9,7 @@ ignore:
|
||||
- "**/styled.tsx"
|
||||
- "**/constants/**/*"
|
||||
- "constants/**/*"
|
||||
- "src/dev/*"
|
||||
|
||||
coverage:
|
||||
status:
|
||||
|
||||
@@ -5,7 +5,6 @@ const { execSync } = require('child_process')
|
||||
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
|
||||
const path = require('path')
|
||||
const ModuleScopePlugin = require('react-dev-utils/ModuleScopePlugin')
|
||||
const TerserPlugin = require('terser-webpack-plugin')
|
||||
const { IgnorePlugin, ProvidePlugin } = require('webpack')
|
||||
const { RetryChunkLoadPlugin } = require('webpack-retry-chunk-load-plugin')
|
||||
|
||||
@@ -131,6 +130,12 @@ module.exports = {
|
||||
},
|
||||
})
|
||||
|
||||
// Retain source maps for node_modules packages:
|
||||
webpackConfig.module.rules[0] = {
|
||||
...webpackConfig.module.rules[0],
|
||||
exclude: /node_modules/,
|
||||
}
|
||||
|
||||
// Configure webpack transpilation (create-react-app specifies transpilation rules in a oneOf):
|
||||
webpackConfig.module.rules[1].oneOf = webpackConfig.module.rules[1].oneOf.map((rule) => {
|
||||
if (rule.loader && rule.loader.match(/babel-loader/)) {
|
||||
@@ -140,18 +145,20 @@ module.exports = {
|
||||
return rule
|
||||
})
|
||||
|
||||
// Run terser compression on node_modules before tree-shaking, so that tree-shaking is more effective.
|
||||
// This works by eliminating dead code, so that webpack can identify unused imports and tree-shake them;
|
||||
// it is only necessary for node_modules - it is done through linting for our own source code -
|
||||
// see https://medium.com/engineering-housing/dead-code-elimination-and-tree-shaking-at-housing-part-1-307a94b30f23#7e03:
|
||||
webpackConfig.module.rules.push({
|
||||
enforce: 'post',
|
||||
test: /node_modules.*\.(js)$/,
|
||||
loader: path.join(__dirname, 'scripts/terser-loader.js'),
|
||||
options: { compress: true, mangle: false },
|
||||
})
|
||||
|
||||
// Configure webpack optimization:
|
||||
webpackConfig.optimization = Object.assign(
|
||||
webpackConfig.optimization,
|
||||
{
|
||||
minimize: isProduction,
|
||||
minimizer: [
|
||||
new TerserPlugin({
|
||||
minify: TerserPlugin.swcMinify,
|
||||
parallel: require('os').cpus().length,
|
||||
}),
|
||||
],
|
||||
},
|
||||
isProduction
|
||||
? {
|
||||
splitChunks: {
|
||||
@@ -170,13 +177,6 @@ module.exports = {
|
||||
// Configure webpack resolution. webpackConfig.cache is unused with swc-loader, but the resolver can still cache:
|
||||
webpackConfig.resolve = Object.assign(webpackConfig.resolve, { unsafeCache: true })
|
||||
|
||||
webpackConfig.ignoreWarnings = [
|
||||
// Source mappings for a package will fail if the package does not provide them, but the build will still succeed,
|
||||
// so it is unnecessary (and bothersome) to log it. This should be turned off when debugging missing sourcemaps.
|
||||
// See https://webpack.js.org/loaders/source-map-loader#ignoring-warnings.
|
||||
/Failed to parse source map/,
|
||||
]
|
||||
|
||||
return webpackConfig
|
||||
},
|
||||
},
|
||||
|
||||
@@ -39,4 +39,30 @@ describe('Landing Page', () => {
|
||||
cy.get(getTestSelector('pool-nav-link')).last().click()
|
||||
cy.url().should('include', '/pools')
|
||||
})
|
||||
|
||||
it('does not render uk compliance banner in US', () => {
|
||||
cy.visit('/swap')
|
||||
cy.contains('UK disclaimer').should('not.exist')
|
||||
})
|
||||
|
||||
it('renders uk compliance banner in uk', () => {
|
||||
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,
|
||||
server_upload_time: Date.now(),
|
||||
payload_size_bytes: byteSize,
|
||||
events_ingested: req.body.events.length,
|
||||
}),
|
||||
{
|
||||
'origin-country': 'GB',
|
||||
}
|
||||
)
|
||||
})
|
||||
cy.visit('/swap')
|
||||
cy.contains('UK disclaimer')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -2,43 +2,48 @@ import { getTestSelector } from '../../utils'
|
||||
|
||||
describe('Mini Portfolio account drawer', () => {
|
||||
beforeEach(() => {
|
||||
cy.intercept(/api.uniswap.org\/v1\/graphql/, cy.spy().as('gqlSpy'))
|
||||
const portfolioSpy = cy.spy().as('portfolioSpy')
|
||||
cy.intercept(/api.uniswap.org\/v1\/graphql/, (req) => {
|
||||
if (req.body.operationName === 'PortfolioBalances') {
|
||||
portfolioSpy(req)
|
||||
}
|
||||
})
|
||||
cy.visit('/swap')
|
||||
})
|
||||
|
||||
it('fetches balances when account button is first hovered', () => {
|
||||
// The balances should not be fetched before the account button is hovered
|
||||
cy.get('@gqlSpy').should('not.have.been.called')
|
||||
cy.get('@portfolioSpy').should('not.have.been.called')
|
||||
|
||||
// Balances should have been fetched once after hover
|
||||
cy.get(getTestSelector('web3-status-connected')).trigger('mouseover')
|
||||
cy.get('@gqlSpy').should('have.been.calledOnce')
|
||||
cy.get('@portfolioSpy').should('have.been.calledOnce')
|
||||
})
|
||||
|
||||
it('should not re-fetch balances on second hover', () => {
|
||||
// The balances should not be fetched before the account button is hovered
|
||||
cy.get('@gqlSpy').should('not.have.been.called')
|
||||
cy.get('@portfolioSpy').should('not.have.been.called')
|
||||
|
||||
// Balances should have been fetched once after hover
|
||||
cy.get(getTestSelector('web3-status-connected')).trigger('mouseover')
|
||||
cy.get('@gqlSpy').should('have.been.calledOnce')
|
||||
cy.get('@portfolioSpy').should('have.been.calledOnce')
|
||||
|
||||
// Balances should not be refetched upon second hover
|
||||
cy.get(getTestSelector('web3-status-connected')).trigger('mouseover')
|
||||
cy.get('@gqlSpy').should('have.been.calledOnce')
|
||||
cy.get('@portfolioSpy').should('have.been.calledOnce')
|
||||
})
|
||||
|
||||
it('should not re-fetch balances when the account drawer is opened', () => {
|
||||
// The balances should not be fetched before the account button is hovered
|
||||
cy.get('@gqlSpy').should('not.have.been.called')
|
||||
cy.get('@portfolioSpy').should('not.have.been.called')
|
||||
|
||||
// Balances should have been fetched once after hover
|
||||
cy.get(getTestSelector('web3-status-connected')).trigger('mouseover')
|
||||
cy.get('@gqlSpy').should('have.been.calledOnce')
|
||||
cy.get('@portfolioSpy').should('have.been.calledOnce')
|
||||
|
||||
// Balances should not be refetched upon opening drawer
|
||||
cy.get(getTestSelector('web3-status-connected')).click()
|
||||
cy.get('@gqlSpy').should('have.been.calledOnce')
|
||||
cy.get('@portfolioSpy').should('have.been.calledOnce')
|
||||
})
|
||||
|
||||
it('fetches account information', () => {
|
||||
@@ -48,7 +53,7 @@ describe('Mini Portfolio account drawer', () => {
|
||||
|
||||
// Verify that wallet state loads correctly
|
||||
cy.get(getTestSelector('mini-portfolio-navbar')).contains('Tokens')
|
||||
cy.get(getTestSelector('mini-portfolio-page')).contains('Hidden (201)')
|
||||
cy.get(getTestSelector('mini-portfolio-page')).contains('Hidden (197)')
|
||||
|
||||
cy.intercept(/graphql/, { fixture: 'mini-portfolio/nfts.json' })
|
||||
cy.get(getTestSelector('mini-portfolio-navbar')).contains('NFTs').click()
|
||||
|
||||
@@ -1,7 +1,33 @@
|
||||
import { ChainId, MaxUint256, UNI_ADDRESSES } from '@uniswap/sdk-core'
|
||||
|
||||
const UNI_MAINNET = UNI_ADDRESSES[ChainId.MAINNET]
|
||||
|
||||
describe('Remove Liquidity', () => {
|
||||
it('loads the token pair', () => {
|
||||
cy.visit('/remove/v2/ETH/0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984')
|
||||
it('loads the token pair in v2', () => {
|
||||
cy.visit(`/remove/v2/ETH/${UNI_MAINNET}`)
|
||||
cy.get('#remove-liquidity-tokena-symbol').should('contain.text', 'ETH')
|
||||
cy.get('#remove-liquidity-tokenb-symbol').should('contain.text', 'UNI')
|
||||
})
|
||||
|
||||
it('loads the token pair in v3', () => {
|
||||
cy.visit(`/remove/1`)
|
||||
cy.get('#remove-liquidity-tokens').should('contain.text', 'UNI/ETH')
|
||||
|
||||
cy.get('#remove-pooled-tokena-symbol').should('contain.text', 'Pooled UNI')
|
||||
cy.get('#remove-pooled-tokenb-symbol').should('contain.text', 'Pooled ETH')
|
||||
})
|
||||
|
||||
it('should redirect to error pages if pool does not exist', () => {
|
||||
// Duplicate-token v2 pools redirect to position unavailable
|
||||
cy.visit(`/remove/v2/ETH/ETH`)
|
||||
cy.contains('Position unavailable')
|
||||
|
||||
// Single-token pools don't exist
|
||||
cy.visit('/remove/v2/ETH')
|
||||
cy.url().should('match', /\/not-found/)
|
||||
|
||||
// Nonexistent v3 pool
|
||||
cy.visit(`/remove/${MaxUint256}`)
|
||||
cy.contains('Position unavailable')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -6,10 +6,9 @@ describe('Swap settings', () => {
|
||||
cy.contains('Settings').should('not.exist')
|
||||
cy.get(getTestSelector('open-settings-dialog-button')).click()
|
||||
cy.get(getTestSelector('mobile-settings-menu')).should('not.exist')
|
||||
cy.contains('Max slippage').should('exist')
|
||||
cy.contains('Max. slippage').should('exist')
|
||||
cy.contains('Transaction deadline').should('exist')
|
||||
cy.contains('UniswapX').should('exist')
|
||||
cy.contains('Local routing').should('exist')
|
||||
cy.get(getTestSelector('open-settings-dialog-button')).click()
|
||||
cy.contains('Settings').should('not.exist')
|
||||
})
|
||||
@@ -26,9 +25,8 @@ describe('Swap settings', () => {
|
||||
cy.get(getTestSelector('mobile-settings-menu'))
|
||||
.should('exist')
|
||||
.within(() => {
|
||||
cy.contains('Max slippage').should('exist')
|
||||
cy.contains('Max. slippage').should('exist')
|
||||
cy.contains('UniswapX').should('exist')
|
||||
cy.contains('Local routing').should('exist')
|
||||
cy.contains('Transaction deadline').should('exist')
|
||||
cy.get(getTestSelector('mobile-settings-close')).click()
|
||||
})
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
import { SwapEventName } from '@uniswap/analytics-events'
|
||||
import { USDC_MAINNET } from 'constants/tokens'
|
||||
|
||||
import { getTestSelector } from '../../utils'
|
||||
|
||||
describe('Swap inputs with no wallet connected', () => {
|
||||
it('can input and load a quote with no wallet connected', () => {
|
||||
cy.visit(`/swap?inputCurrency=ETH&outputCurrency=${USDC_MAINNET.address}`)
|
||||
|
||||
cy.get(getTestSelector('web3-status-connected')).click()
|
||||
// click twice, first time to show confirmation, second to confirm
|
||||
cy.get(getTestSelector('wallet-disconnect')).click()
|
||||
cy.get(getTestSelector('wallet-disconnect')).should('contain', 'Disconnect')
|
||||
cy.get(getTestSelector('wallet-disconnect')).click()
|
||||
cy.get(getTestSelector('close-account-drawer')).click()
|
||||
|
||||
// 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', '')
|
||||
// Verify logging
|
||||
cy.waitForAmplitudeEvent(SwapEventName.SWAP_QUOTE_RECEIVED).then((event: any) => {
|
||||
cy.wrap(event.event_properties).should('have.property', 'quote_latency_milliseconds')
|
||||
cy.wrap(event.event_properties.quote_latency_milliseconds).should('be.a', 'number')
|
||||
cy.wrap(event.event_properties.quote_latency_milliseconds).should('be.gte', 0)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,4 +1,5 @@
|
||||
import { ChainId, CurrencyAmount } from '@uniswap/sdk-core'
|
||||
import { FeatureFlag } from 'featureFlags'
|
||||
|
||||
import { DAI, nativeOnChain, USDC_MAINNET } from '../../../src/constants/tokens'
|
||||
import { getTestSelector } from '../../utils'
|
||||
@@ -26,7 +27,9 @@ function stubSwapTxReceipt() {
|
||||
describe('UniswapX Toggle', () => {
|
||||
beforeEach(() => {
|
||||
cy.intercept(QuoteEndpoint, { fixture: QuoteWhereUniswapXIsBetter })
|
||||
cy.visit(`/swap/?inputCurrency=${USDC_MAINNET.address}&outputCurrency=${DAI.address}`)
|
||||
cy.visit(`/swap/?inputCurrency=${USDC_MAINNET.address}&outputCurrency=${DAI.address}`, {
|
||||
featureFlags: [{ name: FeatureFlag.uniswapXDefaultEnabled, value: false }],
|
||||
})
|
||||
})
|
||||
|
||||
it('only displays uniswapx ui when setting is on', () => {
|
||||
@@ -76,7 +79,9 @@ describe('UniswapX Orders', () => {
|
||||
stubSwapTxReceipt()
|
||||
|
||||
cy.hardhat().then((hardhat) => hardhat.fund(hardhat.wallet, CurrencyAmount.fromRawAmount(USDC_MAINNET, 3e8)))
|
||||
cy.visit(`/swap/?inputCurrency=${USDC_MAINNET.address}&outputCurrency=${DAI.address}`)
|
||||
cy.visit(`/swap/?inputCurrency=${USDC_MAINNET.address}&outputCurrency=${DAI.address}`, {
|
||||
featureFlags: [{ name: FeatureFlag.uniswapXDefaultEnabled, value: false }],
|
||||
})
|
||||
})
|
||||
|
||||
it('can swap exact-in trades using uniswapX', () => {
|
||||
@@ -164,7 +169,9 @@ describe('UniswapX Eth Input', () => {
|
||||
|
||||
stubSwapTxReceipt()
|
||||
|
||||
cy.visit(`/swap/?inputCurrency=ETH&outputCurrency=${DAI.address}`)
|
||||
cy.visit(`/swap/?inputCurrency=ETH&outputCurrency=${DAI.address}`, {
|
||||
featureFlags: [{ name: FeatureFlag.uniswapXDefaultEnabled, value: false }],
|
||||
})
|
||||
})
|
||||
|
||||
it('can swap using uniswapX with ETH as input', () => {
|
||||
@@ -249,7 +256,9 @@ describe('UniswapX activity history', () => {
|
||||
cy.hardhat().then(async (hardhat) => {
|
||||
await hardhat.fund(hardhat.wallet, CurrencyAmount.fromRawAmount(USDC_MAINNET, 3e8))
|
||||
})
|
||||
cy.visit(`/swap/?inputCurrency=${USDC_MAINNET.address}&outputCurrency=${DAI.address}`)
|
||||
cy.visit(`/swap/?inputCurrency=${USDC_MAINNET.address}&outputCurrency=${DAI.address}`, {
|
||||
featureFlags: [{ name: FeatureFlag.uniswapXDefaultEnabled, value: false }],
|
||||
})
|
||||
})
|
||||
|
||||
it('can view UniswapX order status progress in activity', () => {
|
||||
|
||||
@@ -48,40 +48,33 @@ describe('Token details', () => {
|
||||
})
|
||||
|
||||
it('token with warning and low trading volume should have all information populated', () => {
|
||||
// Shiba predator token, low trading volume and also has warning modal
|
||||
cy.visit('/tokens/ethereum/0xa71d0588EAf47f12B13cF8eC750430d21DF04974')
|
||||
// Null token created for this test, 0 trading volume and has warning modal
|
||||
cy.visit('/tokens/ethereum/0x1eFBB78C8b917f67986BcE54cE575069c0143681')
|
||||
|
||||
// Should have missing price chart when price unavailable (expected for this token)
|
||||
if (cy.get('[data-cy="chart-header"]').contains('Price Unavailable')) {
|
||||
if (cy.get('[data-cy="chart-header"]').contains('Price unavailable')) {
|
||||
cy.get('[data-cy="missing-chart"]').should('exist')
|
||||
}
|
||||
// Stats should have: TVL, 24H Volume, 52W low, 52W high
|
||||
cy.get(getTestSelector('token-details-stats')).should('exist')
|
||||
cy.get(getTestSelector('token-details-stats')).within(() => {
|
||||
cy.get('[data-cy="tvl"]').should('exist')
|
||||
cy.get('[data-cy="volume-24h"]').should('exist')
|
||||
cy.get('[data-cy="52w-low"]').should('exist')
|
||||
cy.get('[data-cy="52w-high"]').should('exist')
|
||||
})
|
||||
|
||||
// Stats should not exist
|
||||
cy.get(getTestSelector('token-details-stats')).should('not.exist')
|
||||
|
||||
// About section should have description of token
|
||||
cy.get(getTestSelector('token-details-about-section')).should('exist')
|
||||
cy.contains('QOM is the Shiba Predator').should('exist')
|
||||
cy.contains('No token information available').should('exist')
|
||||
|
||||
// Links section should link out to Etherscan, More analytics, Website, Twitter
|
||||
// Links section should link out to Etherscan, More analytics
|
||||
cy.get('[data-cy="resources-container"]').within(() => {
|
||||
cy.contains('Etherscan')
|
||||
.should('have.attr', 'href')
|
||||
.and('include', 'etherscan.io/address/0xa71d0588EAf47f12B13cF8eC750430d21DF04974')
|
||||
.and('include', 'etherscan.io/address/0x1eFBB78C8b917f67986BcE54cE575069c0143681')
|
||||
cy.contains('More analytics')
|
||||
.should('have.attr', 'href')
|
||||
.and('include', 'info.uniswap.org/#/tokens/0xa71d0588EAf47f12B13cF8eC750430d21DF04974')
|
||||
cy.contains('Website').should('have.attr', 'href').and('include', 'qom')
|
||||
cy.contains('Twitter').should('have.attr', 'href').and('include', 'twitter.com/ShibaPredator1')
|
||||
.and('include', 'info.uniswap.org/#/tokens/0x1eFBB78C8b917f67986BcE54cE575069c0143681')
|
||||
})
|
||||
|
||||
// Contract address should be displayed
|
||||
cy.contains('0xa71d0588EAf47f12B13cF8eC750430d21DF04974').should('exist')
|
||||
cy.contains('0x1eFBB78C8b917f67986BcE54cE575069c0143681').should('exist')
|
||||
|
||||
// Warning label should show if relevant ([spec](https://www.notion.so/3f7fce6f93694be08a94a6984d50298e))
|
||||
cy.get('[data-cy="token-safety-message"]')
|
||||
|
||||
@@ -65,7 +65,7 @@ describe('Universal search bar', () => {
|
||||
cy.get(getTestSelector('searchbar-token-row-ETHEREUM-NATIVE'))
|
||||
|
||||
// Validate that we go to the searched/selected result.
|
||||
getSearchBar().type('{enter}')
|
||||
cy.get(getTestSelector('searchbar-token-row-ETHEREUM-NATIVE')).click()
|
||||
cy.url().should('contain', 'tokens/ethereum/NATIVE')
|
||||
}
|
||||
)
|
||||
|
||||
@@ -19,7 +19,7 @@ describe('disconnect wallet', () => {
|
||||
// Verify wallet has disconnected
|
||||
cy.contains('Connect a wallet').should('exist')
|
||||
cy.get(getTestSelector('navbar-connect-wallet')).contains('Connect')
|
||||
cy.contains('Connect Wallet')
|
||||
cy.contains('Connect wallet')
|
||||
|
||||
// Verify swap input is cleared
|
||||
cy.get('#swap-currency-input .token-amount-input').should('have.value', '1')
|
||||
|
||||
@@ -53,7 +53,7 @@ describe('Wallet Dropdown', () => {
|
||||
|
||||
describe('should change locale with feature flag', () => {
|
||||
beforeEach(() => {
|
||||
cy.visit('/', { featureFlags: [FeatureFlag.currencyConversion] })
|
||||
cy.visit('/', { featureFlags: [{ name: FeatureFlag.currencyConversion, value: true }] })
|
||||
cy.get(getTestSelector('web3-status-connected')).click()
|
||||
cy.get(getTestSelector('wallet-settings')).click()
|
||||
})
|
||||
@@ -147,19 +147,19 @@ describe('Wallet Dropdown', () => {
|
||||
|
||||
describe('local currency', () => {
|
||||
it('loads local currency from the query param', () => {
|
||||
cy.visit('/', { featureFlags: [FeatureFlag.currencyConversion] })
|
||||
cy.visit('/', { featureFlags: [{ name: FeatureFlag.currencyConversion, value: true }] })
|
||||
cy.get(getTestSelector('web3-status-connected')).click()
|
||||
cy.get(getTestSelector('wallet-settings')).click()
|
||||
cy.contains('USD')
|
||||
|
||||
cy.visit('/?cur=AUD', { featureFlags: [FeatureFlag.currencyConversion] })
|
||||
cy.visit('/?cur=AUD', { featureFlags: [{ name: FeatureFlag.currencyConversion, value: true }] })
|
||||
cy.get(getTestSelector('web3-status-connected')).click()
|
||||
cy.get(getTestSelector('wallet-settings')).click()
|
||||
cy.contains('AUD')
|
||||
})
|
||||
|
||||
it('loads local currency from menu', () => {
|
||||
cy.visit('/', { featureFlags: [FeatureFlag.currencyConversion] })
|
||||
cy.visit('/', { featureFlags: [{ name: FeatureFlag.currencyConversion, value: true }] })
|
||||
cy.get(getTestSelector('web3-status-connected')).click()
|
||||
cy.get(getTestSelector('wallet-settings')).click()
|
||||
cy.contains('USD')
|
||||
|
||||
@@ -24,7 +24,7 @@ declare global {
|
||||
}
|
||||
interface VisitOptions {
|
||||
serviceWorker?: true
|
||||
featureFlags?: Array<FeatureFlag>
|
||||
featureFlags?: Array<{ name: FeatureFlag; value: boolean }>
|
||||
/**
|
||||
* Initial user state.
|
||||
* @default {@type import('../utils/user-state').CONNECTED_WALLET_USER_STATE}
|
||||
@@ -53,14 +53,16 @@ Cypress.Commands.overwrite(
|
||||
|
||||
setInitialUserState(win, {
|
||||
...initialState,
|
||||
hideUniswapWalletBanner: true,
|
||||
...CONNECTED_WALLET_USER_STATE,
|
||||
...(options?.userState ?? {}),
|
||||
})
|
||||
|
||||
// Set feature flags, if configured.
|
||||
if (options?.featureFlags) {
|
||||
const featureFlags = options.featureFlags.reduce((flags, flag) => ({ ...flags, [flag]: 'enabled' }), {})
|
||||
const featureFlags = options.featureFlags.reduce(
|
||||
(flags, flag) => ({ ...flags, [flag.name]: flag.value ? 'enabled' : 'control' }),
|
||||
{}
|
||||
)
|
||||
win.localStorage.setItem('featureFlags', JSON.stringify(featureFlags))
|
||||
}
|
||||
|
||||
|
||||
@@ -9,8 +9,9 @@ beforeEach(() => {
|
||||
req.headers['origin'] = 'https://app.uniswap.org'
|
||||
})
|
||||
|
||||
// Infura is disabled for cypress tests - calls should be routed through the connected wallet instead.
|
||||
// Network RPCs are disabled for cypress tests - calls should be routed through the connected wallet instead.
|
||||
cy.intercept(/infura.io/, { statusCode: 404 })
|
||||
cy.intercept(/quiknode.pro/, { statusCode: 404 })
|
||||
|
||||
// Log requests to hardhat.
|
||||
cy.intercept(/:8545/, logJsonRpc)
|
||||
@@ -26,7 +27,10 @@ beforeEach(() => {
|
||||
server_upload_time: Date.now(),
|
||||
payload_size_bytes: byteSize,
|
||||
events_ingested: req.body.events.length,
|
||||
})
|
||||
}),
|
||||
{
|
||||
'origin-country': 'US',
|
||||
}
|
||||
)
|
||||
}).intercept('https://*.sentry.io', { statusCode: 200 })
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { connectionMetaKey } from '../../src/connection/meta'
|
||||
import { ConnectionType } from '../../src/connection/types'
|
||||
import { UserState } from '../../src/state/user/reducer'
|
||||
|
||||
@@ -10,23 +11,30 @@ export const DISCONNECTED_WALLET_USER_STATE: Partial<UserState> = { selectedWall
|
||||
* Other persisted slices are not set, so they will be filled with their respective initial values
|
||||
* when the app runs.
|
||||
*/
|
||||
export function setInitialUserState(win: Cypress.AUTWindow, initialUserState: any) {
|
||||
export function setInitialUserState(win: Cypress.AUTWindow, state: UserState) {
|
||||
// Selected wallet should also be reflected in localStorage, so that eager connections work.
|
||||
if (state.selectedWallet) {
|
||||
win.localStorage.setItem(
|
||||
connectionMetaKey,
|
||||
JSON.stringify({
|
||||
type: state.selectedWallet,
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
win.indexedDB.deleteDatabase('redux')
|
||||
|
||||
const dbRequest = win.indexedDB.open('redux')
|
||||
|
||||
dbRequest.onsuccess = function () {
|
||||
const db = dbRequest.result
|
||||
const transaction = db.transaction('keyvaluepairs', 'readwrite')
|
||||
const store = transaction.objectStore('keyvaluepairs')
|
||||
store.put(
|
||||
{
|
||||
user: initialUserState,
|
||||
user: state,
|
||||
},
|
||||
'persist:interface'
|
||||
)
|
||||
}
|
||||
|
||||
dbRequest.onupgradeneeded = function () {
|
||||
const db = dbRequest.result
|
||||
db.createObjectStore('keyvaluepairs')
|
||||
|
||||
@@ -11,7 +11,7 @@ export const onRequest: PagesFunction = async ({ request, next }) => {
|
||||
}
|
||||
const res = next()
|
||||
try {
|
||||
return new HTMLRewriter().on('head', new MetaTagInjector(data)).transform(await res)
|
||||
return new HTMLRewriter().on('head', new MetaTagInjector(data, request)).transform(await res)
|
||||
} catch (e) {
|
||||
return res
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import { ImageResponse } from '@vercel/og'
|
||||
import React from 'react'
|
||||
|
||||
import { blocklistedCollections } from '../../../../../src/nft/utils/blocklist'
|
||||
import { WATERMARK_URL } from '../../../../constants'
|
||||
import getAsset from '../../../../utils/getAsset'
|
||||
import getFont from '../../../../utils/getFont'
|
||||
@@ -15,6 +16,10 @@ export const onRequest: PagesFunction = async ({ params, request }) => {
|
||||
const tokenId = index[1]?.toString()
|
||||
const cacheUrl = origin + '/nfts/asset/' + collectionAddress + '/' + tokenId
|
||||
|
||||
if (blocklistedCollections.includes(collectionAddress)) {
|
||||
return new Response('Collection unsupported.', { status: 404 })
|
||||
}
|
||||
|
||||
const data = await getRequest(
|
||||
cacheUrl,
|
||||
() => getAsset(collectionAddress, tokenId, cacheUrl),
|
||||
|
||||
@@ -18,3 +18,12 @@ test.each(invalidAssetImageUrl)('invalidAssetImageUrl', async (url) => {
|
||||
const response = await fetch(new Request(url))
|
||||
expect(response.status).toBe(404)
|
||||
})
|
||||
|
||||
const blockedAssetImageUrl = [
|
||||
'http://127.0.0.1:3000/api/image/nfts/asset/0xd4d871419714b778ebec2e22c7c53572b573706e/276',
|
||||
]
|
||||
|
||||
test.each(blockedAssetImageUrl)('blockedAssetImageUrl', async (url) => {
|
||||
const response = await fetch(new Request(url))
|
||||
expect(response.status).toBe(404)
|
||||
})
|
||||
|
||||
@@ -2,9 +2,10 @@
|
||||
import { ImageResponse } from '@vercel/og'
|
||||
import React from 'react'
|
||||
|
||||
import { blocklistedCollections } from '../../../../../src/nft/utils/blocklist'
|
||||
import { getColor } from '../../../../../src/utils/getColor'
|
||||
import { CHECK_URL, WATERMARK_URL } from '../../../../constants'
|
||||
import getCollection from '../../../../utils/getCollection'
|
||||
import getColor from '../../../../utils/getColor'
|
||||
import getFont from '../../../../utils/getFont'
|
||||
import { getRequest } from '../../../../utils/getRequest'
|
||||
|
||||
@@ -15,6 +16,10 @@ export const onRequest: PagesFunction = async ({ params, request }) => {
|
||||
const collectionAddress = index?.toString()
|
||||
const cacheUrl = origin + '/nfts/collection/' + collectionAddress
|
||||
|
||||
if (blocklistedCollections.includes(collectionAddress)) {
|
||||
return new Response('Collection unsupported.', { status: 404 })
|
||||
}
|
||||
|
||||
const data = await getRequest(
|
||||
cacheUrl,
|
||||
() => getCollection(collectionAddress, cacheUrl),
|
||||
|
||||
@@ -23,3 +23,12 @@ test.each(invalidCollectionImageUrls)('invalidAssetImageUrl', async (url) => {
|
||||
const response = await fetch(new Request(url))
|
||||
expect(response.status).toBeOneOf([404, 500])
|
||||
})
|
||||
|
||||
const blockedCollectionImageUrls = [
|
||||
'http://127.0.0.1:3000/api/image/nfts/collection/0xd4d871419714b778ebec2e22c7c53572b573706e',
|
||||
]
|
||||
|
||||
test.each(blockedCollectionImageUrls)('blockedCollectionImageUrl', async (url) => {
|
||||
const response = await fetch(new Request(url))
|
||||
expect(response.status).toBeOneOf([404, 500])
|
||||
})
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
import { ImageResponse } from '@vercel/og'
|
||||
import React from 'react'
|
||||
|
||||
import { getColor } from '../../../../src/utils/getColor'
|
||||
import { WATERMARK_URL } from '../../../constants'
|
||||
import getColor from '../../../utils/getColor'
|
||||
import getFont from '../../../utils/getFont'
|
||||
import getNetworkLogoUrl from '../../../utils/getNetworkLogoURL'
|
||||
import { getRequest } from '../../../utils/getRequest'
|
||||
|
||||
@@ -6,12 +6,15 @@ test('should append meta tag to element', () => {
|
||||
} as unknown as Element
|
||||
const property = 'property'
|
||||
const content = 'content'
|
||||
const injector = new MetaTagInjector({
|
||||
title: 'test',
|
||||
url: 'testUrl',
|
||||
image: 'testImage',
|
||||
description: 'testDescription',
|
||||
})
|
||||
const injector = new MetaTagInjector(
|
||||
{
|
||||
title: 'test',
|
||||
url: 'testUrl',
|
||||
image: 'testImage',
|
||||
description: 'testDescription',
|
||||
},
|
||||
new Request('http://localhost')
|
||||
)
|
||||
injector.append(element, property, content)
|
||||
expect(element.append).toHaveBeenCalledWith(`<meta property="${property}" content="${content}"/>`, { html: true })
|
||||
|
||||
@@ -36,3 +39,22 @@ test('should append meta tag to element', () => {
|
||||
|
||||
expect(element.append).toHaveBeenCalledTimes(13)
|
||||
})
|
||||
|
||||
test('should pass through header blocked paths', () => {
|
||||
const element = {
|
||||
append: jest.fn(),
|
||||
} as unknown as Element
|
||||
const request = new Request('http://localhost')
|
||||
request.headers.set('x-blocked-paths', '/')
|
||||
const injector = new MetaTagInjector(
|
||||
{
|
||||
title: 'test',
|
||||
url: 'testUrl',
|
||||
image: 'testImage',
|
||||
description: 'testDescription',
|
||||
},
|
||||
request
|
||||
)
|
||||
injector.element(element)
|
||||
expect(element.append).toHaveBeenCalledWith(`<meta property="x:blocked-paths" content="/"/>`, { html: true })
|
||||
})
|
||||
|
||||
@@ -10,7 +10,7 @@ type MetaTagInjectorInput = {
|
||||
* to inject meta tags into the <head> of an HTML document.
|
||||
*/
|
||||
export class MetaTagInjector implements HTMLRewriterElementContentHandlers {
|
||||
constructor(private input: MetaTagInjectorInput) {}
|
||||
constructor(private input: MetaTagInjectorInput, private request: Request) {}
|
||||
|
||||
append(element: Element, property: string, content: string) {
|
||||
element.append(`<meta property="${property}" content="${content}"/>`, { html: true })
|
||||
@@ -38,5 +38,10 @@ export class MetaTagInjector implements HTMLRewriterElementContentHandlers {
|
||||
this.append(element, 'twitter:image', this.input.image)
|
||||
this.append(element, 'twitter:image:alt', this.input.title)
|
||||
}
|
||||
|
||||
const blockedPaths = this.request.headers.get('x-blocked-paths')
|
||||
if (blockedPaths) {
|
||||
this.append(element, 'x:blocked-paths', blockedPaths)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,76 +1,2 @@
|
||||
export const WATERMARK_URL = 'https://app.uniswap.org/images/324x74_App_Watermark.png'
|
||||
export const CHECK_URL = 'https://app.uniswap.org/images/54x54_Verified_Check.svg'
|
||||
|
||||
export const DEFAULT_COLOR = [35, 43, 43]
|
||||
|
||||
export const predefinedTokenColors: { [key: string]: number[] } = {
|
||||
// old WBTC
|
||||
'https://raw.githubusercontent.com/Uniswap/assets/master/blockchains/ethereum/assets/0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599/logo.png':
|
||||
[240, 146, 65],
|
||||
// new WBTC
|
||||
'https://assets.coingecko.com/coins/images/7598/large/wrapped_bitcoin_wbtc.png?1548822744': [240, 146, 65],
|
||||
// DAI
|
||||
'https://raw.githubusercontent.com/Uniswap/assets/master/blockchains/ethereum/assets/0x6B175474E89094C44Da98b954EedeAC495271d0F/logo.png':
|
||||
[250, 176, 27],
|
||||
// UNI
|
||||
'https://raw.githubusercontent.com/Uniswap/assets/master/blockchains/ethereum/assets/0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984/logo.png':
|
||||
[230, 53, 140],
|
||||
// BUSD
|
||||
'https://raw.githubusercontent.com/Uniswap/assets/master/blockchains/ethereum/assets/0x4Fabb145d64652a948d72533023f6E7A623C7C53/logo.png':
|
||||
[239, 186, 9],
|
||||
// AI-X
|
||||
'https://s2.coinmarketcap.com/static/img/coins/64x64/26984.png': [41, 161, 241],
|
||||
// ETH
|
||||
'https://token-icons.s3.amazonaws.com/eth.png': [73, 112, 213],
|
||||
// HARRYPOTTERSHIBAINUBITCOIN
|
||||
'https://assets.coingecko.com/coins/images/30323/large/hpos10i_logo_casino_night-dexview.png?1684117567': [
|
||||
222, 49, 16,
|
||||
],
|
||||
// PEPE
|
||||
'https://raw.githubusercontent.com/Uniswap/assets/master/blockchains/ethereum/assets/0x6982508145454Ce325dDbE47a25d4ec3d2311933/logo.png':
|
||||
[62, 174, 20],
|
||||
// Unibot V2
|
||||
'https://s2.coinmarketcap.com/static/img/coins/64x64/25436.png': [74, 10, 79],
|
||||
// UNIBOT v1
|
||||
'https://assets.coingecko.com/coins/images/30462/small/logonoline_%281%29.png?1687510315': [74, 10, 79],
|
||||
// USDC
|
||||
'https://raw.githubusercontent.com/Uniswap/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png':
|
||||
[0, 102, 217],
|
||||
// HEX
|
||||
'https://raw.githubusercontent.com/Uniswap/assets/master/blockchains/ethereum/assets/0x2b591e99afE9f32eAA6214f7B7629768c40Eeb39/logo.png':
|
||||
[249, 63, 140],
|
||||
// MONG
|
||||
'https://raw.githubusercontent.com/Uniswap/assets/master/blockchains/ethereum/assets/0x1ce270557C1f68Cfb577b856766310Bf8B47FD9C/logo.png':
|
||||
[169, 109, 255],
|
||||
// ARB
|
||||
'https://raw.githubusercontent.com/Uniswap/assets/master/blockchains/ethereum/assets/0xB50721BCf8d664c30412Cfbc6cf7a15145234ad1/logo.png':
|
||||
[41, 161, 241],
|
||||
// PSYOP
|
||||
'https://s2.coinmarketcap.com/static/img/coins/64x64/25422.png': [232, 143, 0],
|
||||
// MATIC
|
||||
'https://raw.githubusercontent.com/Uniswap/assets/master/blockchains/ethereum/assets/0x7D1AfA7B718fb893dB30A3aBc0Cfc608AaCfeBB0/logo.png':
|
||||
[169, 109, 255],
|
||||
// TURBO
|
||||
'https://raw.githubusercontent.com/Uniswap/assets/master/blockchains/ethereum/assets/0xA35923162C49cF95e6BF26623385eb431ad920D3/logo.png':
|
||||
[189, 110, 41],
|
||||
// AIDOGE
|
||||
'https://assets.coingecko.com/coins/images/29852/large/photo_2023-04-18_14-25-28.jpg?1681799160': [41, 161, 241],
|
||||
// SIMPSON
|
||||
'https://assets.coingecko.com/coins/images/30243/large/1111.png?1683692033': [232, 143, 0],
|
||||
// OX
|
||||
'https://assets.coingecko.com/coins/images/30604/large/Logo2.png?1685522119': [41, 89, 217],
|
||||
// ANGLE
|
||||
'https://assets.coingecko.com/coins/images/19060/large/ANGLE_Token-light.png?1666774221': [255, 85, 85],
|
||||
// APE
|
||||
'https://raw.githubusercontent.com/Uniswap/assets/master/blockchains/ethereum/assets/0x4d224452801ACEd8B2F0aebE155379bb5D594381/logo.png':
|
||||
[5, 74, 169],
|
||||
// GUSD
|
||||
'https://raw.githubusercontent.com/Uniswap/assets/master/blockchains/ethereum/assets/0x056Fd409E1d7A124BD7017459dFEa2F387b6d5Cd/logo.png':
|
||||
[0, 164, 189],
|
||||
// OGN
|
||||
'https://raw.githubusercontent.com/Uniswap/assets/master/blockchains/ethereum/assets/0x8207c1FfC5B6804F6024322CcF34F29c3541Ae26/logo.png':
|
||||
[5, 74, 169],
|
||||
// RPL
|
||||
'https://raw.githubusercontent.com/Uniswap/assets/master/blockchains/ethereum/assets/0xD33526068D116cE69F19A9ee46F0bd304F21A51f/logo.png':
|
||||
[255, 123, 79],
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ export const onRequest: PagesFunction = async ({ params, request, next }) => {
|
||||
const { index } = params
|
||||
const collectionAddress = index[0]?.toString()
|
||||
const tokenId = index[1]?.toString()
|
||||
return getMetadataRequest(res, request.url, () => getAsset(collectionAddress, tokenId, request.url))
|
||||
return getMetadataRequest(res, request, () => getAsset(collectionAddress, tokenId, request.url))
|
||||
} catch (e) {
|
||||
return res
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ exports[`should inject metadata for valid assets 1`] = `
|
||||
<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 name="theme-color" content="#fff" />
|
||||
<meta
|
||||
http-equiv="Content-Security-Policy"
|
||||
|
||||
@@ -37,7 +37,8 @@ exports[`should inject metadata for valid assets 1`] = `
|
||||
-->
|
||||
<link rel="manifest" href="/manifest.json" />
|
||||
|
||||
<link rel="preconnect" href="https://api.uniswap.org/v1/amplitude-proxy" />
|
||||
<link rel="preconnect" href="https://api.uniswap.org/" crossorigin/>
|
||||
<link rel="preconnect" href="https://mainnet.infura.io/" crossorigin/>
|
||||
|
||||
<link rel="preload" href="/fonts/Basel-Book.woff" as="font" type="font/woff" crossorigin />
|
||||
<link rel="preload" href="/fonts/Basel-Medium.woff" as="font" type="font/woff" crossorigin />
|
||||
@@ -164,7 +165,7 @@ exports[`should inject metadata for valid assets 2`] = `
|
||||
<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 name="theme-color" content="#fff" />
|
||||
<meta
|
||||
http-equiv="Content-Security-Policy"
|
||||
|
||||
@@ -184,7 +185,8 @@ exports[`should inject metadata for valid assets 2`] = `
|
||||
-->
|
||||
<link rel="manifest" href="/manifest.json" />
|
||||
|
||||
<link rel="preconnect" href="https://api.uniswap.org/v1/amplitude-proxy" />
|
||||
<link rel="preconnect" href="https://api.uniswap.org/" crossorigin/>
|
||||
<link rel="preconnect" href="https://mainnet.infura.io/" crossorigin/>
|
||||
|
||||
<link rel="preload" href="/fonts/Basel-Book.woff" as="font" type="font/woff" crossorigin />
|
||||
<link rel="preload" href="/fonts/Basel-Medium.woff" as="font" type="font/woff" crossorigin />
|
||||
@@ -311,7 +313,7 @@ exports[`should inject metadata for valid assets 3`] = `
|
||||
<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 name="theme-color" content="#fff" />
|
||||
<meta
|
||||
http-equiv="Content-Security-Policy"
|
||||
|
||||
@@ -331,7 +333,8 @@ exports[`should inject metadata for valid assets 3`] = `
|
||||
-->
|
||||
<link rel="manifest" href="/manifest.json" />
|
||||
|
||||
<link rel="preconnect" href="https://api.uniswap.org/v1/amplitude-proxy" />
|
||||
<link rel="preconnect" href="https://api.uniswap.org/" crossorigin/>
|
||||
<link rel="preconnect" href="https://mainnet.infura.io/" crossorigin/>
|
||||
|
||||
<link rel="preload" href="/fonts/Basel-Book.woff" as="font" type="font/woff" crossorigin />
|
||||
<link rel="preload" href="/fonts/Basel-Medium.woff" as="font" type="font/woff" crossorigin />
|
||||
|
||||
@@ -7,7 +7,7 @@ export const onRequest: PagesFunction = async ({ params, request, next }) => {
|
||||
try {
|
||||
const { index } = params
|
||||
const collectionAddress = index?.toString()
|
||||
return getMetadataRequest(res, request.url, () => getCollection(collectionAddress, request.url))
|
||||
return getMetadataRequest(res, request, () => getCollection(collectionAddress, request.url))
|
||||
} catch (e) {
|
||||
return res
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ exports[`should inject metadata for collections 1`] = `
|
||||
<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 name="theme-color" content="#fff" />
|
||||
<meta
|
||||
http-equiv="Content-Security-Policy"
|
||||
|
||||
@@ -37,7 +37,8 @@ exports[`should inject metadata for collections 1`] = `
|
||||
-->
|
||||
<link rel="manifest" href="/manifest.json" />
|
||||
|
||||
<link rel="preconnect" href="https://api.uniswap.org/v1/amplitude-proxy" />
|
||||
<link rel="preconnect" href="https://api.uniswap.org/" crossorigin/>
|
||||
<link rel="preconnect" href="https://mainnet.infura.io/" crossorigin/>
|
||||
|
||||
<link rel="preload" href="/fonts/Basel-Book.woff" as="font" type="font/woff" crossorigin />
|
||||
<link rel="preload" href="/fonts/Basel-Medium.woff" as="font" type="font/woff" crossorigin />
|
||||
@@ -164,7 +165,7 @@ exports[`should inject metadata for collections 2`] = `
|
||||
<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 name="theme-color" content="#fff" />
|
||||
<meta
|
||||
http-equiv="Content-Security-Policy"
|
||||
|
||||
@@ -184,7 +185,8 @@ exports[`should inject metadata for collections 2`] = `
|
||||
-->
|
||||
<link rel="manifest" href="/manifest.json" />
|
||||
|
||||
<link rel="preconnect" href="https://api.uniswap.org/v1/amplitude-proxy" />
|
||||
<link rel="preconnect" href="https://api.uniswap.org/" crossorigin/>
|
||||
<link rel="preconnect" href="https://mainnet.infura.io/" crossorigin/>
|
||||
|
||||
<link rel="preload" href="/fonts/Basel-Book.woff" as="font" type="font/woff" crossorigin />
|
||||
<link rel="preload" href="/fonts/Basel-Medium.woff" as="font" type="font/woff" crossorigin />
|
||||
@@ -311,7 +313,7 @@ exports[`should inject metadata for collections 3`] = `
|
||||
<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 name="theme-color" content="#fff" />
|
||||
<meta
|
||||
http-equiv="Content-Security-Policy"
|
||||
|
||||
@@ -331,7 +333,8 @@ exports[`should inject metadata for collections 3`] = `
|
||||
-->
|
||||
<link rel="manifest" href="/manifest.json" />
|
||||
|
||||
<link rel="preconnect" href="https://api.uniswap.org/v1/amplitude-proxy" />
|
||||
<link rel="preconnect" href="https://api.uniswap.org/" crossorigin/>
|
||||
<link rel="preconnect" href="https://mainnet.infura.io/" crossorigin/>
|
||||
|
||||
<link rel="preload" href="/fonts/Basel-Book.woff" as="font" type="font/woff" crossorigin />
|
||||
<link rel="preload" href="/fonts/Basel-Medium.woff" as="font" type="font/woff" crossorigin />
|
||||
@@ -458,7 +461,7 @@ exports[`should inject metadata for collections 4`] = `
|
||||
<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 name="theme-color" content="#fff" />
|
||||
<meta
|
||||
http-equiv="Content-Security-Policy"
|
||||
|
||||
@@ -478,7 +481,8 @@ exports[`should inject metadata for collections 4`] = `
|
||||
-->
|
||||
<link rel="manifest" href="/manifest.json" />
|
||||
|
||||
<link rel="preconnect" href="https://api.uniswap.org/v1/amplitude-proxy" />
|
||||
<link rel="preconnect" href="https://api.uniswap.org/" crossorigin/>
|
||||
<link rel="preconnect" href="https://mainnet.infura.io/" crossorigin/>
|
||||
|
||||
<link rel="preload" href="/fonts/Basel-Book.woff" as="font" type="font/woff" crossorigin />
|
||||
<link rel="preload" href="/fonts/Basel-Medium.woff" as="font" type="font/woff" crossorigin />
|
||||
|
||||
@@ -11,7 +11,7 @@ export const onRequest: PagesFunction = async ({ params, request, next }) => {
|
||||
if (!tokenAddress) {
|
||||
return res
|
||||
}
|
||||
return getMetadataRequest(res, request.url, () => getToken(networkName, tokenAddress, request.url))
|
||||
return getMetadataRequest(res, request, () => getToken(networkName, tokenAddress, request.url))
|
||||
} catch (e) {
|
||||
return res
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ exports[`should inject metadata for valid tokens 1`] = `
|
||||
<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 name="theme-color" content="#fff" />
|
||||
<meta
|
||||
http-equiv="Content-Security-Policy"
|
||||
|
||||
@@ -37,7 +37,8 @@ exports[`should inject metadata for valid tokens 1`] = `
|
||||
-->
|
||||
<link rel="manifest" href="/manifest.json" />
|
||||
|
||||
<link rel="preconnect" href="https://api.uniswap.org/v1/amplitude-proxy" />
|
||||
<link rel="preconnect" href="https://api.uniswap.org/" crossorigin/>
|
||||
<link rel="preconnect" href="https://mainnet.infura.io/" crossorigin/>
|
||||
|
||||
<link rel="preload" href="/fonts/Basel-Book.woff" as="font" type="font/woff" crossorigin />
|
||||
<link rel="preload" href="/fonts/Basel-Medium.woff" as="font" type="font/woff" crossorigin />
|
||||
@@ -164,7 +165,7 @@ exports[`should inject metadata for valid tokens 2`] = `
|
||||
<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 name="theme-color" content="#fff" />
|
||||
<meta
|
||||
http-equiv="Content-Security-Policy"
|
||||
|
||||
@@ -184,7 +185,8 @@ exports[`should inject metadata for valid tokens 2`] = `
|
||||
-->
|
||||
<link rel="manifest" href="/manifest.json" />
|
||||
|
||||
<link rel="preconnect" href="https://api.uniswap.org/v1/amplitude-proxy" />
|
||||
<link rel="preconnect" href="https://api.uniswap.org/" crossorigin/>
|
||||
<link rel="preconnect" href="https://mainnet.infura.io/" crossorigin/>
|
||||
|
||||
<link rel="preload" href="/fonts/Basel-Book.woff" as="font" type="font/woff" crossorigin />
|
||||
<link rel="preload" href="/fonts/Basel-Medium.woff" as="font" type="font/woff" crossorigin />
|
||||
@@ -311,7 +313,7 @@ exports[`should inject metadata for valid tokens 3`] = `
|
||||
<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 name="theme-color" content="#fff" />
|
||||
<meta
|
||||
http-equiv="Content-Security-Policy"
|
||||
|
||||
@@ -331,7 +333,8 @@ exports[`should inject metadata for valid tokens 3`] = `
|
||||
-->
|
||||
<link rel="manifest" href="/manifest.json" />
|
||||
|
||||
<link rel="preconnect" href="https://api.uniswap.org/v1/amplitude-proxy" />
|
||||
<link rel="preconnect" href="https://api.uniswap.org/" crossorigin/>
|
||||
<link rel="preconnect" href="https://mainnet.infura.io/" crossorigin/>
|
||||
|
||||
<link rel="preload" href="/fonts/Basel-Book.woff" as="font" type="font/woff" crossorigin />
|
||||
<link rel="preload" href="/fonts/Basel-Medium.woff" as="font" type="font/woff" crossorigin />
|
||||
@@ -458,7 +461,7 @@ exports[`should inject metadata for valid tokens 4`] = `
|
||||
<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 name="theme-color" content="#fff" />
|
||||
<meta
|
||||
http-equiv="Content-Security-Policy"
|
||||
|
||||
@@ -478,7 +481,8 @@ exports[`should inject metadata for valid tokens 4`] = `
|
||||
-->
|
||||
<link rel="manifest" href="/manifest.json" />
|
||||
|
||||
<link rel="preconnect" href="https://api.uniswap.org/v1/amplitude-proxy" />
|
||||
<link rel="preconnect" href="https://api.uniswap.org/" crossorigin/>
|
||||
<link rel="preconnect" href="https://mainnet.infura.io/" crossorigin/>
|
||||
|
||||
<link rel="preload" href="/fonts/Basel-Book.woff" as="font" type="font/woff" crossorigin />
|
||||
<link rel="preload" href="/fonts/Basel-Medium.woff" as="font" type="font/woff" crossorigin />
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
import { DEFAULT_COLOR } from '../constants'
|
||||
import getColor from './getColor'
|
||||
|
||||
test('should return the average color of a black PNG image', async () => {
|
||||
const image = 'https://static.vecteezy.com/system/resources/previews/001/209/957/original/square-png.png'
|
||||
const color = await getColor(image)
|
||||
expect(color).toEqual([0, 0, 0])
|
||||
})
|
||||
|
||||
test('should return the average color of a blue PNG image', async () => {
|
||||
const image = 'https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcTB2Ztcim-RKbOu57kfjYpXnnS1MO5YMUaUH9Lk5Eg&s'
|
||||
const color = await getColor(image)
|
||||
expect(color).toEqual([2, 6, 251])
|
||||
})
|
||||
|
||||
test('should return the average color of a white PNG image', async () => {
|
||||
const image = 'https://www.cac.cornell.edu/wiki/images/4/44/White_square.png'
|
||||
const color = await getColor(image)
|
||||
expect(color).toEqual([255, 255, 255])
|
||||
})
|
||||
|
||||
test('should return the average color of a white PNG image with whiteness dimmed', async () => {
|
||||
const image = 'https://www.cac.cornell.edu/wiki/images/4/44/White_square.png'
|
||||
const color = await getColor(image, true)
|
||||
expect(color).toEqual(DEFAULT_COLOR)
|
||||
})
|
||||
|
||||
test('should return the average color of a black JPG image', async () => {
|
||||
const image =
|
||||
'https://imageio.forbes.com/specials-images/imageserve/5ed6636cdd5d320006caf841/0x0.jpg?format=jpg&width=1200'
|
||||
const color = await getColor(image)
|
||||
expect(color).toEqual([0, 0, 0])
|
||||
})
|
||||
|
||||
test('should return default color for a gif image', async () => {
|
||||
const image = 'https://thumbs.gfycat.com/AgitatedLiveAgouti-size_restricted.gif'
|
||||
const color = await getColor(image)
|
||||
expect(color).toEqual(DEFAULT_COLOR)
|
||||
})
|
||||
@@ -4,13 +4,13 @@ import { Data } from './cache'
|
||||
|
||||
export async function getMetadataRequest(
|
||||
res: Promise<Response>,
|
||||
url: string,
|
||||
request: Request,
|
||||
getData: () => Promise<Data | undefined>
|
||||
) {
|
||||
try {
|
||||
const cachedData = await getRequest(url, getData, (data): data is Data => true)
|
||||
const cachedData = await getRequest(request.url, getData, (data): data is Data => true)
|
||||
if (cachedData) {
|
||||
return new HTMLRewriter().on('head', new MetaTagInjector(cachedData)).transform(await res)
|
||||
return new HTMLRewriter().on('head', new MetaTagInjector(cachedData, request)).transform(await res)
|
||||
} else {
|
||||
return res
|
||||
}
|
||||
|
||||
33
package.json
@@ -13,14 +13,15 @@
|
||||
"graphql:generate:thegraph": "graphql-codegen --config graphql.thegraph.codegen.config.ts",
|
||||
"graphql:generate": "yarn graphql:generate:data && yarn graphql:generate:thegraph",
|
||||
"graphql": "yarn graphql:fetch && yarn graphql:generate",
|
||||
"sitemap:generate": "node scripts/generate-sitemap.js",
|
||||
"i18n:extract": "lingui extract --locale en-US",
|
||||
"i18n:compile": "lingui compile",
|
||||
"i18n": "yarn i18n:extract --clean && yarn i18n:compile",
|
||||
"prepare": "concurrently \"npm:ajv\" \"npm:contracts\" \"npm:graphql\" \"npm:i18n\"",
|
||||
"prepare": "concurrently \"npm:ajv\" \"npm:contracts\" \"npm:graphql\" \"npm:i18n\" \"npm:sitemap:generate\"",
|
||||
"start": "craco start",
|
||||
"start:cloud": "NODE_OPTIONS=--dns-result-order=ipv4first PORT=3001 npx wrangler pages dev --compatibility-flags=nodejs_compat --compatibility-date=2023-08-01 --proxy=3001 --port=3000 -- yarn start",
|
||||
"build": "craco build",
|
||||
"analyze": "source-map-explorer 'build/static/js/*.js' --only-mapped",
|
||||
"analyze": "source-map-explorer 'build/static/js/*.js' --no-border-checks --gzip",
|
||||
"serve": "serve build -s -l 3000",
|
||||
"lint": "yarn eslint --ignore-path .gitignore --cache --cache-location node_modules/.cache/eslint/ .",
|
||||
"typecheck": "tsc",
|
||||
@@ -114,6 +115,7 @@
|
||||
"@types/ua-parser-js": "^0.7.36",
|
||||
"@types/uuid": "^8.3.4",
|
||||
"@types/wcag-contrast": "^3.0.0",
|
||||
"@types/xml2js": "^0.4.12",
|
||||
"@uniswap/default-token-list": "^11.2.0",
|
||||
"@uniswap/eslint-config": "^1.2.0",
|
||||
"@vanilla-extract/jest-transform": "^1.1.1",
|
||||
@@ -152,6 +154,7 @@
|
||||
"source-map-explorer": "^2.5.3",
|
||||
"start-server-and-test": "^2.0.0",
|
||||
"swc-loader": "^0.2.3",
|
||||
"terser": "^5.19.4",
|
||||
"terser-webpack-plugin": "^5.3.9",
|
||||
"ts-jest": "^29.1.1",
|
||||
"ts-transform-graphql-tag": "^0.2.1",
|
||||
@@ -189,8 +192,8 @@
|
||||
"@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.22.0",
|
||||
"@uniswap/analytics": "1.5.0",
|
||||
"@uniswap/analytics-events": "^2.24.0",
|
||||
"@uniswap/governance": "^1.0.2",
|
||||
"@uniswap/liquidity-staker": "^1.0.2",
|
||||
"@uniswap/merkle-distributor": "^1.0.1",
|
||||
@@ -219,16 +222,16 @@
|
||||
"@visx/react-spring": "^2.12.2",
|
||||
"@visx/responsive": "^2.10.0",
|
||||
"@visx/shape": "^2.11.1",
|
||||
"@web3-react/coinbase-wallet": "^8.2.2",
|
||||
"@web3-react/core": "^8.2.2",
|
||||
"@web3-react/eip1193": "^8.2.2",
|
||||
"@web3-react/empty": "^8.2.2",
|
||||
"@web3-react/gnosis-safe": "^8.2.3",
|
||||
"@web3-react/metamask": "^8.2.3",
|
||||
"@web3-react/network": "^8.2.2",
|
||||
"@web3-react/types": "^8.2.2",
|
||||
"@web3-react/url": "^8.2.2",
|
||||
"@web3-react/walletconnect-v2": "^8.5.0",
|
||||
"@web3-react/coinbase-wallet": "^8.2.3",
|
||||
"@web3-react/core": "^8.2.3",
|
||||
"@web3-react/eip1193": "^8.2.3",
|
||||
"@web3-react/empty": "^8.2.3",
|
||||
"@web3-react/gnosis-safe": "^8.2.4",
|
||||
"@web3-react/metamask": "^8.2.4",
|
||||
"@web3-react/network": "^8.2.3",
|
||||
"@web3-react/types": "^8.2.3",
|
||||
"@web3-react/url": "^8.2.3",
|
||||
"@web3-react/walletconnect-v2": "^8.5.1",
|
||||
"ajv": "^8.11.0",
|
||||
"ajv-formats": "^2.1.1",
|
||||
"array.prototype.flat": "^1.2.4",
|
||||
@@ -252,6 +255,7 @@
|
||||
"ms": "^2.1.3",
|
||||
"multicodec": "^3.0.1",
|
||||
"multihashes": "^4.0.2",
|
||||
"nock": "^13.3.3",
|
||||
"node-vibrant": "^3.2.1-alpha.1",
|
||||
"numbro": "^2.3.6",
|
||||
"polished": "^3.3.2",
|
||||
@@ -291,6 +295,7 @@
|
||||
"workbox-navigation-preload": "^6.1.0",
|
||||
"workbox-precaching": "^6.1.0",
|
||||
"workbox-routing": "^6.1.0",
|
||||
"xml2js": "^0.6.2",
|
||||
"zustand": "^4.3.6"
|
||||
},
|
||||
"engines": {
|
||||
|
||||
15
patches/@web3-react+gnosis-safe+8.2.4.patch
Normal file
@@ -0,0 +1,15 @@
|
||||
diff --git a/node_modules/@web3-react/gnosis-safe/dist/index.js b/node_modules/@web3-react/gnosis-safe/dist/index.js
|
||||
index 015a33c..4cd7cde 100644
|
||||
--- a/node_modules/@web3-react/gnosis-safe/dist/index.js
|
||||
+++ b/node_modules/@web3-react/gnosis-safe/dist/index.js
|
||||
@@ -68,8 +68,8 @@ class GnosisSafe extends types_1.Connector {
|
||||
if (this.eagerConnection)
|
||||
return;
|
||||
// kick off import early to minimize waterfalls
|
||||
- const SafeAppProviderPromise = Promise.resolve().then(() => __importStar(require('@safe-global/safe-apps-provider'))).then(({ SafeAppProvider }) => SafeAppProvider);
|
||||
- yield (this.eagerConnection = Promise.resolve().then(() => __importStar(require('@safe-global/safe-apps-sdk'))).then((m) => __awaiter(this, void 0, void 0, function* () {
|
||||
+ const SafeAppProviderPromise = Promise.resolve().then(async () => __importStar(await import('@safe-global/safe-apps-provider'))).then(({ SafeAppProvider }) => SafeAppProvider);
|
||||
+ yield (this.eagerConnection = Promise.resolve().then(async () => __importStar(await import('@safe-global/safe-apps-sdk'))).then((m) => __awaiter(this, void 0, void 0, function* () {
|
||||
this.sdk = new m.default(this.options);
|
||||
const safe = yield Promise.race([
|
||||
this.sdk.safe.getInfo(),
|
||||
@@ -3,27 +3,27 @@
|
||||
"relation": ["delegate_permission/common.handle_all_urls"],
|
||||
"target": {
|
||||
"namespace": "android_app",
|
||||
"package_name": "com.uniswap",
|
||||
"package_name": "com.uniswap.mobile",
|
||||
"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", "F9:E9:E3:F0:04:28:66:62:81:44:50:7E:D6:A9:5F:B9:65:39:02:70:1D:13:74:15:D3:E1:A3:1B:D4:38:3A:1F"]
|
||||
["49:D9:3D:5D:FB:AA:64:A4:64:80:85:0F:39:A8:C1:D9:25:D3:D4:BC:8E:6B:1F:45:0C:EA:AF:B1:0C:27:DF:B8", "F9:E9:E3:F0:04:28:66:62:81:44:50:7E:D6:A9:5F:B9:65:39:02:70:1D:13:74:15:D3:E1:A3:1B:D4:38:3A:1F"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"relation": ["delegate_permission/common.handle_all_urls"],
|
||||
"target": {
|
||||
"namespace": "android_app",
|
||||
"package_name": "com.uniswap.beta",
|
||||
"package_name": "com.uniswap.mobile.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", "54:4B:62:33:17:9B:5F:A8:E6:5D:D3:A6:E5:9D:80:5F:A5:02:7F:E2:14:B8:C1:7A:AC:4B:8D:E0:65:49:87:41"]
|
||||
["75:41:9C:2D:01:4A:88:4E:8D:C6:EF:E5:51:54:28:6B:99:05:31:43:AD:84:B4:EB:39:28:B8:C3:C4:CE:48:E3", "54:4B:62:33:17:9B:5F:A8:E6:5D:D3:A6:E5:9D:80:5F:A5:02:7F:E2:14:B8:C1:7A:AC:4B:8D:E0:65:49:87:41"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"relation": ["delegate_permission/common.handle_all_urls"],
|
||||
"target": {
|
||||
"namespace": "android_app",
|
||||
"package_name": "com.uniswap.dev",
|
||||
"package_name": "com.uniswap.mobile.dev",
|
||||
"sha256_cert_fingerprints":
|
||||
["5A:6D:23:50:2F:1E:0D:01:DC:96:65:F3:3A:18:4C:4C:8C:67:E0:09:99:9B:B1:9B:BF:44:99:D0:D1:D0:FC:5E", "02:E6:1C:76:8C:75:C3:78:C8:8C:FE:7B:2E:8F:4B:E1:FA:47:F2:F6:1A:DB:57:69:4A:41:99:C6:71:2C:AB:E3", "FA:C6:17:45:DC:09:03:78:6F:B9:ED:E6:2A:96:2B:39:9F:73:48:F0:BB:6F:89:9B:83:32:66:75:91:03:3B:9C"]
|
||||
["45:F8:15:02:C5:4F:AD:82:E7:51:F0:9C:D1:CA:77:C8:C9:BF:06:A6:D9:5A:55:4F:9E:B8:5F:81:33:2B:D0:DB", "02:E6:1C:76:8C:75:C3:78:C8:8C:FE:7B:2E:8F:4B:E1:FA:47:F2:F6:1A:DB:57:69:4A:41:99:C6:71:2C:AB:E3", "FA:C6:17:45:DC:09:03:78:6F:B9:ED:E6:2A:96:2B:39:9F:73:48:F0:BB:6F:89:9B:83:32:66:75:91:03:3B:9C"]
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -14,7 +14,7 @@
|
||||
<link rel="apple-touch-icon" sizes="512x512" href="%PUBLIC_URL%/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 name="theme-color" content="#fff" />
|
||||
<meta
|
||||
http-equiv="Content-Security-Policy"
|
||||
<% if (process.env.REACT_APP_CSP_ALLOW_UNSAFE_EVAL) { %>
|
||||
@@ -36,7 +36,8 @@
|
||||
-->
|
||||
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
||||
|
||||
<link rel="preconnect" href="%REACT_APP_AMPLITUDE_PROXY_URL%" />
|
||||
<link rel="preconnect" href="https://api.uniswap.org/" crossorigin/>
|
||||
<link rel="preconnect" href="https://mainnet.infura.io/" crossorigin/>
|
||||
|
||||
<link rel="preload" href="%PUBLIC_URL%/fonts/Basel-Book.woff" as="font" type="font/woff" crossorigin />
|
||||
<link rel="preload" href="%PUBLIC_URL%/fonts/Basel-Medium.woff" as="font" type="font/woff" crossorigin />
|
||||
|
||||
19
public/sitemap.xml
Normal file
@@ -0,0 +1,19 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.sitemaps.org/schemas/sitemap/0.9 http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd">
|
||||
<url loc="https://app.uniswap.org/" lastmod="2023-10-05T17:48:32.538Z" changefreq="weekly" priority="1"/>
|
||||
<url loc="https://app.uniswap.org/tokens" lastmod="2023-10-05T17:48:32.538Z" changefreq="weekly" priority="0.8"/>
|
||||
<url loc="https://app.uniswap.org/send" lastmod="2023-10-05T17:48:32.538Z" changefreq="weekly" priority="0.6"/>
|
||||
<url loc="https://app.uniswap.org/swap" lastmod="2023-10-05T17:48:32.538Z" changefreq="weekly" priority="0.9"/>
|
||||
<url loc="https://app.uniswap.org/pool/v2/find" lastmod="2023-10-05T17:48:32.538Z" changefreq="weekly" priority="0.6"/>
|
||||
<url loc="https://app.uniswap.org/pool/v2" lastmod="2023-10-05T17:48:32.538Z" changefreq="weekly" priority="0.6"/>
|
||||
<url loc="https://app.uniswap.org/pool" lastmod="2023-10-05T17:48:32.538Z" changefreq="weekly" priority="0.6"/>
|
||||
<url loc="https://app.uniswap.org/pools/v2/find" lastmod="2023-10-05T17:48:32.538Z" changefreq="weekly" priority="0.6"/>
|
||||
<url loc="https://app.uniswap.org/pools/v2" lastmod="2023-10-05T17:48:32.538Z" changefreq="weekly" priority="0.6"/>
|
||||
<url loc="https://app.uniswap.org/pools" lastmod="2023-10-05T17:48:32.538Z" changefreq="weekly" priority="0.7"/>
|
||||
<url loc="https://app.uniswap.org/add/v2" lastmod="2023-10-05T17:48:32.538Z" changefreq="weekly" priority="0.6"/>
|
||||
<url loc="https://app.uniswap.org/add" lastmod="2023-10-05T17:48:32.538Z" changefreq="weekly" priority="0.6"/>
|
||||
<url loc="https://app.uniswap.org/increase" lastmod="2023-10-05T17:48:32.538Z" changefreq="weekly" priority="0.6"/>
|
||||
<url loc="https://app.uniswap.org/migrate/v2" lastmod="2023-10-05T17:48:32.538Z" changefreq="weekly" priority="0.6"/>
|
||||
<url loc="https://app.uniswap.org/nfts" lastmod="2023-10-05T17:48:32.538Z" changefreq="weekly" priority="0.6"/>
|
||||
<url loc="https://app.uniswap.org/nfts/profile" lastmod="2023-10-05T17:48:32.538Z" changefreq="weekly" priority="0.6"/>
|
||||
</urlset>
|
||||
25
scripts/generate-sitemap.js
Normal file
@@ -0,0 +1,25 @@
|
||||
/* eslint-env node */
|
||||
|
||||
const fs = require('fs')
|
||||
const { parseStringPromise, Builder } = require('xml2js')
|
||||
|
||||
fs.readFile('./public/sitemap.xml', 'utf8', async (err, data) => {
|
||||
try {
|
||||
const sitemap = await parseStringPromise(data)
|
||||
|
||||
const lastmodDate = new Date().toISOString()
|
||||
if (sitemap.urlset.url) {
|
||||
sitemap.urlset.url.forEach((url) => {
|
||||
url['$'].lastmod = lastmodDate
|
||||
})
|
||||
}
|
||||
const builder = new Builder()
|
||||
const xml = builder.buildObject(sitemap)
|
||||
fs.writeFile('./public/sitemap.xml', xml, (error) => {
|
||||
if (error) throw error
|
||||
console.log('Sitemap updated')
|
||||
})
|
||||
} catch {
|
||||
throw new Error('Error parsing sitemap.xml')
|
||||
}
|
||||
})
|
||||
15
scripts/terser-loader.js
Normal file
@@ -0,0 +1,15 @@
|
||||
/* eslint-env node */
|
||||
|
||||
const { minify } = require('terser')
|
||||
|
||||
module.exports = async function terserLoader(source, map, meta) {
|
||||
const callback = this.async()
|
||||
const options = this.getOptions()
|
||||
try {
|
||||
const data = await minify(source, options)
|
||||
const { code } = data || {}
|
||||
callback(null, code, map, meta)
|
||||
} catch (e) {
|
||||
callback(e)
|
||||
}
|
||||
}
|
||||
@@ -52,3 +52,8 @@ export const sendAnalyticsEvent: typeof sendAnalyticsTraceEvent = (event, proper
|
||||
sendAnalyticsTraceEvent(event, properties)
|
||||
}
|
||||
}
|
||||
|
||||
// This is only used for initial page load so we can get the user's country
|
||||
export const sendInitializationEvent: typeof sendAnalyticsTraceEvent = (event, properties) => {
|
||||
sendAnalyticsTraceEvent(event, properties)
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 3.3 KiB |
@@ -1,4 +1,15 @@
|
||||
<svg width="1024" height="1024" viewBox="0 0 1024 1024" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="512" cy="512" r="512" fill="#8247E5"/>
|
||||
<path d="M681.469 402.456C669.189 395.312 653.224 395.312 639.716 402.456L543.928 457.228L478.842 492.949L383.055 547.721C370.774 554.865 354.81 554.865 341.301 547.721L265.162 504.856C252.882 497.712 244.286 484.614 244.286 470.325V385.786C244.286 371.498 251.654 358.4 265.162 351.256L340.073 309.581C352.353 302.437 368.318 302.437 381.827 309.581L456.737 351.256C469.018 358.4 477.614 371.498 477.614 385.786V440.558L542.7 403.646V348.874C542.7 334.586 535.332 321.488 521.824 314.344L383.055 235.758C370.774 228.614 354.81 228.614 341.301 235.758L200.076 314.344C186.567 321.488 179.199 334.586 179.199 348.874V507.237C179.199 521.525 186.567 534.623 200.076 541.767L341.301 620.353C353.582 627.498 369.546 627.498 383.055 620.353L478.842 566.772L543.928 529.86L639.716 476.279C651.996 469.135 667.961 469.135 681.469 476.279L756.38 517.953C768.66 525.098 777.257 538.195 777.257 552.484V637.023C777.257 651.312 769.888 664.409 756.38 671.553L681.469 714.419C669.189 721.563 653.224 721.563 639.716 714.419L564.805 672.744C552.525 665.6 543.928 652.502 543.928 638.214V583.442L478.842 620.353V675.125C478.842 689.414 486.21 702.512 499.719 709.656L640.944 788.242C653.224 795.386 669.189 795.386 682.697 788.242L823.922 709.656C836.203 702.512 844.799 689.414 844.799 675.125V516.763C844.799 502.474 837.431 489.377 823.922 482.232L681.469 402.456Z" fill="white"/>
|
||||
<svg width="490" height="490" viewBox="0 0 490 490" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_7383_35741)">
|
||||
<circle cx="245" cy="245" r="245" fill="url(#paint0_linear_7383_35741)"/>
|
||||
<path d="M315.83 297.85L385.12 257.84C388.79 255.72 391.06 251.78 391.06 247.54V167.53C391.06 163.3 388.78 159.35 385.12 157.23L315.83 117.22C312.16 115.1 307.61 115.11 303.94 117.22L234.65 157.23C230.98 159.35 228.71 163.3 228.71 167.53V310.52L180.12 338.57L131.53 310.52V254.41L180.12 226.36L212.17 244.86V207.22L186.06 192.15C184.26 191.11 182.2 190.56 180.11 190.56C178.02 190.56 175.96 191.11 174.17 192.15L104.88 232.16C101.21 234.28 98.9404 238.22 98.9404 242.46V322.47C98.9404 326.7 101.22 330.65 104.88 332.77L174.17 372.78C177.83 374.89 182.39 374.89 186.06 372.78L255.35 332.78C259.02 330.66 261.29 326.71 261.29 322.48V179.49L262.17 178.99L309.88 151.44L358.47 179.49V235.6L309.88 263.65L277.88 245.17V282.81L303.94 297.86C307.61 299.97 312.16 299.97 315.83 297.86V297.85Z" fill="white"/>
|
||||
</g>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_7383_35741" x1="-175" y1="4.36391e-07" x2="416" y2="367" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#A229C5"/>
|
||||
<stop offset="1" stop-color="#7B3FE4"/>
|
||||
</linearGradient>
|
||||
<clipPath id="clip0_7383_35741">
|
||||
<rect width="490" height="490" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.3 KiB |
@@ -1,16 +1 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 24.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 38.4 33.5" style="enable-background:new 0 0 38.4 33.5;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:#8247E5;}
|
||||
</style>
|
||||
<g>
|
||||
<path class="st0" d="M29,10.2c-0.7-0.4-1.6-0.4-2.4,0L21,13.5l-3.8,2.1l-5.5,3.3c-0.7,0.4-1.6,0.4-2.4,0L5,16.3
|
||||
c-0.7-0.4-1.2-1.2-1.2-2.1v-5c0-0.8,0.4-1.6,1.2-2.1l4.3-2.5c0.7-0.4,1.6-0.4,2.4,0L16,7.2c0.7,0.4,1.2,1.2,1.2,2.1v3.3l3.8-2.2V7
|
||||
c0-0.8-0.4-1.6-1.2-2.1l-8-4.7c-0.7-0.4-1.6-0.4-2.4,0L1.2,5C0.4,5.4,0,6.2,0,7v9.4c0,0.8,0.4,1.6,1.2,2.1l8.1,4.7
|
||||
c0.7,0.4,1.6,0.4,2.4,0l5.5-3.2l3.8-2.2l5.5-3.2c0.7-0.4,1.6-0.4,2.4,0l4.3,2.5c0.7,0.4,1.2,1.2,1.2,2.1v5c0,0.8-0.4,1.6-1.2,2.1
|
||||
L29,28.8c-0.7,0.4-1.6,0.4-2.4,0l-4.3-2.5c-0.7-0.4-1.2-1.2-1.2-2.1V21l-3.8,2.2v3.3c0,0.8,0.4,1.6,1.2,2.1l8.1,4.7
|
||||
c0.7,0.4,1.6,0.4,2.4,0l8.1-4.7c0.7-0.4,1.2-1.2,1.2-2.1V17c0-0.8-0.4-1.6-1.2-2.1L29,10.2z"/>
|
||||
</g>
|
||||
</svg>
|
||||
<?xml version="1.0" encoding="UTF-8"?><svg id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 500 500"><defs><style>.cls-1{fill:url(#linear-gradient);}</style><linearGradient id="linear-gradient" x1="54.83" y1="392.31" x2="459.03" y2="97.58" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#a726c1"/><stop offset=".88" stop-color="#803bdf"/><stop offset="1" stop-color="#7b3fe4"/></linearGradient></defs><path class="cls-1" d="m364.03,335.08l111.55-64.4c5.9-3.41,9.57-9.76,9.57-16.58V125.28c0-6.81-3.67-13.17-9.57-16.58l-111.55-64.4c-5.9-3.41-13.24-3.4-19.14,0l-111.55,64.4c-5.9,3.41-9.57,9.76-9.57,16.58v230.19l-78.22,45.15-78.22-45.15v-90.33l78.22-45.15,51.6,29.78v-60.59l-42.03-24.26c-2.9-1.67-6.21-2.55-9.57-2.55s-6.67.88-9.57,2.55L24.42,229.33c-5.9,3.41-9.57,9.76-9.57,16.58v128.81c0,6.81,3.67,13.17,9.57,16.58l111.55,64.41c5.9,3.4,13.23,3.4,19.14,0l111.55-64.4c5.9-3.41,9.57-9.77,9.57-16.58v-230.19l1.41-.81,76.81-44.34,78.22,45.16v90.32l-78.22,45.16-51.52-29.74v60.59l41.95,24.23c5.9,3.4,13.24,3.4,19.14,0Z"/></svg>
|
||||
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 1.1 KiB |
@@ -2,7 +2,8 @@ import { BrowserEvent, InterfaceElementName, SharedEventName } from '@uniswap/an
|
||||
import { TraceEvent } from 'analytics'
|
||||
import { useDisableNFTRoutes } from 'hooks/useDisableNFTRoutes'
|
||||
import styled from 'styled-components'
|
||||
import { BREAKPOINTS, ExternalLink, StyledRouterLink } from 'theme'
|
||||
import { BREAKPOINTS } from 'theme'
|
||||
import { ExternalLink, StyledRouterLink } from 'theme/components'
|
||||
import { useIsDarkMode } from 'theme/components/ThemeToggle'
|
||||
|
||||
import { DiscordIcon, GithubIcon, TwitterIcon } from './Icons'
|
||||
|
||||
@@ -9,11 +9,12 @@ import { Power } from 'components/Icons/Power'
|
||||
import { Settings } from 'components/Icons/Settings'
|
||||
import { AutoRow } from 'components/Row'
|
||||
import { LoadingBubble } from 'components/Tokens/loading'
|
||||
import { DeltaArrow, formatDelta } from 'components/Tokens/TokenDetails/Delta'
|
||||
import { DeltaArrow } from 'components/Tokens/TokenDetails/Delta'
|
||||
import Tooltip from 'components/Tooltip'
|
||||
import { getConnection } from 'connection'
|
||||
import { useDisableNFTRoutes } from 'hooks/useDisableNFTRoutes'
|
||||
import useENSName from 'hooks/useENSName'
|
||||
import { useIsNotOriginCountry } from 'hooks/useIsNotOriginCountry'
|
||||
import { useProfilePageState, useSellAsset, useWalletCollections } from 'nft/hooks'
|
||||
import { useIsNftClaimAvailable } from 'nft/hooks/useIsNftClaimAvailable'
|
||||
import { ProfilePageStateType } from 'nft/types'
|
||||
@@ -23,7 +24,7 @@ import { useNavigate } from 'react-router-dom'
|
||||
import { useAppDispatch } from 'state/hooks'
|
||||
import { updateSelectedWallet } from 'state/user/reducer'
|
||||
import styled from 'styled-components'
|
||||
import { CopyHelper, ExternalLink, ThemedText } from 'theme'
|
||||
import { CopyHelper, ExternalLink, ThemedText } from 'theme/components'
|
||||
import { shortenAddress } from 'utils'
|
||||
import { NumberType, useFormatter } from 'utils/formatNumbers'
|
||||
|
||||
@@ -31,11 +32,11 @@ import { useCloseModal, useFiatOnrampAvailability, useOpenModal, useToggleModal
|
||||
import { ApplicationModal } from '../../state/application/reducer'
|
||||
import { useUserHasAvailableClaim, useUserUnclaimedAmount } from '../../state/claim/hooks'
|
||||
import StatusIcon from '../Identicon/StatusIcon'
|
||||
import { useCachedPortfolioBalancesQuery } from '../PrefetchBalancesWrapper/PrefetchBalancesWrapper'
|
||||
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;
|
||||
@@ -159,7 +160,8 @@ export default function AuthenticatedHeader({ account, openSettings }: { account
|
||||
const resetSellAssets = useSellAsset((state) => state.reset)
|
||||
const clearCollectionFilters = useWalletCollections((state) => state.clearCollectionFilters)
|
||||
const isClaimAvailable = useIsNftClaimAvailable((state) => state.isClaimAvailable)
|
||||
const { formatNumber } = useFormatter()
|
||||
const shouldShowBuyFiatButton = useIsNotOriginCountry('GB')
|
||||
const { formatNumber, formatPercent } = useFormatter()
|
||||
|
||||
const shouldDisableNFTRoutes = useDisableNFTRoutes()
|
||||
|
||||
@@ -282,7 +284,7 @@ export default function AuthenticatedHeader({ account, openSettings }: { account
|
||||
{`${formatNumber({
|
||||
input: Math.abs(absoluteChange as number),
|
||||
type: NumberType.PortfolioBalance,
|
||||
})} (${formatDelta(percentChange)})`}
|
||||
})} (${formatPercent(percentChange)})`}
|
||||
</ThemedText.BodySecondary>
|
||||
</>
|
||||
)}
|
||||
@@ -304,26 +306,28 @@ export default function AuthenticatedHeader({ account, openSettings }: { account
|
||||
<Trans>View and sell NFTs</Trans>
|
||||
</HeaderButton>
|
||||
)}
|
||||
<HeaderButton
|
||||
size={ButtonSize.medium}
|
||||
emphasis={ButtonEmphasis.highSoft}
|
||||
onClick={handleBuyCryptoClick}
|
||||
disabled={disableBuyCryptoButton}
|
||||
data-testid="wallet-buy-crypto"
|
||||
>
|
||||
{error ? (
|
||||
<ThemedText.BodyPrimary>{error}</ThemedText.BodyPrimary>
|
||||
) : (
|
||||
<>
|
||||
{fiatOnrampAvailabilityLoading ? (
|
||||
<StyledLoadingButtonSpinner />
|
||||
) : (
|
||||
<CreditCard height="20px" width="20px" />
|
||||
)}{' '}
|
||||
<Trans>Buy crypto</Trans>
|
||||
</>
|
||||
)}
|
||||
</HeaderButton>
|
||||
{shouldShowBuyFiatButton && (
|
||||
<HeaderButton
|
||||
size={ButtonSize.medium}
|
||||
emphasis={ButtonEmphasis.highSoft}
|
||||
onClick={handleBuyCryptoClick}
|
||||
disabled={disableBuyCryptoButton}
|
||||
data-testid="wallet-buy-crypto"
|
||||
>
|
||||
{error ? (
|
||||
<ThemedText.BodyPrimary>{error}</ThemedText.BodyPrimary>
|
||||
) : (
|
||||
<>
|
||||
{fiatOnrampAvailabilityLoading ? (
|
||||
<StyledLoadingButtonSpinner />
|
||||
) : (
|
||||
<CreditCard height="20px" width="20px" />
|
||||
)}{' '}
|
||||
<Trans>Buy crypto</Trans>
|
||||
</>
|
||||
)}
|
||||
</HeaderButton>
|
||||
)}
|
||||
{Boolean(!fiatOnrampAvailable && fiatOnrampAvailabilityChecked) && (
|
||||
<FiatOnrampNotAvailableText marginTop="8px">
|
||||
<Trans>Not available in your region</Trans>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { InterfaceElementName } from '@uniswap/analytics-events'
|
||||
import { PropsWithChildren, useCallback } from 'react'
|
||||
import styled from 'styled-components'
|
||||
import { ClickableStyle } from 'theme'
|
||||
import { ClickableStyle } from 'theme/components'
|
||||
import { openDownloadApp } from 'utils/openDownloadApp'
|
||||
|
||||
const StyledButton = styled.button<{ padded?: boolean; branded?: boolean }>`
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Trans } from '@lingui/macro'
|
||||
import Tooltip from 'components/Tooltip'
|
||||
import useCopyClipboard from 'hooks/useCopyClipboard'
|
||||
import styled from 'styled-components'
|
||||
import { ThemedText } from 'theme'
|
||||
import { ThemedText } from 'theme/components'
|
||||
|
||||
const Container = styled.div`
|
||||
width: 100%;
|
||||
|
||||
@@ -8,7 +8,7 @@ import { TransactionStatus } from 'graphql/data/__generated__/types-and-hooks'
|
||||
import useENSName from 'hooks/useENSName'
|
||||
import { useCallback } from 'react'
|
||||
import styled from 'styled-components'
|
||||
import { EllipsisStyle, ThemedText } from 'theme'
|
||||
import { EllipsisStyle, ThemedText } from 'theme/components'
|
||||
import { shortenAddress } from 'utils'
|
||||
import { ExplorerDataType, getExplorerLink } from 'utils/getExplorerLink'
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ import { InterfaceTrade } from 'state/routing/types'
|
||||
import { useOrder } from 'state/signatures/hooks'
|
||||
import { UniswapXOrderDetails } from 'state/signatures/types'
|
||||
import styled from 'styled-components'
|
||||
import { ExternalLink, ThemedText } from 'theme'
|
||||
import { ExternalLink, ThemedText } from 'theme/components'
|
||||
import { ExplorerDataType, getExplorerLink } from 'utils/getExplorerLink'
|
||||
|
||||
type SelectedOrderInfo = {
|
||||
|
||||
@@ -0,0 +1,374 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`parseRemote parseRemoteActivities should parse NFT approval 1`] = `
|
||||
Object {
|
||||
"chainId": 1,
|
||||
"descriptor": "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045",
|
||||
"from": "0x50EC05ADe8280758E2077fcBC08D878D4aef79C3",
|
||||
"hash": "someHash",
|
||||
"logos": Array [],
|
||||
"nonce": 12345,
|
||||
"status": "CONFIRMED",
|
||||
"timestamp": 10000,
|
||||
"title": "Unknown Approval",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`parseRemote parseRemoteActivities should parse NFT approval for all 1`] = `
|
||||
Object {
|
||||
"chainId": 1,
|
||||
"descriptor": "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045",
|
||||
"from": "0x50EC05ADe8280758E2077fcBC08D878D4aef79C3",
|
||||
"hash": "someHash",
|
||||
"logos": Array [],
|
||||
"nonce": 12345,
|
||||
"status": "CONFIRMED",
|
||||
"timestamp": 10000,
|
||||
"title": "Unknown Approval",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`parseRemote parseRemoteActivities should parse NFT receive 1`] = `
|
||||
Object {
|
||||
"chainId": 1,
|
||||
"currencies": undefined,
|
||||
"descriptor": "1 SomeCollectionName from ",
|
||||
"from": "0x50EC05ADe8280758E2077fcBC08D878D4aef79C3",
|
||||
"hash": "someHash",
|
||||
"logos": Array [
|
||||
"imageUrl",
|
||||
],
|
||||
"nonce": 12345,
|
||||
"otherAccount": "0x50EC05ADe8280758E2077fcBC08D878D4aef79C3",
|
||||
"status": "CONFIRMED",
|
||||
"timestamp": 10000,
|
||||
"title": "Received",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`parseRemote parseRemoteActivities should parse NFT transfer 1`] = `
|
||||
Object {
|
||||
"chainId": 1,
|
||||
"descriptor": "1 SomeCollectionName",
|
||||
"from": "0x50EC05ADe8280758E2077fcBC08D878D4aef79C3",
|
||||
"hash": "someHash",
|
||||
"logos": Array [
|
||||
"imageUrl",
|
||||
],
|
||||
"nonce": 12345,
|
||||
"status": "CONFIRMED",
|
||||
"timestamp": 10000,
|
||||
"title": "Minted",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`parseRemote parseRemoteActivities should parse closed UniswapX order 1`] = `
|
||||
Object {
|
||||
"chainId": 1,
|
||||
"currencies": Array [
|
||||
Token {
|
||||
"address": "0x6B175474E89094C44Da98b954EedeAC495271d0F",
|
||||
"chainId": 1,
|
||||
"decimals": 18,
|
||||
"isNative": false,
|
||||
"isToken": true,
|
||||
"name": "DAI",
|
||||
"symbol": "DAI",
|
||||
},
|
||||
Token {
|
||||
"address": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
|
||||
"chainId": 1,
|
||||
"decimals": 18,
|
||||
"isNative": false,
|
||||
"isToken": true,
|
||||
"name": "Wrapped Ether",
|
||||
"symbol": "WETH",
|
||||
},
|
||||
],
|
||||
"descriptor": "100 DAI for 200 WETH",
|
||||
"from": "someOfferer",
|
||||
"hash": "someHash",
|
||||
"logos": Array [
|
||||
"someUrl",
|
||||
"someUrl",
|
||||
],
|
||||
"offchainOrderStatus": "expired",
|
||||
"prefixIconSrc": "bolt.svg",
|
||||
"status": "FAILED",
|
||||
"statusMessage": "Your swap could not be fulfilled at this time. Please try again.",
|
||||
"timestamp": 10000,
|
||||
"title": "Swap expired",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`parseRemote parseRemoteActivities should parse eth wrap 1`] = `
|
||||
Object {
|
||||
"chainId": 1,
|
||||
"currencies": Array [
|
||||
ExtendedEther {
|
||||
"chainId": 1,
|
||||
"decimals": 18,
|
||||
"isNative": true,
|
||||
"isToken": false,
|
||||
"name": "Ether",
|
||||
"symbol": "ETH",
|
||||
},
|
||||
Token {
|
||||
"address": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
|
||||
"chainId": 1,
|
||||
"decimals": 18,
|
||||
"isNative": false,
|
||||
"isToken": true,
|
||||
"name": "Wrapped Ether",
|
||||
"symbol": "WETH",
|
||||
},
|
||||
],
|
||||
"descriptor": "100 ETH for 100 WETH",
|
||||
"from": "0x50EC05ADe8280758E2077fcBC08D878D4aef79C3",
|
||||
"hash": "someHash",
|
||||
"logos": Array [
|
||||
"https://token-icons.s3.amazonaws.com/eth.png",
|
||||
"https://raw.githubusercontent.com/Uniswap/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png",
|
||||
],
|
||||
"nonce": 12345,
|
||||
"status": "CONFIRMED",
|
||||
"timestamp": 10000,
|
||||
"title": "Wrapped",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`parseRemote parseRemoteActivities should parse moonpay purchase 1`] = `
|
||||
Object {
|
||||
"chainId": 1,
|
||||
"currencies": Array [
|
||||
Token {
|
||||
"address": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
|
||||
"chainId": 1,
|
||||
"decimals": 18,
|
||||
"isNative": false,
|
||||
"isToken": true,
|
||||
"name": "Wrapped Ether",
|
||||
"symbol": "WETH",
|
||||
},
|
||||
],
|
||||
"descriptor": "100 WETH for 100",
|
||||
"from": "0x50EC05ADe8280758E2077fcBC08D878D4aef79C3",
|
||||
"hash": "someHash",
|
||||
"logos": Array [
|
||||
"moonpay.svg",
|
||||
],
|
||||
"nonce": 12345,
|
||||
"status": "CONFIRMED",
|
||||
"timestamp": 10000,
|
||||
"title": "Purchased",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`parseRemote parseRemoteActivities should parse nft purchase 1`] = `
|
||||
Object {
|
||||
"chainId": 1,
|
||||
"descriptor": "1 SomeCollectionName",
|
||||
"from": "0x50EC05ADe8280758E2077fcBC08D878D4aef79C3",
|
||||
"hash": "someHash",
|
||||
"logos": Array [
|
||||
"imageUrl",
|
||||
],
|
||||
"nonce": 12345,
|
||||
"status": "CONFIRMED",
|
||||
"timestamp": 10000,
|
||||
"title": "Bought",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`parseRemote parseRemoteActivities should parse receive 1`] = `
|
||||
Object {
|
||||
"chainId": 1,
|
||||
"currencies": Array [
|
||||
Token {
|
||||
"address": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
|
||||
"chainId": 1,
|
||||
"decimals": 18,
|
||||
"isNative": false,
|
||||
"isToken": true,
|
||||
"name": "Wrapped Ether",
|
||||
"symbol": "WETH",
|
||||
},
|
||||
],
|
||||
"descriptor": "100 WETH from ",
|
||||
"from": "0x50EC05ADe8280758E2077fcBC08D878D4aef79C3",
|
||||
"hash": "someHash",
|
||||
"logos": Array [
|
||||
"logoUrl",
|
||||
],
|
||||
"nonce": 12345,
|
||||
"otherAccount": "0x50EC05ADe8280758E2077fcBC08D878D4aef79C3",
|
||||
"status": "CONFIRMED",
|
||||
"timestamp": 10000,
|
||||
"title": "Received",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`parseRemote parseRemoteActivities should parse remove liquidity 1`] = `
|
||||
Object {
|
||||
"chainId": 1,
|
||||
"currencies": Array [
|
||||
Token {
|
||||
"address": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
|
||||
"chainId": 1,
|
||||
"decimals": 18,
|
||||
"isNative": false,
|
||||
"isToken": true,
|
||||
"name": "Wrapped Ether",
|
||||
"symbol": "WETH",
|
||||
},
|
||||
Token {
|
||||
"address": "0x6B175474E89094C44Da98b954EedeAC495271d0F",
|
||||
"chainId": 1,
|
||||
"decimals": 18,
|
||||
"isNative": false,
|
||||
"isToken": true,
|
||||
"name": "DAI",
|
||||
"symbol": "DAI",
|
||||
},
|
||||
],
|
||||
"descriptor": "100 WETH and 100 DAI",
|
||||
"from": "0x50EC05ADe8280758E2077fcBC08D878D4aef79C3",
|
||||
"hash": "someHash",
|
||||
"logos": Array [
|
||||
"logoUrl",
|
||||
"logoUrl",
|
||||
],
|
||||
"nonce": 12345,
|
||||
"status": "CONFIRMED",
|
||||
"timestamp": 10000,
|
||||
"title": "Removed Liquidity",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`parseRemote parseRemoteActivities should parse send 1`] = `
|
||||
Object {
|
||||
"chainId": 1,
|
||||
"currencies": Array [
|
||||
Token {
|
||||
"address": "0x6B175474E89094C44Da98b954EedeAC495271d0F",
|
||||
"chainId": 1,
|
||||
"decimals": 18,
|
||||
"isNative": false,
|
||||
"isToken": true,
|
||||
"name": "DAI",
|
||||
"symbol": "DAI",
|
||||
},
|
||||
],
|
||||
"descriptor": "100 DAI to ",
|
||||
"from": "0x50EC05ADe8280758E2077fcBC08D878D4aef79C3",
|
||||
"hash": "someHash",
|
||||
"logos": Array [
|
||||
"logoUrl",
|
||||
],
|
||||
"nonce": 12345,
|
||||
"otherAccount": "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045",
|
||||
"status": "CONFIRMED",
|
||||
"timestamp": 10000,
|
||||
"title": "Sent",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`parseRemote parseRemoteActivities should parse swap 1`] = `
|
||||
Object {
|
||||
"chainId": 1,
|
||||
"currencies": Array [
|
||||
Token {
|
||||
"address": "0x6B175474E89094C44Da98b954EedeAC495271d0F",
|
||||
"chainId": 1,
|
||||
"decimals": 18,
|
||||
"isNative": false,
|
||||
"isToken": true,
|
||||
"name": "DAI",
|
||||
"symbol": "DAI",
|
||||
},
|
||||
Token {
|
||||
"address": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
|
||||
"chainId": 1,
|
||||
"decimals": 18,
|
||||
"isNative": false,
|
||||
"isToken": true,
|
||||
"name": "Wrapped Ether",
|
||||
"symbol": "WETH",
|
||||
},
|
||||
],
|
||||
"descriptor": "100 DAI for 100 WETH",
|
||||
"from": "0x50EC05ADe8280758E2077fcBC08D878D4aef79C3",
|
||||
"hash": "someHash",
|
||||
"logos": Array [
|
||||
"logoUrl",
|
||||
],
|
||||
"nonce": 12345,
|
||||
"status": "CONFIRMED",
|
||||
"timestamp": 10000,
|
||||
"title": "Swapped",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`parseRemote parseRemoteActivities should parse swap order 1`] = `
|
||||
Object {
|
||||
"chainId": 1,
|
||||
"currencies": Array [
|
||||
Token {
|
||||
"address": "0x6B175474E89094C44Da98b954EedeAC495271d0F",
|
||||
"chainId": 1,
|
||||
"decimals": 18,
|
||||
"isNative": false,
|
||||
"isToken": true,
|
||||
"name": "DAI",
|
||||
"symbol": "DAI",
|
||||
},
|
||||
Token {
|
||||
"address": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
|
||||
"chainId": 1,
|
||||
"decimals": 18,
|
||||
"isNative": false,
|
||||
"isToken": true,
|
||||
"name": "Wrapped Ether",
|
||||
"symbol": "WETH",
|
||||
},
|
||||
],
|
||||
"descriptor": "100 DAI for 100 WETH",
|
||||
"from": "0x50EC05ADe8280758E2077fcBC08D878D4aef79C3",
|
||||
"hash": "someHash",
|
||||
"logos": Array [
|
||||
"logoUrl",
|
||||
],
|
||||
"nonce": 12345,
|
||||
"prefixIconSrc": "bolt.svg",
|
||||
"status": "CONFIRMED",
|
||||
"timestamp": 10000,
|
||||
"title": "Swapped",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`parseRemote parseRemoteActivities should parse token approval 1`] = `
|
||||
Object {
|
||||
"chainId": 1,
|
||||
"currencies": Array [
|
||||
Token {
|
||||
"address": "0x6B175474E89094C44Da98b954EedeAC495271d0F",
|
||||
"chainId": 1,
|
||||
"decimals": 18,
|
||||
"isNative": false,
|
||||
"isToken": true,
|
||||
"name": "DAI",
|
||||
"symbol": "DAI",
|
||||
},
|
||||
],
|
||||
"descriptor": "DAI",
|
||||
"from": "0x50EC05ADe8280758E2077fcBC08D878D4aef79C3",
|
||||
"hash": "someHash",
|
||||
"logos": Array [
|
||||
"logoUrl",
|
||||
],
|
||||
"nonce": 12345,
|
||||
"status": "CONFIRMED",
|
||||
"timestamp": 10000,
|
||||
"title": "Approved",
|
||||
}
|
||||
`;
|
||||
@@ -0,0 +1,522 @@
|
||||
import { ChainId, NONFUNGIBLE_POSITION_MANAGER_ADDRESSES, WETH9 } from '@uniswap/sdk-core'
|
||||
import { DAI } from 'constants/tokens'
|
||||
import {
|
||||
AssetActivityPartsFragment,
|
||||
Chain,
|
||||
Currency,
|
||||
NftStandard,
|
||||
SwapOrderStatus,
|
||||
TokenStandard,
|
||||
TransactionDirection,
|
||||
TransactionStatus,
|
||||
TransactionType,
|
||||
} from 'graphql/data/__generated__/types-and-hooks'
|
||||
|
||||
import { MOONPAY_SENDER_ADDRESSES } from '../../constants'
|
||||
|
||||
const MockOrderTimestamp = 10000
|
||||
const MockRecipientAddress = '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045'
|
||||
const MockSenderAddress = '0x50EC05ADe8280758E2077fcBC08D878D4aef79C3'
|
||||
|
||||
const mockAssetActivityPartsFragment = {
|
||||
__typename: 'AssetActivity',
|
||||
id: 'activityId',
|
||||
timestamp: MockOrderTimestamp,
|
||||
chain: Chain.Ethereum,
|
||||
details: {
|
||||
__typename: 'SwapOrderDetails',
|
||||
id: 'detailsId',
|
||||
offerer: 'offererId',
|
||||
hash: 'someHash',
|
||||
inputTokenQuantity: '100',
|
||||
outputTokenQuantity: '200',
|
||||
orderStatus: SwapOrderStatus.Open,
|
||||
inputToken: {
|
||||
__typename: 'Token',
|
||||
id: 'tokenId',
|
||||
chain: Chain.Ethereum,
|
||||
standard: TokenStandard.Erc20,
|
||||
},
|
||||
outputToken: {
|
||||
__typename: 'Token',
|
||||
id: 'tokenId',
|
||||
chain: Chain.Ethereum,
|
||||
standard: TokenStandard.Erc20,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
const mockSwapOrderDetailsPartsFragment = {
|
||||
__typename: 'SwapOrderDetails',
|
||||
id: 'someId',
|
||||
offerer: 'someOfferer',
|
||||
hash: 'someHash',
|
||||
inputTokenQuantity: '100',
|
||||
outputTokenQuantity: '200',
|
||||
orderStatus: SwapOrderStatus.Open,
|
||||
inputToken: {
|
||||
__typename: 'Token',
|
||||
id: DAI.address,
|
||||
name: 'DAI',
|
||||
symbol: DAI.symbol,
|
||||
address: DAI.address,
|
||||
decimals: 18,
|
||||
chain: Chain.Ethereum,
|
||||
standard: TokenStandard.Erc20,
|
||||
project: {
|
||||
__typename: 'TokenProject',
|
||||
id: 'projectId',
|
||||
isSpam: false,
|
||||
logo: {
|
||||
__typename: 'Image',
|
||||
id: 'imageId',
|
||||
url: 'someUrl',
|
||||
},
|
||||
},
|
||||
},
|
||||
outputToken: {
|
||||
__typename: 'Token',
|
||||
id: WETH9[1].address,
|
||||
name: 'Wrapped Ether',
|
||||
symbol: 'WETH',
|
||||
address: WETH9[1].address,
|
||||
decimals: 18,
|
||||
chain: Chain.Ethereum,
|
||||
standard: TokenStandard.Erc20,
|
||||
project: {
|
||||
__typename: 'TokenProject',
|
||||
id: 'projectId',
|
||||
isSpam: false,
|
||||
logo: {
|
||||
__typename: 'Image',
|
||||
id: 'imageId',
|
||||
url: 'someUrl',
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
const mockNftApprovalPartsFragment = {
|
||||
__typename: 'NftApproval',
|
||||
id: 'approvalId',
|
||||
nftStandard: NftStandard.Erc721, // Replace with actual enum value
|
||||
approvedAddress: '0xApprovedAddress',
|
||||
asset: {
|
||||
__typename: 'NftAsset',
|
||||
id: 'assetId',
|
||||
name: 'SomeNftName',
|
||||
tokenId: 'tokenId123',
|
||||
nftContract: {
|
||||
__typename: 'NftContract',
|
||||
id: 'nftContractId',
|
||||
chain: Chain.Ethereum, // Replace with actual enum value
|
||||
address: '0xContractAddress',
|
||||
},
|
||||
image: {
|
||||
__typename: 'Image',
|
||||
id: 'imageId',
|
||||
url: 'imageUrl',
|
||||
},
|
||||
collection: {
|
||||
__typename: 'NftCollection',
|
||||
id: 'collectionId',
|
||||
name: 'SomeCollectionName',
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
const mockNftApproveForAllPartsFragment = {
|
||||
__typename: 'NftApproveForAll',
|
||||
id: 'approveForAllId',
|
||||
nftStandard: NftStandard.Erc721, // Replace with actual enum value
|
||||
operatorAddress: '0xOperatorAddress',
|
||||
approved: true,
|
||||
asset: {
|
||||
__typename: 'NftAsset',
|
||||
id: 'assetId',
|
||||
name: 'SomeNftName',
|
||||
tokenId: 'tokenId123',
|
||||
nftContract: {
|
||||
__typename: 'NftContract',
|
||||
id: 'nftContractId',
|
||||
chain: Chain.Ethereum, // Replace with actual enum value
|
||||
address: '0xContractAddress',
|
||||
},
|
||||
image: {
|
||||
__typename: 'Image',
|
||||
id: 'imageId',
|
||||
url: 'imageUrl',
|
||||
},
|
||||
collection: {
|
||||
__typename: 'NftCollection',
|
||||
id: 'collectionId',
|
||||
name: 'SomeCollectionName',
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
const mockNftTransferPartsFragment = {
|
||||
__typename: 'NftTransfer',
|
||||
id: 'transferId',
|
||||
nftStandard: NftStandard.Erc721,
|
||||
sender: MockSenderAddress,
|
||||
recipient: MockRecipientAddress,
|
||||
direction: TransactionDirection.Out,
|
||||
asset: {
|
||||
__typename: 'NftAsset',
|
||||
id: 'assetId',
|
||||
name: 'SomeNftName',
|
||||
tokenId: 'tokenId123',
|
||||
nftContract: {
|
||||
__typename: 'NftContract',
|
||||
id: 'nftContractId',
|
||||
chain: Chain.Ethereum,
|
||||
address: '0xContractAddress',
|
||||
},
|
||||
image: {
|
||||
__typename: 'Image',
|
||||
id: 'imageId',
|
||||
url: 'imageUrl',
|
||||
},
|
||||
collection: {
|
||||
__typename: 'NftCollection',
|
||||
id: 'collectionId',
|
||||
name: 'SomeCollectionName',
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
const mockTokenTransferOutPartsFragment = {
|
||||
__typename: 'TokenTransfer',
|
||||
id: 'tokenTransferId',
|
||||
tokenStandard: TokenStandard.Erc20,
|
||||
quantity: '100',
|
||||
sender: MockSenderAddress,
|
||||
recipient: MockRecipientAddress,
|
||||
direction: TransactionDirection.Out,
|
||||
asset: {
|
||||
__typename: 'Token',
|
||||
id: DAI.address,
|
||||
name: 'DAI',
|
||||
symbol: 'DAI',
|
||||
address: DAI.address,
|
||||
decimals: 18,
|
||||
chain: Chain.Ethereum,
|
||||
standard: TokenStandard.Erc20,
|
||||
project: {
|
||||
__typename: 'TokenProject',
|
||||
id: 'projectId',
|
||||
isSpam: false,
|
||||
logo: {
|
||||
__typename: 'Image',
|
||||
id: 'logoId',
|
||||
url: 'logoUrl',
|
||||
},
|
||||
},
|
||||
},
|
||||
transactedValue: {
|
||||
__typename: 'Amount',
|
||||
id: 'amountId',
|
||||
currency: Currency.Usd,
|
||||
value: 100,
|
||||
},
|
||||
}
|
||||
|
||||
const mockNativeTokenTransferOutPartsFragment = {
|
||||
__typename: 'TokenTransfer',
|
||||
id: 'tokenTransferId',
|
||||
asset: {
|
||||
__typename: 'Token',
|
||||
id: 'ETH',
|
||||
name: 'Ether',
|
||||
symbol: 'ETH',
|
||||
address: null,
|
||||
decimals: 18,
|
||||
chain: 'ETHEREUM',
|
||||
standard: null,
|
||||
project: {
|
||||
__typename: 'TokenProject',
|
||||
id: 'Ethereum',
|
||||
isSpam: false,
|
||||
logo: {
|
||||
__typename: 'Image',
|
||||
id: 'ETH_logo',
|
||||
url: 'https://token-icons.s3.amazonaws.com/eth.png',
|
||||
},
|
||||
},
|
||||
},
|
||||
tokenStandard: 'NATIVE',
|
||||
quantity: '0.25',
|
||||
sender: MockSenderAddress,
|
||||
recipient: MockRecipientAddress,
|
||||
direction: 'OUT',
|
||||
transactedValue: {
|
||||
__typename: 'Amount',
|
||||
id: 'ETH_amount',
|
||||
currency: 'USD',
|
||||
value: 399.0225,
|
||||
},
|
||||
}
|
||||
|
||||
const mockWrappedEthTransferInPartsFragment = {
|
||||
__typename: 'TokenTransfer',
|
||||
id: 'tokenTransferId',
|
||||
asset: {
|
||||
__typename: 'Token',
|
||||
id: 'WETH',
|
||||
name: 'Wrapped Ether',
|
||||
symbol: 'WETH',
|
||||
address: '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2',
|
||||
decimals: 18,
|
||||
chain: 'ETHEREUM',
|
||||
standard: 'ERC20',
|
||||
project: {
|
||||
__typename: 'TokenProject',
|
||||
id: 'weth_project_id',
|
||||
isSpam: false,
|
||||
logo: {
|
||||
__typename: 'Image',
|
||||
id: 'weth_image',
|
||||
url: 'https://raw.githubusercontent.com/Uniswap/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png',
|
||||
},
|
||||
},
|
||||
},
|
||||
tokenStandard: 'ERC20',
|
||||
quantity: '0.25',
|
||||
sender: MockSenderAddress,
|
||||
recipient: MockRecipientAddress,
|
||||
direction: 'IN',
|
||||
transactedValue: {
|
||||
__typename: 'Amount',
|
||||
id: 'mockWethAmountId',
|
||||
currency: 'USD',
|
||||
value: 399.1334007875,
|
||||
},
|
||||
}
|
||||
|
||||
const mockTokenTransferInPartsFragment = {
|
||||
__typename: 'TokenTransfer',
|
||||
id: 'tokenTransferId',
|
||||
tokenStandard: TokenStandard.Erc20,
|
||||
quantity: '1',
|
||||
sender: MockSenderAddress,
|
||||
recipient: MockRecipientAddress,
|
||||
direction: TransactionDirection.In,
|
||||
asset: {
|
||||
__typename: 'Token',
|
||||
id: WETH9[1].address,
|
||||
name: 'Wrapped Ether',
|
||||
symbol: 'WETH',
|
||||
address: WETH9[1].address,
|
||||
decimals: 18,
|
||||
chain: Chain.Ethereum,
|
||||
standard: TokenStandard.Erc20,
|
||||
project: {
|
||||
__typename: 'TokenProject',
|
||||
id: 'projectId',
|
||||
isSpam: false,
|
||||
logo: {
|
||||
__typename: 'Image',
|
||||
id: 'logoId',
|
||||
url: 'logoUrl',
|
||||
},
|
||||
},
|
||||
},
|
||||
transactedValue: {
|
||||
__typename: 'Amount',
|
||||
id: 'amountId',
|
||||
currency: Currency.Usd,
|
||||
value: 100,
|
||||
},
|
||||
}
|
||||
|
||||
const mockTokenApprovalPartsFragment = {
|
||||
__typename: 'TokenApproval',
|
||||
id: 'tokenApprovalId',
|
||||
tokenStandard: TokenStandard.Erc20,
|
||||
approvedAddress: DAI.address,
|
||||
quantity: '50',
|
||||
asset: {
|
||||
__typename: 'Token',
|
||||
id: 'tokenId',
|
||||
name: 'DAI',
|
||||
symbol: 'DAI',
|
||||
address: DAI.address,
|
||||
decimals: 18,
|
||||
chain: Chain.Ethereum,
|
||||
standard: TokenStandard.Erc20,
|
||||
project: {
|
||||
__typename: 'TokenProject',
|
||||
id: 'projectId',
|
||||
isSpam: false,
|
||||
logo: {
|
||||
__typename: 'Image',
|
||||
id: 'logoId',
|
||||
url: 'logoUrl',
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export const MockOpenUniswapXOrder = {
|
||||
...mockAssetActivityPartsFragment,
|
||||
details: mockSwapOrderDetailsPartsFragment,
|
||||
} as AssetActivityPartsFragment
|
||||
|
||||
export const MockClosedUniswapXOrder = {
|
||||
...mockAssetActivityPartsFragment,
|
||||
details: {
|
||||
...mockSwapOrderDetailsPartsFragment,
|
||||
orderStatus: SwapOrderStatus.Expired,
|
||||
},
|
||||
} as AssetActivityPartsFragment
|
||||
|
||||
const commonTransactionDetailsFields = {
|
||||
__typename: 'TransactionDetails',
|
||||
from: MockSenderAddress,
|
||||
hash: 'someHash',
|
||||
id: 'transactionId',
|
||||
nonce: 12345,
|
||||
status: TransactionStatus.Confirmed,
|
||||
to: MockRecipientAddress,
|
||||
}
|
||||
|
||||
export const MockNFTApproval = {
|
||||
...mockAssetActivityPartsFragment,
|
||||
details: {
|
||||
...commonTransactionDetailsFields,
|
||||
type: TransactionType.Approve,
|
||||
assetChanges: [mockNftApprovalPartsFragment],
|
||||
},
|
||||
} as AssetActivityPartsFragment
|
||||
|
||||
export const MockNFTApprovalForAll = {
|
||||
...mockAssetActivityPartsFragment,
|
||||
details: {
|
||||
...commonTransactionDetailsFields,
|
||||
type: TransactionType.Approve,
|
||||
assetChanges: [mockNftApproveForAllPartsFragment],
|
||||
},
|
||||
} as AssetActivityPartsFragment
|
||||
|
||||
export const MockNFTTransfer = {
|
||||
...mockAssetActivityPartsFragment,
|
||||
details: {
|
||||
...commonTransactionDetailsFields,
|
||||
type: TransactionType.Mint,
|
||||
assetChanges: [mockNftTransferPartsFragment],
|
||||
},
|
||||
} as AssetActivityPartsFragment
|
||||
|
||||
export const MockTokenTransfer = {
|
||||
...mockAssetActivityPartsFragment,
|
||||
details: {
|
||||
...commonTransactionDetailsFields,
|
||||
type: TransactionType.Swap,
|
||||
assetChanges: [mockTokenTransferOutPartsFragment, mockTokenTransferInPartsFragment],
|
||||
},
|
||||
} as AssetActivityPartsFragment
|
||||
|
||||
export const MockSwapOrder = {
|
||||
...mockAssetActivityPartsFragment,
|
||||
details: {
|
||||
...commonTransactionDetailsFields,
|
||||
type: TransactionType.SwapOrder,
|
||||
assetChanges: [mockTokenTransferOutPartsFragment, mockTokenTransferInPartsFragment],
|
||||
},
|
||||
} as AssetActivityPartsFragment
|
||||
|
||||
export const MockTokenApproval = {
|
||||
...mockAssetActivityPartsFragment,
|
||||
details: {
|
||||
...commonTransactionDetailsFields,
|
||||
type: TransactionType.Approve,
|
||||
assetChanges: [mockTokenApprovalPartsFragment],
|
||||
},
|
||||
} as AssetActivityPartsFragment
|
||||
|
||||
export const MockTokenSend = {
|
||||
...mockAssetActivityPartsFragment,
|
||||
details: {
|
||||
...commonTransactionDetailsFields,
|
||||
type: TransactionType.Send,
|
||||
assetChanges: [mockTokenTransferOutPartsFragment],
|
||||
},
|
||||
} as AssetActivityPartsFragment
|
||||
|
||||
export const MockTokenReceive = {
|
||||
...mockAssetActivityPartsFragment,
|
||||
details: {
|
||||
...commonTransactionDetailsFields,
|
||||
type: TransactionType.Receive,
|
||||
assetChanges: [mockTokenTransferInPartsFragment],
|
||||
},
|
||||
} as AssetActivityPartsFragment
|
||||
|
||||
export const MockRemoveLiquidity = {
|
||||
...mockAssetActivityPartsFragment,
|
||||
details: {
|
||||
...commonTransactionDetailsFields,
|
||||
to: NONFUNGIBLE_POSITION_MANAGER_ADDRESSES[ChainId.MAINNET],
|
||||
type: TransactionType.Receive,
|
||||
assetChanges: [
|
||||
mockTokenTransferInPartsFragment,
|
||||
{
|
||||
...mockTokenTransferOutPartsFragment,
|
||||
direction: TransactionDirection.In,
|
||||
},
|
||||
],
|
||||
},
|
||||
} as AssetActivityPartsFragment
|
||||
|
||||
export const MockMoonpayPurchase = {
|
||||
...mockAssetActivityPartsFragment,
|
||||
details: {
|
||||
...commonTransactionDetailsFields,
|
||||
type: TransactionType.Receive,
|
||||
assetChanges: [
|
||||
{
|
||||
...mockTokenTransferInPartsFragment,
|
||||
sender: MOONPAY_SENDER_ADDRESSES[0],
|
||||
},
|
||||
],
|
||||
},
|
||||
} as AssetActivityPartsFragment
|
||||
|
||||
export const MockNFTReceive = {
|
||||
...mockAssetActivityPartsFragment,
|
||||
details: {
|
||||
...commonTransactionDetailsFields,
|
||||
type: TransactionType.Receive,
|
||||
assetChanges: [
|
||||
{
|
||||
...mockNftTransferPartsFragment,
|
||||
direction: TransactionDirection.In,
|
||||
},
|
||||
],
|
||||
},
|
||||
} as AssetActivityPartsFragment
|
||||
|
||||
export const MockNFTPurchase = {
|
||||
...mockAssetActivityPartsFragment,
|
||||
details: {
|
||||
...commonTransactionDetailsFields,
|
||||
type: TransactionType.Swap,
|
||||
assetChanges: [
|
||||
mockTokenTransferOutPartsFragment,
|
||||
{
|
||||
...mockNftTransferPartsFragment,
|
||||
direction: TransactionDirection.In,
|
||||
},
|
||||
],
|
||||
},
|
||||
} as AssetActivityPartsFragment
|
||||
|
||||
export const MockWrap = {
|
||||
...mockAssetActivityPartsFragment,
|
||||
details: {
|
||||
...commonTransactionDetailsFields,
|
||||
type: TransactionType.Lend,
|
||||
assetChanges: [mockNativeTokenTransferOutPartsFragment, mockWrappedEthTransferInPartsFragment],
|
||||
},
|
||||
} as AssetActivityPartsFragment
|
||||
@@ -2,6 +2,7 @@ import { TransactionStatus, useActivityQuery } from 'graphql/data/__generated__/
|
||||
import { useEffect, useMemo } from 'react'
|
||||
import { usePendingOrders } from 'state/signatures/hooks'
|
||||
import { usePendingTransactions, useTransactionCanceller } from 'state/transactions/hooks'
|
||||
import { useFormatter } from 'utils/formatNumbers'
|
||||
|
||||
import { useLocalActivities } from './parseLocal'
|
||||
import { parseRemoteActivities } from './parseRemote'
|
||||
@@ -55,6 +56,7 @@ function combineActivities(localMap: ActivityMap = {}, remoteMap: ActivityMap =
|
||||
}
|
||||
|
||||
export function useAllActivities(account: string) {
|
||||
const { formatNumberOrString } = useFormatter()
|
||||
const { data, loading, refetch } = useActivityQuery({
|
||||
variables: { account },
|
||||
errorPolicy: 'all',
|
||||
@@ -62,7 +64,10 @@ export function useAllActivities(account: string) {
|
||||
})
|
||||
|
||||
const localMap = useLocalActivities(account)
|
||||
const remoteMap = useMemo(() => parseRemoteActivities(data?.portfolios?.[0].assetActivities), [data?.portfolios])
|
||||
const remoteMap = useMemo(
|
||||
() => parseRemoteActivities(formatNumberOrString, data?.portfolios?.[0].assetActivities),
|
||||
[data?.portfolios, formatNumberOrString]
|
||||
)
|
||||
const updateCancelledTx = useTransactionCanceller()
|
||||
|
||||
/* Updates locally stored pendings tx's when remote data contains a conflicting cancellation tx */
|
||||
|
||||
@@ -1,80 +1,17 @@
|
||||
import { t } from '@lingui/macro'
|
||||
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 { PollingInterval } from 'graphql/data/util'
|
||||
import { atom, useAtom } from 'jotai'
|
||||
import { EmptyWalletModule } from 'nft/components/profile/view/EmptyWalletContent'
|
||||
import { useEffect, useMemo } from 'react'
|
||||
import styled from 'styled-components'
|
||||
import { ThemedText } from 'theme'
|
||||
import { ThemedText } from 'theme/components'
|
||||
|
||||
import { PortfolioSkeleton, PortfolioTabWrapper } from '../PortfolioRow'
|
||||
import { ActivityRow } from './ActivityRow'
|
||||
import { useAllActivities } from './hooks'
|
||||
import { Activity } from './types'
|
||||
|
||||
interface ActivityGroup {
|
||||
title: string
|
||||
transactions: Array<Activity>
|
||||
}
|
||||
|
||||
const sortActivities = (a: Activity, b: Activity) => b.timestamp - a.timestamp
|
||||
|
||||
const createGroups = (activities?: Array<Activity>) => {
|
||||
if (!activities) return undefined
|
||||
const now = Date.now()
|
||||
|
||||
const pending: Array<Activity> = []
|
||||
const today: Array<Activity> = []
|
||||
const currentWeek: Array<Activity> = []
|
||||
const last30Days: Array<Activity> = []
|
||||
const currentYear: Array<Activity> = []
|
||||
const yearMap: { [key: string]: Array<Activity> } = {}
|
||||
|
||||
// TODO(cartcrom): create different time bucket system for activities to fall in based on design wants
|
||||
activities.forEach((activity) => {
|
||||
if (activity.status === TransactionStatus.Pending) {
|
||||
pending.push(activity)
|
||||
return
|
||||
}
|
||||
const addedTime = activity.timestamp * 1000
|
||||
|
||||
if (isSameDay(now, addedTime)) {
|
||||
today.push(activity)
|
||||
} else if (isSameWeek(addedTime, now)) {
|
||||
currentWeek.push(activity)
|
||||
} else if (isSameMonth(addedTime, now)) {
|
||||
last30Days.push(activity)
|
||||
} else if (isSameYear(addedTime, now)) {
|
||||
currentYear.push(activity)
|
||||
} else {
|
||||
const year = getYear(addedTime)
|
||||
|
||||
if (!yearMap[year]) {
|
||||
yearMap[year] = [activity]
|
||||
} else {
|
||||
yearMap[year].push(activity)
|
||||
}
|
||||
}
|
||||
})
|
||||
const sortedYears = Object.keys(yearMap)
|
||||
.sort((a, b) => parseInt(b) - parseInt(a))
|
||||
.map((year) => ({ title: year, transactions: yearMap[year] }))
|
||||
|
||||
const transactionGroups: Array<ActivityGroup> = [
|
||||
{ title: t`Pending`, transactions: pending.sort(sortActivities) },
|
||||
{ title: t`Today`, transactions: today.sort(sortActivities) },
|
||||
{ title: t`This week`, transactions: currentWeek.sort(sortActivities) },
|
||||
{ title: t`This month`, transactions: last30Days.sort(sortActivities) },
|
||||
{ title: t`This year`, transactions: currentYear.sort(sortActivities) },
|
||||
...sortedYears,
|
||||
]
|
||||
|
||||
return transactionGroups.filter((transactionInformation) => transactionInformation.transactions.length > 0)
|
||||
}
|
||||
import { createGroups } from './utils'
|
||||
|
||||
const ActivityGroupWrapper = styled(Column)`
|
||||
margin-top: 16px;
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
TransactionType as MockTxType,
|
||||
} from 'state/transactions/types'
|
||||
import { renderHook } from 'test-utils/render'
|
||||
import { useFormatter } from 'utils/formatNumbers'
|
||||
|
||||
import { UniswapXOrderStatus } from '../../../../lib/hooks/orders/types'
|
||||
import { SignatureDetails, SignatureType } from '../../../../state/signatures/types'
|
||||
@@ -237,6 +238,8 @@ jest.mock('../../../../state/transactions/hooks', () => {
|
||||
|
||||
describe('parseLocalActivity', () => {
|
||||
it('returns swap activity fields with known tokens, exact input', () => {
|
||||
const { formatNumber } = renderHook(() => useFormatter()).result.current
|
||||
|
||||
const details = {
|
||||
info: mockSwapInfo(
|
||||
MockTradeType.EXACT_INPUT,
|
||||
@@ -251,7 +254,7 @@ describe('parseLocalActivity', () => {
|
||||
},
|
||||
} as TransactionDetails
|
||||
const chainId = ChainId.MAINNET
|
||||
expect(transactionToActivity(details, chainId, mockTokenAddressMap)).toEqual({
|
||||
expect(transactionToActivity(details, chainId, mockTokenAddressMap, formatNumber)).toEqual({
|
||||
chainId: 1,
|
||||
currencies: [MockUSDC_MAINNET, MockDAI],
|
||||
descriptor: '1.00 USDC for 1.00 DAI',
|
||||
@@ -264,6 +267,8 @@ describe('parseLocalActivity', () => {
|
||||
})
|
||||
|
||||
it('returns swap activity fields with known tokens, exact output', () => {
|
||||
const { formatNumber } = renderHook(() => useFormatter()).result.current
|
||||
|
||||
const details = {
|
||||
info: mockSwapInfo(
|
||||
MockTradeType.EXACT_OUTPUT,
|
||||
@@ -278,7 +283,7 @@ describe('parseLocalActivity', () => {
|
||||
},
|
||||
} as TransactionDetails
|
||||
const chainId = ChainId.MAINNET
|
||||
expect(transactionToActivity(details, chainId, mockTokenAddressMap)).toMatchObject({
|
||||
expect(transactionToActivity(details, chainId, mockTokenAddressMap, formatNumber)).toMatchObject({
|
||||
chainId: 1,
|
||||
currencies: [MockUSDC_MAINNET, MockDAI],
|
||||
descriptor: '1.00 USDC for 1.00 DAI',
|
||||
@@ -288,6 +293,8 @@ describe('parseLocalActivity', () => {
|
||||
})
|
||||
|
||||
it('returns swap activity fields with unknown tokens', () => {
|
||||
const { formatNumber } = renderHook(() => useFormatter()).result.current
|
||||
|
||||
const details = {
|
||||
info: mockSwapInfo(
|
||||
MockTradeType.EXACT_INPUT,
|
||||
@@ -303,7 +310,7 @@ describe('parseLocalActivity', () => {
|
||||
} as TransactionDetails
|
||||
const chainId = ChainId.MAINNET
|
||||
const tokens = {} as ChainTokenMap
|
||||
expect(transactionToActivity(details, chainId, tokens)).toMatchObject({
|
||||
expect(transactionToActivity(details, chainId, tokens, formatNumber)).toMatchObject({
|
||||
chainId: 1,
|
||||
currencies: [undefined, undefined],
|
||||
descriptor: 'Unknown for Unknown',
|
||||
@@ -496,13 +503,16 @@ describe('parseLocalActivity', () => {
|
||||
})
|
||||
|
||||
it('Signature to activity - returns undefined if is on chain order', () => {
|
||||
const { formatNumber } = renderHook(() => useFormatter()).result.current
|
||||
|
||||
expect(
|
||||
signatureToActivity(
|
||||
{
|
||||
type: SignatureType.SIGN_UNISWAPX_ORDER,
|
||||
status: UniswapXOrderStatus.FILLED,
|
||||
} as SignatureDetails,
|
||||
{}
|
||||
{},
|
||||
formatNumber
|
||||
)
|
||||
).toBeUndefined()
|
||||
|
||||
@@ -512,7 +522,8 @@ describe('parseLocalActivity', () => {
|
||||
type: SignatureType.SIGN_UNISWAPX_ORDER,
|
||||
status: UniswapXOrderStatus.CANCELLED,
|
||||
} as SignatureDetails,
|
||||
{}
|
||||
{},
|
||||
formatNumber
|
||||
)
|
||||
).toBeUndefined()
|
||||
})
|
||||
|
||||
@@ -2,7 +2,6 @@ import { BigNumber } from '@ethersproject/bignumber'
|
||||
import { t } from '@lingui/macro'
|
||||
import { ChainId, Currency, CurrencyAmount, TradeType } from '@uniswap/sdk-core'
|
||||
import UniswapXBolt from 'assets/svg/bolt.svg'
|
||||
import { SupportedLocale } from 'constants/locales'
|
||||
import { nativeOnChain } from 'constants/tokens'
|
||||
import { TransactionStatus } from 'graphql/data/__generated__/types-and-hooks'
|
||||
import { ChainTokenMap, useAllTokensMultichain } from 'hooks/Tokens'
|
||||
@@ -24,11 +23,13 @@ import {
|
||||
TransactionType,
|
||||
WrapTransactionInfo,
|
||||
} from 'state/transactions/types'
|
||||
import { formatCurrencyAmount, useFormatterLocales } from 'utils/formatNumbers'
|
||||
import { NumberType, useFormatter } from 'utils/formatNumbers'
|
||||
|
||||
import { CancelledTransactionTitleTable, getActivityTitle, OrderTextTable } from '../constants'
|
||||
import { Activity, ActivityMap } from './types'
|
||||
|
||||
type FormatNumberFunctionType = ReturnType<typeof useFormatter>['formatNumber']
|
||||
|
||||
function getCurrency(currencyId: string, chainId: ChainId, tokens: ChainTokenMap): Currency | undefined {
|
||||
return currencyId === 'ETH' ? nativeOnChain(chainId) : tokens[chainId]?.[currencyId]
|
||||
}
|
||||
@@ -38,15 +39,21 @@ function buildCurrencyDescriptor(
|
||||
amtA: string,
|
||||
currencyB: Currency | undefined,
|
||||
amtB: string,
|
||||
delimiter = t`for`,
|
||||
locale?: SupportedLocale
|
||||
formatNumber: FormatNumberFunctionType,
|
||||
delimiter = t`for`
|
||||
) {
|
||||
const formattedA = currencyA
|
||||
? formatCurrencyAmount({ amount: CurrencyAmount.fromRawAmount(currencyA, amtA), locale })
|
||||
? formatNumber({
|
||||
input: parseFloat(CurrencyAmount.fromRawAmount(currencyA, amtA).toSignificant()),
|
||||
type: NumberType.TokenNonTx,
|
||||
})
|
||||
: t`Unknown`
|
||||
const symbolA = currencyA?.symbol ?? ''
|
||||
const formattedB = currencyB
|
||||
? formatCurrencyAmount({ amount: CurrencyAmount.fromRawAmount(currencyB, amtB), locale })
|
||||
? formatNumber({
|
||||
input: parseFloat(CurrencyAmount.fromRawAmount(currencyB, amtB).toSignificant()),
|
||||
type: NumberType.TokenNonTx,
|
||||
})
|
||||
: t`Unknown`
|
||||
const symbolB = currencyB?.symbol ?? ''
|
||||
return [formattedA, symbolA, delimiter, formattedB, symbolB].filter(Boolean).join(' ')
|
||||
@@ -56,7 +63,7 @@ function parseSwap(
|
||||
swap: ExactInputSwapTransactionInfo | ExactOutputSwapTransactionInfo,
|
||||
chainId: ChainId,
|
||||
tokens: ChainTokenMap,
|
||||
locale?: SupportedLocale
|
||||
formatNumber: FormatNumberFunctionType
|
||||
): Partial<Activity> {
|
||||
const tokenIn = getCurrency(swap.inputCurrencyId, chainId, tokens)
|
||||
const tokenOut = getCurrency(swap.outputCurrencyId, chainId, tokens)
|
||||
@@ -66,18 +73,29 @@ function parseSwap(
|
||||
: [swap.expectedInputCurrencyAmountRaw, swap.outputCurrencyAmountRaw]
|
||||
|
||||
return {
|
||||
descriptor: buildCurrencyDescriptor(tokenIn, inputRaw, tokenOut, outputRaw, undefined, locale),
|
||||
descriptor: buildCurrencyDescriptor(tokenIn, inputRaw, tokenOut, outputRaw, formatNumber, undefined),
|
||||
currencies: [tokenIn, tokenOut],
|
||||
prefixIconSrc: swap.isUniswapXOrder ? UniswapXBolt : undefined,
|
||||
}
|
||||
}
|
||||
|
||||
function parseWrap(wrap: WrapTransactionInfo, chainId: ChainId, status: TransactionStatus): Partial<Activity> {
|
||||
function parseWrap(
|
||||
wrap: WrapTransactionInfo,
|
||||
chainId: ChainId,
|
||||
status: TransactionStatus,
|
||||
formatNumber: FormatNumberFunctionType
|
||||
): Partial<Activity> {
|
||||
const native = nativeOnChain(chainId)
|
||||
const wrapped = native.wrapped
|
||||
const [input, output] = wrap.unwrapped ? [wrapped, native] : [native, wrapped]
|
||||
|
||||
const descriptor = buildCurrencyDescriptor(input, wrap.currencyAmountRaw, output, wrap.currencyAmountRaw)
|
||||
const descriptor = buildCurrencyDescriptor(
|
||||
input,
|
||||
wrap.currencyAmountRaw,
|
||||
output,
|
||||
wrap.currencyAmountRaw,
|
||||
formatNumber
|
||||
)
|
||||
const title = getActivityTitle(TransactionType.WRAP, status, wrap.unwrapped)
|
||||
const currencies = wrap.unwrapped ? [wrapped, native] : [native, wrapped]
|
||||
|
||||
@@ -107,11 +125,16 @@ type GenericLPInfo = Omit<
|
||||
AddLiquidityV3PoolTransactionInfo | RemoveLiquidityV3TransactionInfo | AddLiquidityV2PoolTransactionInfo,
|
||||
'type'
|
||||
>
|
||||
function parseLP(lp: GenericLPInfo, chainId: ChainId, tokens: ChainTokenMap): Partial<Activity> {
|
||||
function parseLP(
|
||||
lp: GenericLPInfo,
|
||||
chainId: ChainId,
|
||||
tokens: ChainTokenMap,
|
||||
formatNumber: FormatNumberFunctionType
|
||||
): Partial<Activity> {
|
||||
const baseCurrency = getCurrency(lp.baseCurrencyId, chainId, tokens)
|
||||
const quoteCurrency = getCurrency(lp.quoteCurrencyId, chainId, tokens)
|
||||
const [baseRaw, quoteRaw] = [lp.expectedAmountBaseRaw, lp.expectedAmountQuoteRaw]
|
||||
const descriptor = buildCurrencyDescriptor(baseCurrency, baseRaw, quoteCurrency, quoteRaw, t`and`)
|
||||
const descriptor = buildCurrencyDescriptor(baseCurrency, baseRaw, quoteCurrency, quoteRaw, formatNumber, t`and`)
|
||||
|
||||
return { descriptor, currencies: [baseCurrency, quoteCurrency] }
|
||||
}
|
||||
@@ -119,7 +142,8 @@ function parseLP(lp: GenericLPInfo, chainId: ChainId, tokens: ChainTokenMap): Pa
|
||||
function parseCollectFees(
|
||||
collect: CollectFeesTransactionInfo,
|
||||
chainId: ChainId,
|
||||
tokens: ChainTokenMap
|
||||
tokens: ChainTokenMap,
|
||||
formatNumber: FormatNumberFunctionType
|
||||
): Partial<Activity> {
|
||||
// Adapts CollectFeesTransactionInfo to generic LP type
|
||||
const {
|
||||
@@ -128,7 +152,12 @@ function parseCollectFees(
|
||||
expectedCurrencyOwed0: expectedAmountBaseRaw,
|
||||
expectedCurrencyOwed1: expectedAmountQuoteRaw,
|
||||
} = collect
|
||||
return parseLP({ baseCurrencyId, quoteCurrencyId, expectedAmountBaseRaw, expectedAmountQuoteRaw }, chainId, tokens)
|
||||
return parseLP(
|
||||
{ baseCurrencyId, quoteCurrencyId, expectedAmountBaseRaw, expectedAmountQuoteRaw },
|
||||
chainId,
|
||||
tokens,
|
||||
formatNumber
|
||||
)
|
||||
}
|
||||
|
||||
function parseMigrateCreateV3(
|
||||
@@ -157,7 +186,7 @@ export function transactionToActivity(
|
||||
details: TransactionDetails,
|
||||
chainId: ChainId,
|
||||
tokens: ChainTokenMap,
|
||||
locale?: SupportedLocale
|
||||
formatNumber: FormatNumberFunctionType
|
||||
): Activity | undefined {
|
||||
try {
|
||||
const status = getTransactionStatus(details)
|
||||
@@ -176,19 +205,19 @@ export function transactionToActivity(
|
||||
let additionalFields: Partial<Activity> = {}
|
||||
const info = details.info
|
||||
if (info.type === TransactionType.SWAP) {
|
||||
additionalFields = parseSwap(info, chainId, tokens, locale)
|
||||
additionalFields = parseSwap(info, chainId, tokens, formatNumber)
|
||||
} else if (info.type === TransactionType.APPROVAL) {
|
||||
additionalFields = parseApproval(info, chainId, tokens, status)
|
||||
} else if (info.type === TransactionType.WRAP) {
|
||||
additionalFields = parseWrap(info, chainId, status)
|
||||
additionalFields = parseWrap(info, chainId, status, formatNumber)
|
||||
} else if (
|
||||
info.type === TransactionType.ADD_LIQUIDITY_V3_POOL ||
|
||||
info.type === TransactionType.REMOVE_LIQUIDITY_V3 ||
|
||||
info.type === TransactionType.ADD_LIQUIDITY_V2_POOL
|
||||
) {
|
||||
additionalFields = parseLP(info, chainId, tokens)
|
||||
additionalFields = parseLP(info, chainId, tokens, formatNumber)
|
||||
} else if (info.type === TransactionType.COLLECT_FEES) {
|
||||
additionalFields = parseCollectFees(info, chainId, tokens)
|
||||
additionalFields = parseCollectFees(info, chainId, tokens, formatNumber)
|
||||
} else if (info.type === TransactionType.MIGRATE_LIQUIDITY_V3 || info.type === TransactionType.CREATE_V3_POOL) {
|
||||
additionalFields = parseMigrateCreateV3(info, chainId, tokens)
|
||||
}
|
||||
@@ -210,7 +239,7 @@ export function transactionToActivity(
|
||||
export function signatureToActivity(
|
||||
signature: SignatureDetails,
|
||||
tokens: ChainTokenMap,
|
||||
locale?: SupportedLocale
|
||||
formatNumber: FormatNumberFunctionType
|
||||
): Activity | undefined {
|
||||
switch (signature.type) {
|
||||
case SignatureType.SIGN_UNISWAPX_ORDER: {
|
||||
@@ -229,7 +258,7 @@ export function signatureToActivity(
|
||||
from: signature.offerer,
|
||||
statusMessage,
|
||||
prefixIconSrc: UniswapXBolt,
|
||||
...parseSwap(signature.swapInfo, signature.chainId, tokens, locale),
|
||||
...parseSwap(signature.swapInfo, signature.chainId, tokens, formatNumber),
|
||||
}
|
||||
}
|
||||
default:
|
||||
@@ -241,24 +270,24 @@ export function useLocalActivities(account: string): ActivityMap {
|
||||
const allTransactions = useMultichainTransactions()
|
||||
const allSignatures = useAllSignatures()
|
||||
const tokens = useAllTokensMultichain()
|
||||
const { formatterLocale } = useFormatterLocales()
|
||||
const { formatNumber } = useFormatter()
|
||||
|
||||
return useMemo(() => {
|
||||
const activityMap: ActivityMap = {}
|
||||
for (const [transaction, chainId] of allTransactions) {
|
||||
if (transaction.from !== account) continue
|
||||
|
||||
const activity = transactionToActivity(transaction, chainId, tokens, formatterLocale)
|
||||
const activity = transactionToActivity(transaction, chainId, tokens, formatNumber)
|
||||
if (activity) activityMap[transaction.hash] = activity
|
||||
}
|
||||
|
||||
for (const signature of Object.values(allSignatures)) {
|
||||
if (signature.offerer !== account) continue
|
||||
|
||||
const activity = signatureToActivity(signature, tokens, formatterLocale)
|
||||
const activity = signatureToActivity(signature, tokens, formatNumber)
|
||||
if (activity) activityMap[signature.id] = activity
|
||||
}
|
||||
|
||||
return activityMap
|
||||
}, [account, allSignatures, allTransactions, formatterLocale, tokens])
|
||||
}, [account, allSignatures, allTransactions, formatNumber, tokens])
|
||||
}
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
import { act, renderHook } from '@testing-library/react'
|
||||
import ms from 'ms'
|
||||
|
||||
import {
|
||||
MockClosedUniswapXOrder,
|
||||
MockMoonpayPurchase,
|
||||
MockNFTApproval,
|
||||
MockNFTApprovalForAll,
|
||||
MockNFTPurchase,
|
||||
MockNFTReceive,
|
||||
MockNFTTransfer,
|
||||
MockOpenUniswapXOrder,
|
||||
MockRemoveLiquidity,
|
||||
MockSwapOrder,
|
||||
MockTokenApproval,
|
||||
MockTokenReceive,
|
||||
MockTokenSend,
|
||||
MockTokenTransfer,
|
||||
MockWrap,
|
||||
} from './fixtures/activity'
|
||||
import { parseRemoteActivities, useTimeSince } from './parseRemote'
|
||||
|
||||
describe('parseRemote', () => {
|
||||
beforeEach(() => {
|
||||
jest.useFakeTimers()
|
||||
})
|
||||
describe('parseRemoteActivities', () => {
|
||||
it('should not parse open UniswapX order', () => {
|
||||
const result = parseRemoteActivities(jest.fn(), [MockOpenUniswapXOrder])
|
||||
expect(result).toEqual({})
|
||||
})
|
||||
it('should parse closed UniswapX order', () => {
|
||||
const result = parseRemoteActivities(jest.fn(), [MockClosedUniswapXOrder])
|
||||
expect(result?.['someHash']).toMatchSnapshot()
|
||||
})
|
||||
it('should parse NFT approval', () => {
|
||||
const result = parseRemoteActivities(jest.fn(), [MockNFTApproval])
|
||||
expect(result?.['someHash']).toMatchSnapshot()
|
||||
})
|
||||
it('should parse NFT approval for all', () => {
|
||||
const result = parseRemoteActivities(jest.fn(), [MockNFTApprovalForAll])
|
||||
expect(result?.['someHash']).toMatchSnapshot()
|
||||
})
|
||||
it('should parse NFT transfer', () => {
|
||||
const result = parseRemoteActivities(jest.fn(), [MockNFTTransfer])
|
||||
expect(result?.['someHash']).toMatchSnapshot()
|
||||
})
|
||||
it('should parse swap', () => {
|
||||
const result = parseRemoteActivities(jest.fn().mockReturnValue('100'), [MockTokenTransfer])
|
||||
expect(result?.['someHash']).toMatchSnapshot()
|
||||
})
|
||||
it('should parse nft purchase', () => {
|
||||
const result = parseRemoteActivities(jest.fn().mockReturnValue('100'), [MockNFTPurchase])
|
||||
expect(result?.['someHash']).toMatchSnapshot()
|
||||
})
|
||||
it('should parse token approval', () => {
|
||||
const result = parseRemoteActivities(jest.fn(), [MockTokenApproval])
|
||||
expect(result?.['someHash']).toMatchSnapshot()
|
||||
})
|
||||
it('should parse send', () => {
|
||||
const result = parseRemoteActivities(jest.fn().mockReturnValue(100), [MockTokenSend])
|
||||
expect(result?.['someHash']).toMatchSnapshot()
|
||||
})
|
||||
it('should parse receive', () => {
|
||||
const result = parseRemoteActivities(jest.fn().mockReturnValue(100), [MockTokenReceive])
|
||||
expect(result?.['someHash']).toMatchSnapshot()
|
||||
})
|
||||
it('should parse NFT receive', () => {
|
||||
const result = parseRemoteActivities(jest.fn().mockReturnValue(100), [MockNFTReceive])
|
||||
expect(result?.['someHash']).toMatchSnapshot()
|
||||
})
|
||||
it('should parse remove liquidity', () => {
|
||||
const result = parseRemoteActivities(jest.fn().mockReturnValue(100), [MockRemoveLiquidity])
|
||||
expect(result?.['someHash']).toMatchSnapshot()
|
||||
})
|
||||
it('should parse moonpay purchase', () => {
|
||||
const result = parseRemoteActivities(jest.fn().mockReturnValue('100'), [MockMoonpayPurchase])
|
||||
expect(result?.['someHash']).toMatchSnapshot()
|
||||
})
|
||||
it('should parse swap order', () => {
|
||||
const result = parseRemoteActivities(jest.fn().mockReturnValue('100'), [MockSwapOrder])
|
||||
expect(result?.['someHash']).toMatchSnapshot()
|
||||
})
|
||||
it('should parse eth wrap', () => {
|
||||
const result = parseRemoteActivities(jest.fn().mockReturnValue('100'), [MockWrap])
|
||||
expect(result?.['someHash']).toMatchSnapshot()
|
||||
})
|
||||
})
|
||||
|
||||
describe('useTimeSince', () => {
|
||||
beforeEach(() => {
|
||||
jest.useFakeTimers()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
jest.useRealTimers()
|
||||
})
|
||||
|
||||
it('should initialize with the correct time since', () => {
|
||||
const timestamp = Math.floor(Date.now() / 1000) - 60 // 60 seconds ago
|
||||
const { result } = renderHook(() => useTimeSince(timestamp))
|
||||
|
||||
expect(result.current).toBe('1m')
|
||||
})
|
||||
|
||||
it('should update time since every second', async () => {
|
||||
const timestamp = Math.floor(Date.now() / 1000) - 50 // 50 seconds ago
|
||||
const { result, rerender } = renderHook(() => useTimeSince(timestamp))
|
||||
|
||||
act(() => {
|
||||
jest.advanceTimersByTime(ms('1.1s'))
|
||||
})
|
||||
rerender()
|
||||
|
||||
expect(result.current).toBe('51s')
|
||||
})
|
||||
|
||||
it('should stop updating after 61 seconds', () => {
|
||||
const timestamp = Math.floor(Date.now() / 1000) - 61 // 61 seconds ago
|
||||
const { result, rerender } = renderHook(() => useTimeSince(timestamp))
|
||||
|
||||
act(() => {
|
||||
jest.advanceTimersByTime(ms('121.1s'))
|
||||
})
|
||||
rerender()
|
||||
|
||||
// maxes out at 1m
|
||||
expect(result.current).toBe('1m')
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -21,7 +21,8 @@ import { gqlToCurrency, logSentryErrorForUnsupportedChain, supportedChainIdFromG
|
||||
import ms from 'ms'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { isAddress } from 'utils'
|
||||
import { formatFiatPrice, formatNumberOrString, NumberType } from 'utils/formatNumbers'
|
||||
import { isSameAddress } from 'utils/addresses'
|
||||
import { NumberType, useFormatter } from 'utils/formatNumbers'
|
||||
|
||||
import { MOONPAY_SENDER_ADDRESSES, OrderStatusTable, OrderTextTable } from '../constants'
|
||||
import { Activity } from './types'
|
||||
@@ -34,6 +35,8 @@ type TransactionChanges = {
|
||||
NftApproveForAll: NftApproveForAllPartsFragment[]
|
||||
}
|
||||
|
||||
type FormatNumberOrStringFunctionType = ReturnType<typeof useFormatter>['formatNumberOrString']
|
||||
|
||||
// TODO: Move common contract metadata to a backend service
|
||||
const UNI_IMG =
|
||||
'https://raw.githubusercontent.com/Uniswap/assets/master/blockchains/ethereum/assets/0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984/logo.png'
|
||||
@@ -75,10 +78,6 @@ const COMMON_CONTRACTS: { [key: string]: Partial<Activity> | undefined } = {
|
||||
},
|
||||
}
|
||||
|
||||
function isSameAddress(a?: string, b?: string) {
|
||||
return a === b || a?.toLowerCase() === b?.toLowerCase() // Lazy-lowercases the addresses
|
||||
}
|
||||
|
||||
function callsPositionManagerContract(assetActivity: TransactionActivity) {
|
||||
const supportedChain = supportedChainIdFromGQLChain(assetActivity.chain)
|
||||
if (!supportedChain) return false
|
||||
@@ -140,13 +139,13 @@ function getSwapDescriptor({
|
||||
* @param transactedValue Transacted value amount from TokenTransfer API response
|
||||
* @returns parsed & formatted USD value as a string if currency is of type USD
|
||||
*/
|
||||
function formatTransactedValue(transactedValue: TokenTransferPartsFragment['transactedValue']): string {
|
||||
if (!transactedValue) return '-'
|
||||
function getTransactedValue(transactedValue: TokenTransferPartsFragment['transactedValue']): number | undefined {
|
||||
if (!transactedValue) return undefined
|
||||
const price = transactedValue?.currency === GQLCurrency.Usd ? transactedValue.value ?? undefined : undefined
|
||||
return formatFiatPrice(price)
|
||||
return price
|
||||
}
|
||||
|
||||
function parseSwap(changes: TransactionChanges) {
|
||||
function parseSwap(changes: TransactionChanges, formatNumberOrString: FormatNumberOrStringFunctionType) {
|
||||
if (changes.NftTransfer.length > 0 && changes.TokenTransfer.length === 1) {
|
||||
const collectionCounts = getCollectionCounts(changes.NftTransfer)
|
||||
|
||||
@@ -168,8 +167,8 @@ function parseSwap(changes: TransactionChanges) {
|
||||
|
||||
if (sent && received) {
|
||||
const adjustedInput = parseFloat(sent.quantity) - parseFloat(refund?.quantity ?? '0')
|
||||
const inputAmount = formatNumberOrString(adjustedInput, NumberType.TokenNonTx)
|
||||
const outputAmount = formatNumberOrString(received.quantity, NumberType.TokenNonTx)
|
||||
const inputAmount = formatNumberOrString({ input: adjustedInput, type: NumberType.TokenNonTx })
|
||||
const outputAmount = formatNumberOrString({ input: received.quantity, type: NumberType.TokenNonTx })
|
||||
return {
|
||||
title: getSwapTitle(sent, received),
|
||||
descriptor: getSwapDescriptor({ tokenIn: sent.asset, inputAmount, tokenOut: received.asset, outputAmount }),
|
||||
@@ -180,8 +179,21 @@ function parseSwap(changes: TransactionChanges) {
|
||||
return { title: t`Unknown Swap` }
|
||||
}
|
||||
|
||||
function parseSwapOrder(changes: TransactionChanges) {
|
||||
return { ...parseSwap(changes), prefixIconSrc: UniswapXBolt }
|
||||
/**
|
||||
* Wrap/unwrap transactions are labelled as lend transactions on the backend.
|
||||
* This function parses the transaction changes to determine if the transaction is a wrap/unwrap transaction.
|
||||
*/
|
||||
function parseLend(changes: TransactionChanges, formatNumberOrString: FormatNumberOrStringFunctionType) {
|
||||
const native = changes.TokenTransfer.find((t) => t.tokenStandard === 'NATIVE')?.asset
|
||||
const erc20 = changes.TokenTransfer.find((t) => t.tokenStandard === 'ERC20')?.asset
|
||||
if (native && erc20 && gqlToCurrency(native)?.wrapped.address === gqlToCurrency(erc20)?.wrapped.address) {
|
||||
return parseSwap(changes, formatNumberOrString)
|
||||
}
|
||||
return { title: t`Unknown Lend` }
|
||||
}
|
||||
|
||||
function parseSwapOrder(changes: TransactionChanges, formatNumberOrString: FormatNumberOrStringFunctionType) {
|
||||
return { ...parseSwap(changes, formatNumberOrString), prefixIconSrc: UniswapXBolt }
|
||||
}
|
||||
|
||||
function parseApprove(changes: TransactionChanges) {
|
||||
@@ -194,12 +206,12 @@ function parseApprove(changes: TransactionChanges) {
|
||||
return { title: t`Unknown Approval` }
|
||||
}
|
||||
|
||||
function parseLPTransfers(changes: TransactionChanges) {
|
||||
function parseLPTransfers(changes: TransactionChanges, formatNumberOrString: FormatNumberOrStringFunctionType) {
|
||||
const poolTokenA = changes.TokenTransfer[0]
|
||||
const poolTokenB = changes.TokenTransfer[1]
|
||||
|
||||
const tokenAQuanitity = formatNumberOrString(poolTokenA.quantity, NumberType.TokenNonTx)
|
||||
const tokenBQuantity = formatNumberOrString(poolTokenB.quantity, NumberType.TokenNonTx)
|
||||
const tokenAQuanitity = formatNumberOrString({ input: poolTokenA.quantity, type: NumberType.TokenNonTx })
|
||||
const tokenBQuantity = formatNumberOrString({ input: poolTokenB.quantity, type: NumberType.TokenNonTx })
|
||||
|
||||
return {
|
||||
descriptor: `${tokenAQuanitity} ${poolTokenA.asset.symbol} and ${tokenBQuantity} ${poolTokenB.asset.symbol}`,
|
||||
@@ -211,11 +223,15 @@ function parseLPTransfers(changes: TransactionChanges) {
|
||||
type TransactionActivity = AssetActivityPartsFragment & { details: TransactionDetailsPartsFragment }
|
||||
type OrderActivity = AssetActivityPartsFragment & { details: SwapOrderDetailsPartsFragment }
|
||||
|
||||
function parseSendReceive(changes: TransactionChanges, assetActivity: TransactionActivity) {
|
||||
function parseSendReceive(
|
||||
changes: TransactionChanges,
|
||||
formatNumberOrString: FormatNumberOrStringFunctionType,
|
||||
assetActivity: TransactionActivity
|
||||
) {
|
||||
// TODO(cartcrom): remove edge cases after backend implements
|
||||
// Edge case: Receiving two token transfers in interaction w/ V3 manager === removing liquidity. These edge cases should potentially be moved to backend
|
||||
if (changes.TokenTransfer.length === 2 && callsPositionManagerContract(assetActivity)) {
|
||||
return { title: t`Removed Liquidity`, ...parseLPTransfers(changes) }
|
||||
return { title: t`Removed Liquidity`, ...parseLPTransfers(changes, formatNumberOrString) }
|
||||
}
|
||||
|
||||
let transfer: NftTransferPartsFragment | TokenTransferPartsFragment | undefined
|
||||
@@ -230,7 +246,7 @@ function parseSendReceive(changes: TransactionChanges, assetActivity: Transactio
|
||||
} else if (changes.TokenTransfer.length === 1) {
|
||||
transfer = changes.TokenTransfer[0]
|
||||
assetName = transfer.asset.symbol
|
||||
amount = formatNumberOrString(transfer.quantity, NumberType.TokenNonTx)
|
||||
amount = formatNumberOrString({ input: transfer.quantity, type: NumberType.TokenNonTx })
|
||||
currencies = [gqlToCurrency(transfer.asset)]
|
||||
}
|
||||
|
||||
@@ -241,7 +257,10 @@ function parseSendReceive(changes: TransactionChanges, assetActivity: Transactio
|
||||
return isMoonpayPurchase && transfer.__typename === 'TokenTransfer'
|
||||
? {
|
||||
title: t`Purchased`,
|
||||
descriptor: `${amount} ${assetName} ${t`for`} ${formatTransactedValue(transfer.transactedValue)}`,
|
||||
descriptor: `${amount} ${assetName} ${t`for`} ${formatNumberOrString({
|
||||
input: getTransactedValue(transfer.transactedValue),
|
||||
type: NumberType.FiatTokenPrice,
|
||||
})}`,
|
||||
logos: [moonpayLogoSrc],
|
||||
currencies,
|
||||
}
|
||||
@@ -263,27 +282,40 @@ function parseSendReceive(changes: TransactionChanges, assetActivity: Transactio
|
||||
return { title: t`Unknown Send` }
|
||||
}
|
||||
|
||||
function parseMint(changes: TransactionChanges, assetActivity: TransactionActivity) {
|
||||
function parseMint(
|
||||
changes: TransactionChanges,
|
||||
formatNumberOrString: FormatNumberOrStringFunctionType,
|
||||
assetActivity: TransactionActivity
|
||||
) {
|
||||
const collectionMap = getCollectionCounts(changes.NftTransfer)
|
||||
if (Object.keys(collectionMap).length === 1) {
|
||||
const collectionName = Object.keys(collectionMap)[0]
|
||||
|
||||
// Edge case: Minting a v3 positon represents adding liquidity
|
||||
if (changes.TokenTransfer.length === 2 && callsPositionManagerContract(assetActivity)) {
|
||||
return { title: t`Added Liquidity`, ...parseLPTransfers(changes) }
|
||||
return { title: t`Added Liquidity`, ...parseLPTransfers(changes, formatNumberOrString) }
|
||||
}
|
||||
return { title: t`Minted`, descriptor: `${collectionMap[collectionName]} ${collectionName}` }
|
||||
}
|
||||
return { title: t`Unknown Mint` }
|
||||
}
|
||||
|
||||
function parseUnknown(_changes: TransactionChanges, assetActivity: TransactionActivity) {
|
||||
function parseUnknown(
|
||||
_changes: TransactionChanges,
|
||||
_formatNumberOrString: FormatNumberOrStringFunctionType,
|
||||
assetActivity: TransactionActivity
|
||||
) {
|
||||
return { title: t`Contract Interaction`, ...COMMON_CONTRACTS[assetActivity.details.to.toLowerCase()] }
|
||||
}
|
||||
|
||||
type ActivityTypeParser = (changes: TransactionChanges, assetActivity: TransactionActivity) => Partial<Activity>
|
||||
type ActivityTypeParser = (
|
||||
changes: TransactionChanges,
|
||||
formatNumberOrString: FormatNumberOrStringFunctionType,
|
||||
assetActivity: TransactionActivity
|
||||
) => Partial<Activity>
|
||||
const ActivityParserByType: { [key: string]: ActivityTypeParser | undefined } = {
|
||||
[ActivityType.Swap]: parseSwap,
|
||||
[ActivityType.Lend]: parseLend,
|
||||
[ActivityType.SwapOrder]: parseSwapOrder,
|
||||
[ActivityType.Approve]: parseApprove,
|
||||
[ActivityType.Send]: parseSendReceive,
|
||||
@@ -345,7 +377,10 @@ function parseUniswapXOrder({ details, chain, timestamp }: OrderActivity): Activ
|
||||
}
|
||||
}
|
||||
|
||||
function parseRemoteActivity(assetActivity: AssetActivityPartsFragment): Activity | undefined {
|
||||
function parseRemoteActivity(
|
||||
assetActivity: AssetActivityPartsFragment,
|
||||
formatNumberOrString: FormatNumberOrStringFunctionType
|
||||
): Activity | undefined {
|
||||
try {
|
||||
if (assetActivity.details.__typename === 'SwapOrderDetails') {
|
||||
return parseUniswapXOrder(assetActivity as OrderActivity)
|
||||
@@ -371,6 +406,7 @@ function parseRemoteActivity(assetActivity: AssetActivityPartsFragment): Activit
|
||||
})
|
||||
return undefined
|
||||
}
|
||||
|
||||
const defaultFields = {
|
||||
hash: assetActivity.details.hash,
|
||||
chainId: supportedChain,
|
||||
@@ -385,6 +421,7 @@ function parseRemoteActivity(assetActivity: AssetActivityPartsFragment): Activit
|
||||
|
||||
const parsedFields = ActivityParserByType[assetActivity.details.type]?.(
|
||||
changes,
|
||||
formatNumberOrString,
|
||||
assetActivity as TransactionActivity
|
||||
)
|
||||
return { ...defaultFields, ...parsedFields }
|
||||
@@ -394,9 +431,12 @@ function parseRemoteActivity(assetActivity: AssetActivityPartsFragment): Activit
|
||||
}
|
||||
}
|
||||
|
||||
export function parseRemoteActivities(assetActivities?: readonly AssetActivityPartsFragment[]) {
|
||||
export function parseRemoteActivities(
|
||||
formatNumberOrString: FormatNumberOrStringFunctionType,
|
||||
assetActivities?: readonly AssetActivityPartsFragment[]
|
||||
) {
|
||||
return assetActivities?.reduce((acc: { [hash: string]: Activity }, assetActivity) => {
|
||||
const activity = parseRemoteActivity(assetActivity)
|
||||
const activity = parseRemoteActivity(assetActivity, formatNumberOrString)
|
||||
if (activity) acc[activity.hash] = activity
|
||||
return acc
|
||||
}, {})
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
import { TransactionStatus } from 'graphql/data/__generated__/types-and-hooks' // Replace with the actual import if this is incorrect
|
||||
|
||||
import { Activity } from './types'
|
||||
import { createGroups } from './utils'
|
||||
|
||||
describe('createGroups', () => {
|
||||
it('should return undefined if activities is undefined', () => {
|
||||
expect(createGroups(undefined)).toBeUndefined()
|
||||
})
|
||||
|
||||
it('should return an empty array if activities is empty', () => {
|
||||
expect(createGroups([])).toEqual([])
|
||||
})
|
||||
|
||||
it('should sort and group activities based on status and time', () => {
|
||||
const mockActivities = [
|
||||
{ timestamp: 1700000000, status: TransactionStatus.Pending },
|
||||
{ timestamp: 1650000000, status: TransactionStatus.Confirmed },
|
||||
{ timestamp: Date.now() / 1000 - 300, status: TransactionStatus.Confirmed },
|
||||
] as Activity[]
|
||||
|
||||
const result = createGroups(mockActivities)
|
||||
|
||||
expect(result).toContainEqual(
|
||||
expect.objectContaining({
|
||||
title: 'Pending',
|
||||
transactions: expect.arrayContaining([
|
||||
expect.objectContaining({ timestamp: 1700000000, status: TransactionStatus.Pending }),
|
||||
]),
|
||||
})
|
||||
)
|
||||
|
||||
expect(result).toContainEqual(
|
||||
expect.objectContaining({
|
||||
title: 'Today',
|
||||
transactions: expect.arrayContaining([
|
||||
expect.objectContaining({ timestamp: expect.any(Number), status: TransactionStatus.Confirmed }),
|
||||
]),
|
||||
})
|
||||
)
|
||||
})
|
||||
})
|
||||
65
src/components/AccountDrawer/MiniPortfolio/Activity/utils.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
import { t } from '@lingui/macro'
|
||||
import { getYear, isSameDay, isSameMonth, isSameWeek, isSameYear } from 'date-fns'
|
||||
import { TransactionStatus } from 'graphql/data/__generated__/types-and-hooks'
|
||||
|
||||
import { Activity } from './types'
|
||||
|
||||
interface ActivityGroup {
|
||||
title: string
|
||||
transactions: Array<Activity>
|
||||
}
|
||||
|
||||
const sortActivities = (a: Activity, b: Activity) => b.timestamp - a.timestamp
|
||||
|
||||
export const createGroups = (activities?: Array<Activity>) => {
|
||||
if (!activities) return undefined
|
||||
const now = Date.now()
|
||||
|
||||
const pending: Array<Activity> = []
|
||||
const today: Array<Activity> = []
|
||||
const currentWeek: Array<Activity> = []
|
||||
const last30Days: Array<Activity> = []
|
||||
const currentYear: Array<Activity> = []
|
||||
const yearMap: { [key: string]: Array<Activity> } = {}
|
||||
|
||||
// TODO(cartcrom): create different time bucket system for activities to fall in based on design wants
|
||||
activities.forEach((activity) => {
|
||||
if (activity.status === TransactionStatus.Pending) {
|
||||
pending.push(activity)
|
||||
return
|
||||
}
|
||||
const addedTime = activity.timestamp * 1000
|
||||
|
||||
if (isSameDay(now, addedTime)) {
|
||||
today.push(activity)
|
||||
} else if (isSameWeek(addedTime, now)) {
|
||||
currentWeek.push(activity)
|
||||
} else if (isSameMonth(addedTime, now)) {
|
||||
last30Days.push(activity)
|
||||
} else if (isSameYear(addedTime, now)) {
|
||||
currentYear.push(activity)
|
||||
} else {
|
||||
const year = getYear(addedTime)
|
||||
|
||||
if (!yearMap[year]) {
|
||||
yearMap[year] = [activity]
|
||||
} else {
|
||||
yearMap[year].push(activity)
|
||||
}
|
||||
}
|
||||
})
|
||||
const sortedYears = Object.keys(yearMap)
|
||||
.sort((a, b) => parseInt(b) - parseInt(a))
|
||||
.map((year) => ({ title: year, transactions: yearMap[year] }))
|
||||
|
||||
const transactionGroups: Array<ActivityGroup> = [
|
||||
{ title: t`Pending`, transactions: pending.sort(sortActivities) },
|
||||
{ title: t`Today`, transactions: today.sort(sortActivities) },
|
||||
{ title: t`This week`, transactions: currentWeek.sort(sortActivities) },
|
||||
{ title: t`This month`, transactions: last30Days.sort(sortActivities) },
|
||||
{ title: t`This year`, transactions: currentYear.sort(sortActivities) },
|
||||
...sortedYears,
|
||||
]
|
||||
|
||||
return transactionGroups.filter((transactionInformation) => transactionInformation.transactions.length > 0)
|
||||
}
|
||||
@@ -4,7 +4,7 @@ import Row from 'components/Row'
|
||||
import { PropsWithChildren } from 'react'
|
||||
import { ChevronDown } from 'react-feather'
|
||||
import styled from 'styled-components'
|
||||
import { ThemedText } from 'theme'
|
||||
import { ThemedText } from 'theme/components'
|
||||
|
||||
const ExpandIcon = styled(ChevronDown)<{ $expanded: boolean }>`
|
||||
color: ${({ theme }) => theme.neutral2};
|
||||
|
||||
@@ -11,7 +11,7 @@ import { WalletAsset } from 'nft/types'
|
||||
import { floorFormatter } from 'nft/utils'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import styled from 'styled-components'
|
||||
import { ThemedText } from 'theme'
|
||||
import { ThemedText } from 'theme/components'
|
||||
|
||||
const FloorPrice = styled(Row)`
|
||||
opacity: 0;
|
||||
|
||||
@@ -9,8 +9,9 @@ import MulticallJSON from '@uniswap/v3-periphery/artifacts/contracts/lens/Uniswa
|
||||
import NFTPositionManagerJSON from '@uniswap/v3-periphery/artifacts/contracts/NonfungiblePositionManager.sol/NonfungiblePositionManager.json'
|
||||
import { useWeb3React } from '@web3-react/core'
|
||||
import { isSupportedChain } from 'constants/chains'
|
||||
import { RPC_PROVIDERS } from 'constants/providers'
|
||||
import { DEPRECATED_RPC_PROVIDERS, RPC_PROVIDERS } from 'constants/providers'
|
||||
import { BaseContract } from 'ethers/lib/ethers'
|
||||
import { useFallbackProviderEnabled } from 'featureFlags/flags/fallbackProvider'
|
||||
import { ContractInput, useUniswapPricesQuery } from 'graphql/data/__generated__/types-and-hooks'
|
||||
import { toContractInput } from 'graphql/data/util'
|
||||
import useStablecoinPrice from 'hooks/useStablecoinPrice'
|
||||
@@ -31,6 +32,8 @@ function useContractMultichain<T extends BaseContract>(
|
||||
): ContractMap<T> {
|
||||
const { chainId: walletChainId, provider: walletProvider } = useWeb3React()
|
||||
|
||||
const networkProviders = useFallbackProviderEnabled() ? RPC_PROVIDERS : DEPRECATED_RPC_PROVIDERS
|
||||
|
||||
return useMemo(() => {
|
||||
const relevantChains =
|
||||
chainIds ??
|
||||
@@ -43,14 +46,14 @@ function useContractMultichain<T extends BaseContract>(
|
||||
walletProvider && walletChainId === chainId
|
||||
? walletProvider
|
||||
: isSupportedChain(chainId)
|
||||
? RPC_PROVIDERS[chainId]
|
||||
? networkProviders[chainId]
|
||||
: undefined
|
||||
if (provider) {
|
||||
acc[chainId] = getContract(addressMap[chainId] ?? '', ABI, provider) as T
|
||||
}
|
||||
return acc
|
||||
}, {})
|
||||
}, [ABI, addressMap, chainIds, walletChainId, walletProvider])
|
||||
}, [ABI, addressMap, chainIds, networkProviders, walletChainId, walletProvider])
|
||||
}
|
||||
|
||||
export function useV3ManagerContracts(chainIds: ChainId[]): ContractMap<NonfungiblePositionManager> {
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
import { BigNumber } from '@ethersproject/bignumber'
|
||||
import { ChainId, WETH9 } from '@uniswap/sdk-core'
|
||||
import { FeeAmount, Pool, Position } from '@uniswap/v3-sdk'
|
||||
import { USDC_MAINNET } from 'constants/tokens'
|
||||
import { mocked } from 'test-utils/mocked'
|
||||
import { owner, useMultiChainPositionsReturnValue } from 'test-utils/pools/fixtures'
|
||||
import { render } from 'test-utils/render'
|
||||
|
||||
import Pools from '.'
|
||||
@@ -12,53 +9,6 @@ jest.mock('./useMultiChainPositions')
|
||||
|
||||
jest.spyOn(console, 'warn').mockImplementation()
|
||||
|
||||
const owner = '0xf5b6bb25f5beaea03dd014c6ef9fa9f3926bf36c'
|
||||
|
||||
const pool = new Pool(
|
||||
USDC_MAINNET,
|
||||
WETH9[ChainId.MAINNET],
|
||||
FeeAmount.MEDIUM,
|
||||
'1851127709498178402383049949138810',
|
||||
'7076437181775065414',
|
||||
201189
|
||||
)
|
||||
|
||||
const position = new Position({
|
||||
pool,
|
||||
liquidity: 1341008833950736,
|
||||
tickLower: 200040,
|
||||
tickUpper: 202560,
|
||||
})
|
||||
const details = {
|
||||
nonce: BigNumber.from('0'),
|
||||
tokenId: BigNumber.from('0'),
|
||||
operator: '0x0',
|
||||
token0: USDC_MAINNET.address,
|
||||
token1: WETH9[ChainId.MAINNET].address,
|
||||
fee: FeeAmount.MEDIUM,
|
||||
tickLower: -100,
|
||||
tickUpper: 100,
|
||||
liquidity: BigNumber.from('9000'),
|
||||
feeGrowthInside0LastX128: BigNumber.from('0'),
|
||||
feeGrowthInside1LastX128: BigNumber.from('0'),
|
||||
tokensOwed0: BigNumber.from('0'),
|
||||
tokensOwed1: BigNumber.from('0'),
|
||||
}
|
||||
const useMultiChainPositionsReturnValue = {
|
||||
positions: [
|
||||
{
|
||||
owner,
|
||||
chainId: ChainId.MAINNET,
|
||||
position,
|
||||
pool,
|
||||
details,
|
||||
inRange: true,
|
||||
closed: false,
|
||||
},
|
||||
],
|
||||
loading: false,
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
mocked(useMultiChainPositions).mockReturnValue(useMultiChainPositionsReturnValue)
|
||||
})
|
||||
|
||||
@@ -12,7 +12,7 @@ import { EmptyWalletModule } from 'nft/components/profile/view/EmptyWalletConten
|
||||
import { useCallback, useMemo, useReducer } from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import styled from 'styled-components'
|
||||
import { ThemedText } from 'theme'
|
||||
import { ThemedText } from 'theme/components'
|
||||
import { NumberType, useFormatter } from 'utils/formatNumbers'
|
||||
|
||||
import { ExpandoRow } from '../ExpandoRow'
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { BrowserEvent, InterfaceElementName, SharedEventName } from '@uniswap/analytics-events'
|
||||
import { TraceEvent } from 'analytics'
|
||||
import { useCachedPortfolioBalancesQuery } from 'components/AccountDrawer/PrefetchBalancesWrapper'
|
||||
import { useCachedPortfolioBalancesQuery } from 'components/PrefetchBalancesWrapper/PrefetchBalancesWrapper'
|
||||
import Row from 'components/Row'
|
||||
import { DeltaArrow, formatDelta } from 'components/Tokens/TokenDetails/Delta'
|
||||
import { DeltaArrow } from 'components/Tokens/TokenDetails/Delta'
|
||||
import { TokenBalance } from 'graphql/data/__generated__/types-and-hooks'
|
||||
import { getTokenDetailsURL, gqlToCurrency, logSentryErrorForUnsupportedChain } from 'graphql/data/util'
|
||||
import { useAtomValue } from 'jotai/utils'
|
||||
@@ -10,7 +10,7 @@ import { EmptyWalletModule } from 'nft/components/profile/view/EmptyWalletConten
|
||||
import { useCallback, useMemo, useState } from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import styled from 'styled-components'
|
||||
import { EllipsisStyle, ThemedText } from 'theme'
|
||||
import { EllipsisStyle, ThemedText } from 'theme/components'
|
||||
import { NumberType, useFormatter } from 'utils/formatNumbers'
|
||||
import { splitHiddenTokens } from 'utils/splitHiddenTokens'
|
||||
|
||||
@@ -71,6 +71,7 @@ const TokenNameText = styled(ThemedText.SubHeader)`
|
||||
type PortfolioToken = NonNullable<TokenBalance['token']>
|
||||
|
||||
function TokenRow({ token, quantity, denominatedValue, tokenProjectMarket }: TokenBalance & { token: PortfolioToken }) {
|
||||
const { formatPercent } = useFormatter()
|
||||
const percentChange = tokenProjectMarket?.pricePercentChange?.value ?? 0
|
||||
|
||||
const navigate = useNavigate()
|
||||
@@ -120,7 +121,7 @@ function TokenRow({ token, quantity, denominatedValue, tokenProjectMarket }: Tok
|
||||
</ThemedText.SubHeader>
|
||||
<Row justify="flex-end">
|
||||
<DeltaArrow delta={percentChange} />
|
||||
<ThemedText.BodySecondary>{formatDelta(percentChange)}</ThemedText.BodySecondary>
|
||||
<ThemedText.BodySecondary>{formatPercent(percentChange)}</ThemedText.BodySecondary>
|
||||
</Row>
|
||||
</>
|
||||
)
|
||||
|
||||
@@ -8,7 +8,8 @@ import { useDisableNFTRoutes } from 'hooks/useDisableNFTRoutes'
|
||||
import { useIsNftPage } from 'hooks/useIsNftPage'
|
||||
import { useEffect, useState } from 'react'
|
||||
import styled, { useTheme } from 'styled-components'
|
||||
import { BREAKPOINTS, ThemedText } from 'theme'
|
||||
import { BREAKPOINTS } from 'theme'
|
||||
import { ThemedText } from 'theme/components'
|
||||
|
||||
import { ActivityTab } from './Activity'
|
||||
import { usePendingActivity } from './Activity/hooks'
|
||||
|
||||
@@ -8,7 +8,7 @@ import { useActiveLocale } from 'hooks/useActiveLocale'
|
||||
import { ReactNode } from 'react'
|
||||
import { ChevronRight } from 'react-feather'
|
||||
import styled from 'styled-components'
|
||||
import { ClickableStyle, ThemedText } from 'theme'
|
||||
import { ClickableStyle, ThemedText } from 'theme/components'
|
||||
import ThemeToggle from 'theme/components/ThemeToggle'
|
||||
|
||||
import { AnalyticsToggle } from './AnalyticsToggle'
|
||||
|
||||
@@ -2,7 +2,7 @@ import Column from 'components/Column'
|
||||
import Row from 'components/Row'
|
||||
import Toggle from 'components/Toggle'
|
||||
import styled from 'styled-components'
|
||||
import { ThemedText } from 'theme'
|
||||
import { ThemedText } from 'theme/components'
|
||||
|
||||
const StyledColumn = styled(Column)`
|
||||
width: 100%;
|
||||
|
||||
@@ -2,7 +2,7 @@ import Column from 'components/Column'
|
||||
import { ScrollBarStyles } from 'components/Common'
|
||||
import { ArrowLeft } from 'react-feather'
|
||||
import styled from 'styled-components'
|
||||
import { ClickableStyle, ThemedText } from 'theme'
|
||||
import { ClickableStyle, ThemedText } from 'theme/components'
|
||||
|
||||
const Menu = styled(Column)`
|
||||
width: 100%;
|
||||
|
||||
@@ -12,7 +12,7 @@ import { UniwalletConnect as UniwalletConnectV2 } from 'connection/WalletConnect
|
||||
import { QRCodeSVG } from 'qrcode.react'
|
||||
import { useEffect, useState } from 'react'
|
||||
import styled, { useTheme } from 'styled-components'
|
||||
import { CloseIcon, ThemedText } from 'theme'
|
||||
import { CloseIcon, ThemedText } from 'theme/components'
|
||||
import { isIOS } from 'utils/userAgent'
|
||||
|
||||
import uniPng from '../../assets/images/uniwallet_modal_icon.png'
|
||||
|
||||
@@ -2,6 +2,7 @@ import { BrowserEvent, InterfaceEventName } from '@uniswap/analytics-events'
|
||||
import { TraceEvent } from 'analytics'
|
||||
import { ScrollBarStyles } from 'components/Common'
|
||||
import useDisableScrolling from 'hooks/useDisableScrolling'
|
||||
import usePrevious from 'hooks/usePrevious'
|
||||
import { useWindowSize } from 'hooks/useWindowSize'
|
||||
import { atom } from 'jotai'
|
||||
import { useAtomValue, useUpdateAtom } from 'jotai/utils'
|
||||
@@ -9,7 +10,8 @@ import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { ChevronsRight } from 'react-feather'
|
||||
import { useGesture } from 'react-use-gesture'
|
||||
import styled from 'styled-components'
|
||||
import { BREAKPOINTS, ClickableStyle } from 'theme'
|
||||
import { BREAKPOINTS } from 'theme'
|
||||
import { ClickableStyle } from 'theme/components'
|
||||
import { Z_INDEX } from 'theme/zIndex'
|
||||
import { isMobile } from 'utils/userAgent'
|
||||
|
||||
@@ -166,12 +168,13 @@ const CloseDrawer = styled.div`
|
||||
|
||||
function AccountDrawer() {
|
||||
const [walletDrawerOpen, toggleWalletDrawer] = useAccountDrawer()
|
||||
const wasWalletDrawerOpen = usePrevious(walletDrawerOpen)
|
||||
const scrollRef = useRef<HTMLDivElement>(null)
|
||||
useEffect(() => {
|
||||
if (!walletDrawerOpen) {
|
||||
if (wasWalletDrawerOpen && !walletDrawerOpen) {
|
||||
scrollRef.current?.scrollTo({ top: 0, behavior: 'smooth' })
|
||||
}
|
||||
}, [walletDrawerOpen])
|
||||
}, [walletDrawerOpen, wasWalletDrawerOpen])
|
||||
|
||||
// close on escape keypress
|
||||
useEffect(() => {
|
||||
|
||||
@@ -5,7 +5,8 @@ import { Check } from 'react-feather'
|
||||
import type { To } from 'react-router-dom'
|
||||
import { Link } from 'react-router-dom'
|
||||
import styled, { useTheme } from 'styled-components'
|
||||
import { BREAKPOINTS, ClickableStyle, ThemedText } from 'theme'
|
||||
import { BREAKPOINTS } from 'theme'
|
||||
import { ClickableStyle, ThemedText } from 'theme/components'
|
||||
|
||||
const InternalLinkMenuItem = styled(Link)`
|
||||
${ClickableStyle}
|
||||
|
||||
@@ -4,10 +4,10 @@ import { t } from '@lingui/macro'
|
||||
import { useWeb3React } from '@web3-react/core'
|
||||
import { ChangeEvent, ReactNode, useCallback } from 'react'
|
||||
import styled, { useTheme } from 'styled-components'
|
||||
import { ExternalLink, ThemedText } from 'theme/components'
|
||||
import { flexColumnNoWrap } from 'theme/styles'
|
||||
|
||||
import useENS from '../../hooks/useENS'
|
||||
import { ExternalLink, ThemedText } from '../../theme'
|
||||
import { ExplorerDataType, getExplorerLink } from '../../utils/getExplorerLink'
|
||||
import { AutoColumn } from '../Column'
|
||||
import { RowBetween } from '../Row'
|
||||
|
||||
@@ -10,14 +10,14 @@ import Loader from 'components/Icons/LoadingSpinner'
|
||||
import { useContract } from 'hooks/useContract'
|
||||
import { ChevronRightIcon } from 'nft/components/icons'
|
||||
import { useIsNftClaimAvailable } from 'nft/hooks/useIsNftClaimAvailable'
|
||||
import { CollectionRewardsFetcher } from 'nft/queries/genie/GetAirdorpMerkle'
|
||||
import { CollectionRewardsFetcher } from 'nft/queries/genie'
|
||||
import { Airdrop, Rewards } from 'nft/types/airdrop'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { AlertTriangle } from 'react-feather'
|
||||
import { useModalIsOpen, useToggleModal } from 'state/application/hooks'
|
||||
import { ApplicationModal } from 'state/application/reducer'
|
||||
import styled from 'styled-components'
|
||||
import { CloseIcon, ThemedText } from 'theme'
|
||||
import { CloseIcon, ThemedText } from 'theme/components'
|
||||
|
||||
import Modal from '../Modal'
|
||||
|
||||
|
||||
@@ -55,7 +55,7 @@ export default function RangeBadge({ removed, inRange }: { removed?: boolean; in
|
||||
>
|
||||
<LabelText color={theme.success}>
|
||||
<BadgeText>
|
||||
<Trans>In Range</Trans>
|
||||
<Trans>In range</Trans>
|
||||
</BadgeText>
|
||||
<ActiveDot />
|
||||
</LabelText>
|
||||
|
||||
@@ -7,7 +7,7 @@ import baseLogoUrl from 'assets/svg/base_background_icon.svg'
|
||||
import { useScreenSize } from 'hooks/useScreenSize'
|
||||
import { useLocation } from 'react-router-dom'
|
||||
import { useHideBaseWalletBanner } from 'state/user/hooks'
|
||||
import { ThemedText } from 'theme'
|
||||
import { ThemedText } from 'theme/components'
|
||||
import { openDownloadApp, openWalletMicrosite } from 'utils/openDownloadApp'
|
||||
import { isIOS, isMobileSafari } from 'utils/userAgent'
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { SpinnerSVG } from 'theme'
|
||||
import { SpinnerSVG } from 'theme/components'
|
||||
|
||||
const ButtonLoadingSpinner = (props: React.ComponentPropsWithoutRef<'svg'>) => (
|
||||
<SpinnerSVG width="20" height="20" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg" {...props}>
|
||||
|
||||
70
src/components/Charts/PriceChart/ChartModel.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
import { ScaleLinear, scaleLinear } from 'd3'
|
||||
import { PricePoint } from 'graphql/data/util'
|
||||
|
||||
import { cleanPricePoints, getPriceBounds } from './utils'
|
||||
|
||||
export enum ChartErrorType {
|
||||
NO_DATA_AVAILABLE,
|
||||
NO_RECENT_VOLUME,
|
||||
INVALID_CHART,
|
||||
}
|
||||
|
||||
type ChartDimensions = {
|
||||
width: number
|
||||
height: number
|
||||
marginTop: number
|
||||
marginBottom: number
|
||||
}
|
||||
|
||||
export type ErroredChartModel = { error: ChartErrorType; dimensions: ChartDimensions }
|
||||
|
||||
export type ChartModel = {
|
||||
prices: PricePoint[]
|
||||
startingPrice: PricePoint
|
||||
endingPrice: PricePoint
|
||||
lastValidPrice: PricePoint
|
||||
blanks: PricePoint[][]
|
||||
timeScale: ScaleLinear<number, number>
|
||||
priceScale: ScaleLinear<number, number>
|
||||
dimensions: ChartDimensions
|
||||
error: undefined
|
||||
}
|
||||
|
||||
type ChartModelArgs = { prices?: PricePoint[]; dimensions: ChartDimensions }
|
||||
export function buildChartModel({ dimensions, prices }: ChartModelArgs): ChartModel | ErroredChartModel {
|
||||
if (!prices) {
|
||||
return { error: ChartErrorType.NO_DATA_AVAILABLE, dimensions }
|
||||
}
|
||||
|
||||
const innerHeight = dimensions.height - dimensions.marginTop - dimensions.marginBottom
|
||||
if (innerHeight < 0) {
|
||||
return { error: ChartErrorType.INVALID_CHART, dimensions }
|
||||
}
|
||||
|
||||
const { prices: fixedPrices, blanks, lastValidPrice } = cleanPricePoints(prices)
|
||||
if (fixedPrices.length < 2 || !lastValidPrice) {
|
||||
return { error: ChartErrorType.NO_RECENT_VOLUME, dimensions }
|
||||
}
|
||||
|
||||
const startingPrice = prices[0]
|
||||
const endingPrice = prices[prices.length - 1]
|
||||
const { min, max } = getPriceBounds(prices)
|
||||
|
||||
// x-axis scale
|
||||
const timeScale = scaleLinear().domain([startingPrice.timestamp, endingPrice.timestamp]).range([0, dimensions.width])
|
||||
|
||||
// y-axis scale
|
||||
const priceScale = scaleLinear().domain([min, max]).range([innerHeight, 0])
|
||||
|
||||
return {
|
||||
prices: fixedPrices,
|
||||
startingPrice,
|
||||
endingPrice,
|
||||
lastValidPrice,
|
||||
blanks,
|
||||
timeScale,
|
||||
priceScale,
|
||||
dimensions,
|
||||
error: undefined,
|
||||
}
|
||||
}
|
||||
@@ -15,9 +15,13 @@ exports[`PriceChart renders correctly with all prices filled 1`] = `
|
||||
}
|
||||
|
||||
.c1 {
|
||||
font-size: 36px;
|
||||
display: -webkit-box;
|
||||
display: -webkit-flex;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
font-size: 24px;
|
||||
line-height: 44px;
|
||||
font-weight: 485;
|
||||
}
|
||||
|
||||
.c2 {
|
||||
@@ -38,11 +42,15 @@ exports[`PriceChart renders correctly with all prices filled 1`] = `
|
||||
class="c0"
|
||||
data-cy="chart-header"
|
||||
>
|
||||
<span
|
||||
<div
|
||||
class="c1"
|
||||
>
|
||||
$1.00
|
||||
</span>
|
||||
<div
|
||||
class="css-15popx1"
|
||||
>
|
||||
$1.00
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="c2"
|
||||
>
|
||||
@@ -337,10 +345,10 @@ exports[`PriceChart renders correctly with all prices filled 1`] = `
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
||||
exports[`PriceChart renders correctly with no prices filled 1`] = `
|
||||
exports[`PriceChart renders correctly with empty price array 1`] = `
|
||||
<DocumentFragment>
|
||||
.c3 {
|
||||
color: #222222;
|
||||
.c1 {
|
||||
color: #CECECE;
|
||||
}
|
||||
|
||||
.c0 {
|
||||
@@ -351,41 +359,22 @@ exports[`PriceChart renders correctly with no prices filled 1`] = `
|
||||
animation-duration: 250ms;
|
||||
}
|
||||
|
||||
.c1 {
|
||||
font-size: 36px;
|
||||
line-height: 44px;
|
||||
font-weight: 485;
|
||||
}
|
||||
|
||||
.c2 {
|
||||
font-size: 24px;
|
||||
line-height: 44px;
|
||||
color: #CECECE;
|
||||
}
|
||||
|
||||
<div
|
||||
class="c0"
|
||||
data-cy="chart-header"
|
||||
>
|
||||
<span
|
||||
class="c1 c2"
|
||||
>
|
||||
Price Unavailable
|
||||
</span>
|
||||
<div
|
||||
class="c3 css-142zc9n"
|
||||
style="color: rgb(206, 206, 206);"
|
||||
class="c1 css-slqfkh"
|
||||
>
|
||||
Missing chart data
|
||||
Price unavailable
|
||||
</div>
|
||||
<div
|
||||
class="c1 css-142zc9n"
|
||||
>
|
||||
Missing price data due to recently low trading volume on Uniswap v3
|
||||
</div>
|
||||
</div>
|
||||
.c0 text {
|
||||
font-size: 12px;
|
||||
font-weight: 485;
|
||||
}
|
||||
|
||||
<svg
|
||||
class="c0"
|
||||
<svg
|
||||
data-cy="missing-chart"
|
||||
height="392"
|
||||
style="min-width: 100%;"
|
||||
@@ -398,23 +387,18 @@ exports[`PriceChart renders correctly with no prices filled 1`] = `
|
||||
stroke="#22222212"
|
||||
stroke-width="2"
|
||||
/>
|
||||
<text
|
||||
fill="#CECECE"
|
||||
x="20"
|
||||
y="377"
|
||||
/>
|
||||
</svg>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
||||
exports[`PriceChart renders correctly with some prices filled 1`] = `
|
||||
<DocumentFragment>
|
||||
.c4 {
|
||||
.c2 {
|
||||
display: inline-block;
|
||||
height: inherit;
|
||||
}
|
||||
|
||||
.c6 {
|
||||
.c4 {
|
||||
color: #7D7D7D;
|
||||
}
|
||||
|
||||
@@ -424,19 +408,20 @@ exports[`PriceChart renders correctly with some prices filled 1`] = `
|
||||
animation: iAjNNh 125ms ease-in;
|
||||
-webkit-animation-duration: 250ms;
|
||||
animation-duration: 250ms;
|
||||
}
|
||||
|
||||
.c3 {
|
||||
font-size: 36px;
|
||||
line-height: 44px;
|
||||
font-weight: 485;
|
||||
}
|
||||
|
||||
.c1 {
|
||||
color: #7D7D7D;
|
||||
}
|
||||
|
||||
.c5 {
|
||||
.c1 {
|
||||
display: -webkit-box;
|
||||
display: -webkit-flex;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
font-size: 24px;
|
||||
line-height: 44px;
|
||||
}
|
||||
|
||||
.c3 {
|
||||
height: 16px;
|
||||
display: -webkit-box;
|
||||
display: -webkit-flex;
|
||||
@@ -450,16 +435,6 @@ exports[`PriceChart renders correctly with some prices filled 1`] = `
|
||||
color: #7D7D7D;
|
||||
}
|
||||
|
||||
.c2 {
|
||||
display: -webkit-box;
|
||||
display: -webkit-flex;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
font-size: 24px;
|
||||
line-height: 44px;
|
||||
}
|
||||
|
||||
<div
|
||||
class="c0"
|
||||
data-cy="chart-header"
|
||||
@@ -468,69 +443,66 @@ exports[`PriceChart renders correctly with some prices filled 1`] = `
|
||||
class="c1"
|
||||
>
|
||||
<div
|
||||
class="c2"
|
||||
class="css-15popx1"
|
||||
>
|
||||
<span
|
||||
class="c3"
|
||||
>
|
||||
$1.00
|
||||
</span>
|
||||
<div
|
||||
class="c4"
|
||||
>
|
||||
<div>
|
||||
<svg
|
||||
fill="none"
|
||||
height="16"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
viewBox="0 0 24 24"
|
||||
width="16"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<circle
|
||||
cx="12"
|
||||
cy="12"
|
||||
r="10"
|
||||
/>
|
||||
<line
|
||||
x1="12"
|
||||
x2="12"
|
||||
y1="16"
|
||||
y2="12"
|
||||
/>
|
||||
<line
|
||||
x1="12"
|
||||
x2="12.01"
|
||||
y1="8"
|
||||
y2="8"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
$1.00
|
||||
</div>
|
||||
<div
|
||||
class="c5"
|
||||
class="c2"
|
||||
>
|
||||
0.00%
|
||||
<svg
|
||||
aria-label="up"
|
||||
class="c6"
|
||||
fill="none"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
width="16"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M13.3021 7.7547L17.6821 14.2475C18.4182 15.3388 17.7942 17 16.6482 17L7.3518 17C6.2058 17 5.5818 15.3376 6.3179 14.2475L10.6979 7.7547C11.377 6.7484 12.623 6.7484 13.3021 7.7547Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
<div>
|
||||
<svg
|
||||
data-testid="chart-stale-icon"
|
||||
fill="none"
|
||||
height="16"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
viewBox="0 0 24 24"
|
||||
width="16"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<circle
|
||||
cx="12"
|
||||
cy="12"
|
||||
r="10"
|
||||
/>
|
||||
<line
|
||||
x1="12"
|
||||
x2="12"
|
||||
y1="16"
|
||||
y2="12"
|
||||
/>
|
||||
<line
|
||||
x1="12"
|
||||
x2="12.01"
|
||||
y1="8"
|
||||
y2="8"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="c3"
|
||||
>
|
||||
0.00%
|
||||
<svg
|
||||
aria-label="up"
|
||||
class="c4"
|
||||
fill="none"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
width="16"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M13.3021 7.7547L17.6821 14.2475C18.4182 15.3388 17.7942 17 16.6482 17L7.3518 17C6.2058 17 5.5818 15.3376 6.3179 14.2475L10.6979 7.7547C11.377 6.7484 12.623 6.7484 13.3021 7.7547Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<svg
|
||||
data-cy="price-chart"
|
||||
@@ -805,3 +777,548 @@ exports[`PriceChart renders correctly with some prices filled 1`] = `
|
||||
</svg>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
||||
exports[`PriceChart renders correctly with undefined prices 1`] = `
|
||||
<DocumentFragment>
|
||||
.c1 {
|
||||
color: #CECECE;
|
||||
}
|
||||
|
||||
.c0 {
|
||||
position: absolute;
|
||||
-webkit-animation: iAjNNh 125ms ease-in;
|
||||
animation: iAjNNh 125ms ease-in;
|
||||
-webkit-animation-duration: 250ms;
|
||||
animation-duration: 250ms;
|
||||
}
|
||||
|
||||
<div
|
||||
class="c0"
|
||||
data-cy="chart-header"
|
||||
>
|
||||
<div
|
||||
class="c1 css-slqfkh"
|
||||
>
|
||||
Price unavailable
|
||||
</div>
|
||||
<div
|
||||
class="c1 css-142zc9n"
|
||||
>
|
||||
Missing chart data
|
||||
</div>
|
||||
</div>
|
||||
<svg
|
||||
data-cy="missing-chart"
|
||||
height="392"
|
||||
style="min-width: 100%;"
|
||||
width="780"
|
||||
>
|
||||
<path
|
||||
d="M 0 241 Q 104 171, 208 241 T 416 241
|
||||
M 416 241 Q 520 171, 624 241 T 832 241"
|
||||
fill="transparent"
|
||||
stroke="#22222212"
|
||||
stroke-width="2"
|
||||
/>
|
||||
</svg>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
||||
exports[`PriceChart renders stale UI 1`] = `
|
||||
<DocumentFragment>
|
||||
.c2 {
|
||||
display: inline-block;
|
||||
height: inherit;
|
||||
}
|
||||
|
||||
.c4 {
|
||||
color: #7D7D7D;
|
||||
}
|
||||
|
||||
.c0 {
|
||||
position: absolute;
|
||||
-webkit-animation: iAjNNh 125ms ease-in;
|
||||
animation: iAjNNh 125ms ease-in;
|
||||
-webkit-animation-duration: 250ms;
|
||||
animation-duration: 250ms;
|
||||
color: #7D7D7D;
|
||||
}
|
||||
|
||||
.c1 {
|
||||
display: -webkit-box;
|
||||
display: -webkit-flex;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
font-size: 24px;
|
||||
line-height: 44px;
|
||||
}
|
||||
|
||||
.c3 {
|
||||
height: 16px;
|
||||
display: -webkit-box;
|
||||
display: -webkit-flex;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
-webkit-align-items: center;
|
||||
-webkit-box-align: center;
|
||||
-ms-flex-align: center;
|
||||
align-items: center;
|
||||
margin-top: 4px;
|
||||
color: #7D7D7D;
|
||||
}
|
||||
|
||||
<div
|
||||
class="c0"
|
||||
data-cy="chart-header"
|
||||
>
|
||||
<div
|
||||
class="c1"
|
||||
>
|
||||
<div
|
||||
class="css-15popx1"
|
||||
>
|
||||
$1.00
|
||||
</div>
|
||||
<div
|
||||
class="c2"
|
||||
>
|
||||
<div>
|
||||
<svg
|
||||
data-testid="chart-stale-icon"
|
||||
fill="none"
|
||||
height="16"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
viewBox="0 0 24 24"
|
||||
width="16"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<circle
|
||||
cx="12"
|
||||
cy="12"
|
||||
r="10"
|
||||
/>
|
||||
<line
|
||||
x1="12"
|
||||
x2="12"
|
||||
y1="16"
|
||||
y2="12"
|
||||
/>
|
||||
<line
|
||||
x1="12"
|
||||
x2="12.01"
|
||||
y1="8"
|
||||
y2="8"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="c3"
|
||||
>
|
||||
0.00%
|
||||
<svg
|
||||
aria-label="up"
|
||||
class="c4"
|
||||
fill="none"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
width="16"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M13.3021 7.7547L17.6821 14.2475C18.4182 15.3388 17.7942 17 16.6482 17L7.3518 17C6.2058 17 5.5818 15.3376 6.3179 14.2475L10.6979 7.7547C11.377 6.7484 12.623 6.7484 13.3021 7.7547Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<svg
|
||||
data-cy="price-chart"
|
||||
height="392"
|
||||
style="min-width: 100%;"
|
||||
width="780"
|
||||
>
|
||||
<g
|
||||
class="visx-group visx-axis visx-axis-bottom"
|
||||
transform="translate(0, 391)"
|
||||
>
|
||||
<g
|
||||
class="visx-group visx-axis-tick"
|
||||
transform="translate(0, 0)"
|
||||
>
|
||||
<svg
|
||||
font-size="10"
|
||||
style="overflow: visible;"
|
||||
x="0"
|
||||
y="0.25em"
|
||||
>
|
||||
<text
|
||||
fill="#222"
|
||||
font-family="Arial"
|
||||
font-size="10"
|
||||
text-anchor="middle"
|
||||
transform=""
|
||||
x="48.75"
|
||||
y="18"
|
||||
>
|
||||
<tspan
|
||||
dy="0em"
|
||||
x="48.75"
|
||||
>
|
||||
1,694,538,840
|
||||
</tspan>
|
||||
</text>
|
||||
</svg>
|
||||
</g>
|
||||
<g
|
||||
class="visx-group visx-axis-tick"
|
||||
transform="translate(0, 0)"
|
||||
>
|
||||
<svg
|
||||
font-size="10"
|
||||
style="overflow: visible;"
|
||||
x="0"
|
||||
y="0.25em"
|
||||
>
|
||||
<text
|
||||
fill="#222"
|
||||
font-family="Arial"
|
||||
font-size="10"
|
||||
text-anchor="middle"
|
||||
transform=""
|
||||
x="109.6875"
|
||||
y="18"
|
||||
>
|
||||
<tspan
|
||||
dy="0em"
|
||||
x="109.6875"
|
||||
>
|
||||
1,694,538,845
|
||||
</tspan>
|
||||
</text>
|
||||
</svg>
|
||||
</g>
|
||||
<g
|
||||
class="visx-group visx-axis-tick"
|
||||
transform="translate(0, 0)"
|
||||
>
|
||||
<svg
|
||||
font-size="10"
|
||||
style="overflow: visible;"
|
||||
x="0"
|
||||
y="0.25em"
|
||||
>
|
||||
<text
|
||||
fill="#222"
|
||||
font-family="Arial"
|
||||
font-size="10"
|
||||
text-anchor="middle"
|
||||
transform=""
|
||||
x="170.625"
|
||||
y="18"
|
||||
>
|
||||
<tspan
|
||||
dy="0em"
|
||||
x="170.625"
|
||||
>
|
||||
1,694,538,850
|
||||
</tspan>
|
||||
</text>
|
||||
</svg>
|
||||
</g>
|
||||
<g
|
||||
class="visx-group visx-axis-tick"
|
||||
transform="translate(0, 0)"
|
||||
>
|
||||
<svg
|
||||
font-size="10"
|
||||
style="overflow: visible;"
|
||||
x="0"
|
||||
y="0.25em"
|
||||
>
|
||||
<text
|
||||
fill="#222"
|
||||
font-family="Arial"
|
||||
font-size="10"
|
||||
text-anchor="middle"
|
||||
transform=""
|
||||
x="231.5625"
|
||||
y="18"
|
||||
>
|
||||
<tspan
|
||||
dy="0em"
|
||||
x="231.5625"
|
||||
>
|
||||
1,694,538,855
|
||||
</tspan>
|
||||
</text>
|
||||
</svg>
|
||||
</g>
|
||||
<g
|
||||
class="visx-group visx-axis-tick"
|
||||
transform="translate(0, 0)"
|
||||
>
|
||||
<svg
|
||||
font-size="10"
|
||||
style="overflow: visible;"
|
||||
x="0"
|
||||
y="0.25em"
|
||||
>
|
||||
<text
|
||||
fill="#222"
|
||||
font-family="Arial"
|
||||
font-size="10"
|
||||
text-anchor="middle"
|
||||
transform=""
|
||||
x="292.5"
|
||||
y="18"
|
||||
>
|
||||
<tspan
|
||||
dy="0em"
|
||||
x="292.5"
|
||||
>
|
||||
1,694,538,860
|
||||
</tspan>
|
||||
</text>
|
||||
</svg>
|
||||
</g>
|
||||
<g
|
||||
class="visx-group visx-axis-tick"
|
||||
transform="translate(0, 0)"
|
||||
>
|
||||
<svg
|
||||
font-size="10"
|
||||
style="overflow: visible;"
|
||||
x="0"
|
||||
y="0.25em"
|
||||
>
|
||||
<text
|
||||
fill="#222"
|
||||
font-family="Arial"
|
||||
font-size="10"
|
||||
text-anchor="middle"
|
||||
transform=""
|
||||
x="353.4375"
|
||||
y="18"
|
||||
>
|
||||
<tspan
|
||||
dy="0em"
|
||||
x="353.4375"
|
||||
>
|
||||
1,694,538,865
|
||||
</tspan>
|
||||
</text>
|
||||
</svg>
|
||||
</g>
|
||||
<g
|
||||
class="visx-group visx-axis-tick"
|
||||
transform="translate(0, 0)"
|
||||
>
|
||||
<svg
|
||||
font-size="10"
|
||||
style="overflow: visible;"
|
||||
x="0"
|
||||
y="0.25em"
|
||||
>
|
||||
<text
|
||||
fill="#222"
|
||||
font-family="Arial"
|
||||
font-size="10"
|
||||
text-anchor="middle"
|
||||
transform=""
|
||||
x="414.375"
|
||||
y="18"
|
||||
>
|
||||
<tspan
|
||||
dy="0em"
|
||||
x="414.375"
|
||||
>
|
||||
1,694,538,870
|
||||
</tspan>
|
||||
</text>
|
||||
</svg>
|
||||
</g>
|
||||
<g
|
||||
class="visx-group visx-axis-tick"
|
||||
transform="translate(0, 0)"
|
||||
>
|
||||
<svg
|
||||
font-size="10"
|
||||
style="overflow: visible;"
|
||||
x="0"
|
||||
y="0.25em"
|
||||
>
|
||||
<text
|
||||
fill="#222"
|
||||
font-family="Arial"
|
||||
font-size="10"
|
||||
text-anchor="middle"
|
||||
transform=""
|
||||
x="475.3125"
|
||||
y="18"
|
||||
>
|
||||
<tspan
|
||||
dy="0em"
|
||||
x="475.3125"
|
||||
>
|
||||
1,694,538,875
|
||||
</tspan>
|
||||
</text>
|
||||
</svg>
|
||||
</g>
|
||||
<g
|
||||
class="visx-group visx-axis-tick"
|
||||
transform="translate(0, 0)"
|
||||
>
|
||||
<svg
|
||||
font-size="10"
|
||||
style="overflow: visible;"
|
||||
x="0"
|
||||
y="0.25em"
|
||||
>
|
||||
<text
|
||||
fill="#222"
|
||||
font-family="Arial"
|
||||
font-size="10"
|
||||
text-anchor="middle"
|
||||
transform=""
|
||||
x="536.25"
|
||||
y="18"
|
||||
>
|
||||
<tspan
|
||||
dy="0em"
|
||||
x="536.25"
|
||||
>
|
||||
1,694,538,880
|
||||
</tspan>
|
||||
</text>
|
||||
</svg>
|
||||
</g>
|
||||
<g
|
||||
class="visx-group visx-axis-tick"
|
||||
transform="translate(0, 0)"
|
||||
>
|
||||
<svg
|
||||
font-size="10"
|
||||
style="overflow: visible;"
|
||||
x="0"
|
||||
y="0.25em"
|
||||
>
|
||||
<text
|
||||
fill="#222"
|
||||
font-family="Arial"
|
||||
font-size="10"
|
||||
text-anchor="middle"
|
||||
transform=""
|
||||
x="597.1875"
|
||||
y="18"
|
||||
>
|
||||
<tspan
|
||||
dy="0em"
|
||||
x="597.1875"
|
||||
>
|
||||
1,694,538,885
|
||||
</tspan>
|
||||
</text>
|
||||
</svg>
|
||||
</g>
|
||||
<g
|
||||
class="visx-group visx-axis-tick"
|
||||
transform="translate(0, 0)"
|
||||
>
|
||||
<svg
|
||||
font-size="10"
|
||||
style="overflow: visible;"
|
||||
x="0"
|
||||
y="0.25em"
|
||||
>
|
||||
<text
|
||||
fill="#222"
|
||||
font-family="Arial"
|
||||
font-size="10"
|
||||
text-anchor="middle"
|
||||
transform=""
|
||||
x="658.125"
|
||||
y="18"
|
||||
>
|
||||
<tspan
|
||||
dy="0em"
|
||||
x="658.125"
|
||||
>
|
||||
1,694,538,890
|
||||
</tspan>
|
||||
</text>
|
||||
</svg>
|
||||
</g>
|
||||
<g
|
||||
class="visx-group visx-axis-tick"
|
||||
transform="translate(0, 0)"
|
||||
>
|
||||
<svg
|
||||
font-size="10"
|
||||
style="overflow: visible;"
|
||||
x="0"
|
||||
y="0.25em"
|
||||
>
|
||||
<text
|
||||
fill="#222"
|
||||
font-family="Arial"
|
||||
font-size="10"
|
||||
text-anchor="middle"
|
||||
transform=""
|
||||
x="719.0625"
|
||||
y="18"
|
||||
>
|
||||
<tspan
|
||||
dy="0em"
|
||||
x="719.0625"
|
||||
>
|
||||
1,694,538,895
|
||||
</tspan>
|
||||
</text>
|
||||
</svg>
|
||||
</g>
|
||||
<g
|
||||
class="visx-group visx-axis-tick"
|
||||
transform="translate(0, 0)"
|
||||
>
|
||||
<svg
|
||||
font-size="10"
|
||||
style="overflow: visible;"
|
||||
x="0"
|
||||
y="0.25em"
|
||||
>
|
||||
<text
|
||||
fill="#222"
|
||||
font-family="Arial"
|
||||
font-size="10"
|
||||
text-anchor="middle"
|
||||
transform=""
|
||||
x="780"
|
||||
y="18"
|
||||
>
|
||||
<tspan
|
||||
dy="0em"
|
||||
x="780"
|
||||
>
|
||||
1,694,538,900
|
||||
</tspan>
|
||||
</text>
|
||||
</svg>
|
||||
</g>
|
||||
</g>
|
||||
<rect
|
||||
fill="transparent"
|
||||
height="392"
|
||||
width="780"
|
||||
x="0"
|
||||
y="0"
|
||||
/>
|
||||
</svg>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
74
src/components/Charts/PriceChart/index.test.tsx
Normal file
@@ -0,0 +1,74 @@
|
||||
import { TimePeriod } from 'graphql/data/util'
|
||||
import { render, screen } from 'test-utils/render'
|
||||
|
||||
import { PriceChart } from '.'
|
||||
|
||||
jest.mock('components/Charts/AnimatedInLineChart', () => ({
|
||||
__esModule: true,
|
||||
default: jest.fn(() => null),
|
||||
}))
|
||||
jest.mock('components/Charts/FadeInLineChart', () => ({
|
||||
__esModule: true,
|
||||
default: jest.fn(() => null),
|
||||
}))
|
||||
|
||||
describe('PriceChart', () => {
|
||||
it('renders correctly with all prices filled', () => {
|
||||
const mockPrices = Array.from({ length: 13 }, (_, i) => ({
|
||||
value: 1,
|
||||
timestamp: i * 3600,
|
||||
}))
|
||||
|
||||
const { asFragment } = render(
|
||||
<PriceChart prices={mockPrices} width={780} height={392} timePeriod={TimePeriod.HOUR} />
|
||||
)
|
||||
expect(asFragment()).toMatchSnapshot()
|
||||
expect(asFragment().textContent).toContain('$1.00')
|
||||
expect(asFragment().textContent).toContain('0.00%')
|
||||
})
|
||||
it('renders correctly with some prices filled', () => {
|
||||
const mockPrices = Array.from({ length: 13 }, (_, i) => ({
|
||||
value: i < 10 ? 1 : 0,
|
||||
timestamp: i * 3600,
|
||||
}))
|
||||
|
||||
const { asFragment } = render(
|
||||
<PriceChart prices={mockPrices} width={780} height={392} timePeriod={TimePeriod.HOUR} />
|
||||
)
|
||||
expect(asFragment()).toMatchSnapshot()
|
||||
expect(asFragment().textContent).toContain('$1.00')
|
||||
expect(asFragment().textContent).toContain('0.00%')
|
||||
})
|
||||
it('renders correctly with empty price array', () => {
|
||||
const { asFragment } = render(<PriceChart prices={[]} width={780} height={392} timePeriod={TimePeriod.HOUR} />)
|
||||
expect(asFragment()).toMatchSnapshot()
|
||||
expect(asFragment().textContent).toContain('Price unavailable')
|
||||
expect(asFragment().textContent).toContain('Missing price data due to recently low trading volume on Uniswap v3')
|
||||
})
|
||||
it('renders correctly with undefined prices', () => {
|
||||
const { asFragment } = render(
|
||||
<PriceChart prices={undefined} width={780} height={392} timePeriod={TimePeriod.HOUR} />
|
||||
)
|
||||
expect(asFragment()).toMatchSnapshot()
|
||||
expect(asFragment().textContent).toContain('Price unavailable')
|
||||
expect(asFragment().textContent).toContain('Missing chart data')
|
||||
})
|
||||
it('renders stale UI', () => {
|
||||
const { asFragment } = render(
|
||||
<PriceChart
|
||||
prices={[
|
||||
{ value: 1, timestamp: 1694538836 },
|
||||
{ value: 1, timestamp: 1694538840 },
|
||||
{ value: 1, timestamp: 1694538844 },
|
||||
{ value: 0, timestamp: 1694538900 },
|
||||
]}
|
||||
width={780}
|
||||
height={392}
|
||||
timePeriod={TimePeriod.HOUR}
|
||||
/>
|
||||
)
|
||||
expect(asFragment()).toMatchSnapshot()
|
||||
expect(asFragment().textContent).toContain('$1.00')
|
||||
expect(screen.getByTestId('chart-stale-icon')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
308
src/components/Charts/PriceChart/index.tsx
Normal file
@@ -0,0 +1,308 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import { AxisBottom } from '@visx/axis'
|
||||
import { localPoint } from '@visx/event'
|
||||
import { EventType } from '@visx/event/lib/types'
|
||||
import { GlyphCircle } from '@visx/glyph'
|
||||
import { Line } from '@visx/shape'
|
||||
import AnimatedInLineChart from 'components/Charts/AnimatedInLineChart'
|
||||
import FadedInLineChart from 'components/Charts/FadeInLineChart'
|
||||
import { buildChartModel, ChartErrorType, ChartModel, ErroredChartModel } from 'components/Charts/PriceChart/ChartModel'
|
||||
import { getTimestampFormatter, TimestampFormatterType } from 'components/Charts/PriceChart/format'
|
||||
import { getNearestPricePoint, getTicks } from 'components/Charts/PriceChart/utils'
|
||||
import { MouseoverTooltip } from 'components/Tooltip'
|
||||
import { curveCardinal } from 'd3'
|
||||
import { PricePoint, TimePeriod } from 'graphql/data/util'
|
||||
import { useActiveLocale } from 'hooks/useActiveLocale'
|
||||
import { ReactNode, useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import { Info } from 'react-feather'
|
||||
import styled, { useTheme } from 'styled-components'
|
||||
import { ThemedText } from 'theme/components'
|
||||
import { textFadeIn } from 'theme/styles'
|
||||
import { useFormatter } from 'utils/formatNumbers'
|
||||
|
||||
import { calculateDelta, DeltaArrow } from '../../Tokens/TokenDetails/Delta'
|
||||
|
||||
const CHART_MARGIN = { top: 100, bottom: 48, crosshair: 72 }
|
||||
|
||||
const ChartHeaderWrapper = styled.div<{ stale?: boolean }>`
|
||||
position: absolute;
|
||||
${textFadeIn};
|
||||
animation-duration: ${({ theme }) => theme.transition.duration.medium};
|
||||
${({ theme, stale }) => stale && `color: ${theme.neutral2}`};
|
||||
`
|
||||
const PriceContainer = styled.div`
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
font-size: 24px;
|
||||
line-height: 44px;
|
||||
`
|
||||
const DeltaContainer = styled.div`
|
||||
height: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-top: 4px;
|
||||
color: ${({ theme }) => theme.neutral2};
|
||||
`
|
||||
|
||||
interface ChartDeltaProps {
|
||||
startingPrice: PricePoint
|
||||
endingPrice: PricePoint
|
||||
noColor?: boolean
|
||||
}
|
||||
|
||||
function ChartDelta({ startingPrice, endingPrice, noColor }: ChartDeltaProps) {
|
||||
const delta = calculateDelta(startingPrice.value, endingPrice.value)
|
||||
const { formatPercent } = useFormatter()
|
||||
|
||||
return (
|
||||
<DeltaContainer>
|
||||
{formatPercent(delta)}
|
||||
<DeltaArrow delta={delta} noColor={noColor} />
|
||||
</DeltaContainer>
|
||||
)
|
||||
}
|
||||
|
||||
interface ChartHeaderProps {
|
||||
crosshairPrice?: PricePoint
|
||||
chart: ChartModel
|
||||
}
|
||||
|
||||
function ChartHeader({ crosshairPrice, chart }: ChartHeaderProps) {
|
||||
const { formatFiatPrice } = useFormatter()
|
||||
|
||||
const { startingPrice, endingPrice, lastValidPrice } = chart
|
||||
|
||||
const priceOutdated = lastValidPrice !== endingPrice
|
||||
const displayPrice = crosshairPrice ?? (priceOutdated ? lastValidPrice : endingPrice)
|
||||
|
||||
const displayIsStale = priceOutdated && !crosshairPrice
|
||||
return (
|
||||
<ChartHeaderWrapper data-cy="chart-header" stale={displayIsStale}>
|
||||
<PriceContainer>
|
||||
<ThemedText.HeadlineLarge color="inherit">
|
||||
{formatFiatPrice({ price: displayPrice.value })}
|
||||
</ThemedText.HeadlineLarge>
|
||||
{displayIsStale && (
|
||||
<MouseoverTooltip text={<Trans>This price may not be up-to-date due to low trading volume.</Trans>}>
|
||||
<Info size={16} data-testid="chart-stale-icon" />
|
||||
</MouseoverTooltip>
|
||||
)}
|
||||
</PriceContainer>
|
||||
<ChartDelta startingPrice={startingPrice} endingPrice={displayPrice} noColor={priceOutdated} />
|
||||
</ChartHeaderWrapper>
|
||||
)
|
||||
}
|
||||
|
||||
function ChartBody({ chart, timePeriod }: { chart: ChartModel; timePeriod: TimePeriod }) {
|
||||
const locale = useActiveLocale()
|
||||
|
||||
const { prices, blanks, timeScale, priceScale, dimensions } = chart
|
||||
|
||||
const { ticks, tickTimestampFormatter, crosshairTimestampFormatter } = useMemo(() => {
|
||||
// Limits the number of ticks based on graph width
|
||||
const maxTicks = Math.floor(dimensions.width / 100)
|
||||
|
||||
const ticks = getTicks(chart.startingPrice.timestamp, chart.endingPrice.timestamp, timePeriod, maxTicks)
|
||||
const tickTimestampFormatter = getTimestampFormatter(timePeriod, locale, TimestampFormatterType.TICK)
|
||||
const crosshairTimestampFormatter = getTimestampFormatter(timePeriod, locale, TimestampFormatterType.CROSSHAIR)
|
||||
|
||||
return { ticks, tickTimestampFormatter, crosshairTimestampFormatter }
|
||||
}, [dimensions.width, chart.startingPrice.timestamp, chart.endingPrice.timestamp, timePeriod, locale])
|
||||
|
||||
const theme = useTheme()
|
||||
const [crosshair, setCrosshair] = useState<{ x: number; y: number; price: PricePoint }>()
|
||||
const resetCrosshair = useCallback(() => setCrosshair(undefined), [setCrosshair])
|
||||
|
||||
const setCrosshairOnHover = useCallback(
|
||||
(event: Element | EventType) => {
|
||||
const { x } = localPoint(event) || { x: 0 }
|
||||
const price = getNearestPricePoint(x, prices, timeScale)
|
||||
|
||||
if (price) {
|
||||
const x = timeScale(price.timestamp)
|
||||
const y = priceScale(price.value)
|
||||
setCrosshair({ x, y, price })
|
||||
}
|
||||
},
|
||||
[priceScale, timeScale, prices]
|
||||
)
|
||||
|
||||
// Resets the crosshair when the time period is changed, to avoid stale UI
|
||||
useEffect(() => resetCrosshair(), [resetCrosshair, timePeriod])
|
||||
|
||||
const crosshairEdgeMax = dimensions.width * 0.85
|
||||
const crosshairAtEdge = !!crosshair && crosshair.x > crosshairEdgeMax
|
||||
|
||||
// Default curve doesn't look good for the HOUR chart.
|
||||
// Higher values make the curve more rigid, lower values smooth the curve but make it less "sticky" to real data points,
|
||||
// making it unacceptable for shorter durations / smaller variances.
|
||||
const curveTension = timePeriod === TimePeriod.HOUR ? 1 : 0.9
|
||||
|
||||
const getX = useCallback((p: PricePoint) => timeScale(p.timestamp), [timeScale])
|
||||
const getY = useCallback((p: PricePoint) => priceScale(p.value), [priceScale])
|
||||
const curve = useMemo(() => curveCardinal.tension(curveTension), [curveTension])
|
||||
|
||||
return (
|
||||
<>
|
||||
<ChartHeader chart={chart} crosshairPrice={crosshair?.price} />
|
||||
<svg data-cy="price-chart" width={dimensions.width} height={dimensions.height} style={{ minWidth: '100%' }}>
|
||||
<AnimatedInLineChart
|
||||
data={prices}
|
||||
getX={getX}
|
||||
getY={getY}
|
||||
marginTop={dimensions.marginTop}
|
||||
curve={curve}
|
||||
strokeWidth={2}
|
||||
/>
|
||||
{blanks.map((blank, index) => (
|
||||
<FadedInLineChart
|
||||
key={index}
|
||||
data={blank}
|
||||
getX={getX}
|
||||
getY={getY}
|
||||
marginTop={dimensions.marginTop}
|
||||
curve={curve}
|
||||
strokeWidth={2}
|
||||
color={theme.neutral3}
|
||||
dashed
|
||||
/>
|
||||
))}
|
||||
{crosshair !== undefined ? (
|
||||
<g>
|
||||
<AxisBottom
|
||||
top={dimensions.height - 1}
|
||||
scale={timeScale}
|
||||
stroke={theme.surface3}
|
||||
hideTicks={true}
|
||||
tickValues={ticks}
|
||||
tickFormat={tickTimestampFormatter}
|
||||
tickLabelProps={() => ({
|
||||
fill: theme.neutral2,
|
||||
fontSize: 12,
|
||||
textAnchor: 'middle',
|
||||
transform: 'translate(0 -29)',
|
||||
})}
|
||||
/>
|
||||
<text
|
||||
x={crosshair.x + (crosshairAtEdge ? -4 : 4)}
|
||||
y={CHART_MARGIN.crosshair + 10}
|
||||
textAnchor={crosshairAtEdge ? 'end' : 'start'}
|
||||
fontSize={12}
|
||||
fill={theme.neutral2}
|
||||
>
|
||||
{crosshairTimestampFormatter(crosshair.price.timestamp)}
|
||||
</text>
|
||||
<Line
|
||||
from={{ x: crosshair.x, y: CHART_MARGIN.crosshair }}
|
||||
to={{ x: crosshair.x, y: dimensions.height }}
|
||||
stroke={theme.surface3}
|
||||
strokeWidth={1}
|
||||
pointerEvents="none"
|
||||
strokeDasharray="4,4"
|
||||
/>
|
||||
<GlyphCircle
|
||||
left={crosshair.x}
|
||||
top={crosshair.y + dimensions.marginTop}
|
||||
size={50}
|
||||
fill={theme.accent1}
|
||||
stroke={theme.surface3}
|
||||
strokeWidth={0.5}
|
||||
/>
|
||||
</g>
|
||||
) : (
|
||||
<AxisBottom
|
||||
hideAxisLine={true}
|
||||
scale={timeScale}
|
||||
stroke={theme.surface3}
|
||||
top={dimensions.height - 1}
|
||||
hideTicks
|
||||
/>
|
||||
)}
|
||||
{!dimensions.width && (
|
||||
// Ensures an axis is drawn even if the width is not yet initialized.
|
||||
<line
|
||||
x1={0}
|
||||
y1={dimensions.height - 1}
|
||||
x2="100%"
|
||||
y2={dimensions.height - 1}
|
||||
fill="transparent"
|
||||
shapeRendering="crispEdges"
|
||||
stroke={theme.surface3}
|
||||
strokeWidth={1}
|
||||
/>
|
||||
)}
|
||||
<rect
|
||||
x={0}
|
||||
y={0}
|
||||
width={dimensions.width}
|
||||
height={dimensions.height}
|
||||
fill="transparent"
|
||||
onTouchStart={setCrosshairOnHover}
|
||||
onTouchMove={setCrosshairOnHover}
|
||||
onMouseMove={setCrosshairOnHover}
|
||||
onMouseLeave={resetCrosshair}
|
||||
/>
|
||||
</svg>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const CHART_ERROR_MESSAGES: Record<ChartErrorType, ReactNode> = {
|
||||
[ChartErrorType.NO_DATA_AVAILABLE]: <Trans>Missing chart data</Trans>,
|
||||
[ChartErrorType.NO_RECENT_VOLUME]: <Trans>Missing price data due to recently low trading volume on Uniswap v3</Trans>,
|
||||
[ChartErrorType.INVALID_CHART]: <Trans>Invalid chart</Trans>,
|
||||
}
|
||||
|
||||
function MissingPriceChart({ chart }: { chart: ErroredChartModel }) {
|
||||
const theme = useTheme()
|
||||
const midPoint = chart.dimensions.height / 2 + 45
|
||||
|
||||
return (
|
||||
<>
|
||||
<ChartHeaderWrapper data-cy="chart-header">
|
||||
<ThemedText.HeadlineLarge fontSize={24} color="neutral3">
|
||||
Price unavailable
|
||||
</ThemedText.HeadlineLarge>
|
||||
<ThemedText.BodySmall color="neutral3">{CHART_ERROR_MESSAGES[chart.error]}</ThemedText.BodySmall>
|
||||
</ChartHeaderWrapper>
|
||||
<svg
|
||||
data-cy="missing-chart"
|
||||
width={chart.dimensions.width}
|
||||
height={chart.dimensions.height}
|
||||
style={{ minWidth: '100%' }}
|
||||
>
|
||||
<path
|
||||
d={`M 0 ${midPoint} Q 104 ${midPoint - 70}, 208 ${midPoint} T 416 ${midPoint}
|
||||
M 416 ${midPoint} Q 520 ${midPoint - 70}, 624 ${midPoint} T 832 ${midPoint}`}
|
||||
stroke={theme.surface3}
|
||||
fill="transparent"
|
||||
strokeWidth="2"
|
||||
/>
|
||||
</svg>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
interface PriceChartProps {
|
||||
width: number
|
||||
height: number
|
||||
prices?: PricePoint[]
|
||||
timePeriod: TimePeriod
|
||||
}
|
||||
|
||||
export function PriceChart({ width, height, prices, timePeriod }: PriceChartProps) {
|
||||
const chart = useMemo(
|
||||
() =>
|
||||
buildChartModel({
|
||||
dimensions: { width, height, marginBottom: CHART_MARGIN.bottom, marginTop: CHART_MARGIN.top },
|
||||
prices,
|
||||
}),
|
||||
[width, height, prices]
|
||||
)
|
||||
|
||||
if (chart.error !== undefined) {
|
||||
return <MissingPriceChart chart={chart} />
|
||||
}
|
||||
|
||||
return <ChartBody chart={chart} timePeriod={timePeriod} />
|
||||
}
|
||||
@@ -2,9 +2,9 @@ import { Trans } from '@lingui/macro'
|
||||
import Column from 'components/Column'
|
||||
import { BlockedIcon } from 'components/TokenSafety/TokenSafetyIcon'
|
||||
import styled, { useTheme } from 'styled-components'
|
||||
import { ExternalLink, ThemedText } from 'theme'
|
||||
import { ExternalLink, ThemedText } from 'theme/components'
|
||||
import { CopyHelper } from 'theme/components'
|
||||
|
||||
import { CopyHelper } from '../../theme'
|
||||
import Modal from '../Modal'
|
||||
|
||||
const ContentWrapper = styled(Column)`
|
||||
@@ -25,7 +25,7 @@ export default function ConnectedAccountBlocked(props: ConnectedAccountBlockedPr
|
||||
<ContentWrapper>
|
||||
<BlockedIcon size="22px" />
|
||||
<ThemedText.DeprecatedLargeHeader lineHeight={2} marginBottom={1} marginTop={1}>
|
||||
<Trans>Blocked Address</Trans>
|
||||
<Trans>Blocked address</Trans>
|
||||
</ThemedText.DeprecatedLargeHeader>
|
||||
<ThemedText.DeprecatedDarkGray fontSize={12} marginBottom={12}>
|
||||
{props.account}
|
||||
|
||||
@@ -5,7 +5,7 @@ import { LoadingBubble } from 'components/Tokens/loading'
|
||||
import { MouseoverTooltip } from 'components/Tooltip'
|
||||
import { useMemo } from 'react'
|
||||
import styled from 'styled-components'
|
||||
import { ThemedText } from 'theme'
|
||||
import { ThemedText } from 'theme/components'
|
||||
import { NumberType, useFormatter } from 'utils/formatNumbers'
|
||||
import { warningSeverity } from 'utils/prices'
|
||||
|
||||
|
||||
@@ -4,10 +4,10 @@ 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'
|
||||
import PrefetchBalancesWrapper from 'components/PrefetchBalancesWrapper/PrefetchBalancesWrapper'
|
||||
import Tooltip from 'components/Tooltip'
|
||||
import { isSupportedChain } from 'constants/chains'
|
||||
import ms from 'ms'
|
||||
@@ -15,12 +15,12 @@ import { darken } from 'polished'
|
||||
import { forwardRef, ReactNode, useCallback, useEffect, useState } from 'react'
|
||||
import { Lock } from 'react-feather'
|
||||
import styled, { useTheme } from 'styled-components'
|
||||
import { ThemedText } from 'theme/components'
|
||||
import { flexColumnNoWrap, flexRowNoWrap } from 'theme/styles'
|
||||
import { NumberType, useFormatter } from 'utils/formatNumbers'
|
||||
|
||||
import { ReactComponent as DropDown } from '../../assets/images/dropdown.svg'
|
||||
import { useCurrencyBalance } from '../../state/connection/hooks'
|
||||
import { ThemedText } from '../../theme'
|
||||
import { ButtonGray } from '../Button'
|
||||
import DoubleCurrencyLogo from '../DoubleLogo'
|
||||
import { Input as NumericalInput } from '../NumericalInput'
|
||||
@@ -67,8 +67,8 @@ const CurrencySelect = styled(ButtonGray)<{
|
||||
opacity: ${({ disabled }) => (!disabled ? 1 : 0.4)};
|
||||
color: ${({ selected, theme }) => (selected ? theme.neutral1 : theme.white)};
|
||||
cursor: pointer;
|
||||
height: unset;
|
||||
border-radius: 16px;
|
||||
height: 36px;
|
||||
border-radius: 18px;
|
||||
outline: none;
|
||||
user-select: none;
|
||||
border: 1px solid ${({ selected, theme }) => (selected ? theme.surface3 : theme.accent1)};
|
||||
|
||||
@@ -5,16 +5,17 @@ import { Pair } from '@uniswap/v2-sdk'
|
||||
import { useWeb3React } from '@web3-react/core'
|
||||
import { TraceEvent } from 'analytics'
|
||||
import { LoadingOpacityContainer, loadingOpacityMixin } from 'components/Loader/styled'
|
||||
import PrefetchBalancesWrapper from 'components/PrefetchBalancesWrapper/PrefetchBalancesWrapper'
|
||||
import { isSupportedChain } from 'constants/chains'
|
||||
import { darken } from 'polished'
|
||||
import { ReactNode, useCallback, useState } from 'react'
|
||||
import styled, { useTheme } from 'styled-components'
|
||||
import { ThemedText } from 'theme/components'
|
||||
import { flexColumnNoWrap, flexRowNoWrap } from 'theme/styles'
|
||||
import { formatCurrencyAmount } from 'utils/formatCurrencyAmount'
|
||||
|
||||
import { ReactComponent as DropDown } from '../../assets/images/dropdown.svg'
|
||||
import { useCurrencyBalance } from '../../state/connection/hooks'
|
||||
import { ThemedText } from '../../theme'
|
||||
import { ButtonGray } from '../Button'
|
||||
import DoubleCurrencyLogo from '../DoubleLogo'
|
||||
import CurrencyLogo from '../Logo/CurrencyLogo'
|
||||
@@ -160,6 +161,10 @@ const StyledNumericalInput = styled(NumericalInput)<{ $loading: boolean }>`
|
||||
text-align: left;
|
||||
`
|
||||
|
||||
const StyledPrefetchBalancesWrapper = styled(PrefetchBalancesWrapper)<{ $fullWidth: boolean }>`
|
||||
width: ${({ $fullWidth }) => ($fullWidth ? '100%' : 'auto')};
|
||||
`
|
||||
|
||||
interface CurrencyInputPanelProps {
|
||||
value: string
|
||||
onUserInput: (value: string) => void
|
||||
@@ -230,45 +235,50 @@ export default function CurrencyInputPanel({
|
||||
/>
|
||||
)}
|
||||
|
||||
<CurrencySelect
|
||||
disabled={!chainAllowed}
|
||||
visible={currency !== undefined}
|
||||
selected={!!currency}
|
||||
hideInput={hideInput}
|
||||
className="open-currency-select-button"
|
||||
onClick={() => {
|
||||
if (onCurrencySelect) {
|
||||
setModalOpen(true)
|
||||
}
|
||||
}}
|
||||
pointerEvents={!onCurrencySelect ? 'none' : undefined}
|
||||
>
|
||||
<Aligner>
|
||||
<RowFixed>
|
||||
{pair ? (
|
||||
<span style={{ marginRight: '0.5rem' }}>
|
||||
<DoubleCurrencyLogo currency0={pair.token0} currency1={pair.token1} size={24} margin={true} />
|
||||
</span>
|
||||
) : (
|
||||
currency && <CurrencyLogo style={{ marginRight: '0.5rem' }} currency={currency} size="24px" />
|
||||
)}
|
||||
{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 a token</Trans>}
|
||||
</StyledTokenName>
|
||||
)}
|
||||
</RowFixed>
|
||||
{onCurrencySelect && <StyledDropDown selected={!!currency} />}
|
||||
</Aligner>
|
||||
</CurrencySelect>
|
||||
<StyledPrefetchBalancesWrapper shouldFetchOnAccountUpdate={modalOpen} $fullWidth={hideInput}>
|
||||
<CurrencySelect
|
||||
disabled={!chainAllowed}
|
||||
visible={currency !== undefined}
|
||||
selected={!!currency}
|
||||
hideInput={hideInput}
|
||||
className="open-currency-select-button"
|
||||
onClick={() => {
|
||||
if (onCurrencySelect) {
|
||||
setModalOpen(true)
|
||||
}
|
||||
}}
|
||||
pointerEvents={!onCurrencySelect ? 'none' : undefined}
|
||||
>
|
||||
<Aligner>
|
||||
<RowFixed>
|
||||
{pair ? (
|
||||
<span style={{ marginRight: '0.5rem' }}>
|
||||
<DoubleCurrencyLogo currency0={pair.token0} currency1={pair.token1} size={24} margin={true} />
|
||||
</span>
|
||||
) : (
|
||||
currency && <CurrencyLogo style={{ marginRight: '0.5rem' }} currency={currency} size="24px" />
|
||||
)}
|
||||
{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 a token</Trans>}
|
||||
</StyledTokenName>
|
||||
)}
|
||||
</RowFixed>
|
||||
{onCurrencySelect && <StyledDropDown selected={!!currency} />}
|
||||
</Aligner>
|
||||
</CurrencySelect>
|
||||
</StyledPrefetchBalancesWrapper>
|
||||
</InputRow>
|
||||
{Boolean(!hideInput && !hideBalance && currency) && (
|
||||
<FiatRow>
|
||||
|
||||
@@ -7,9 +7,9 @@ import { useIsMobile } from 'nft/hooks'
|
||||
import React, { PropsWithChildren, useState } from 'react'
|
||||
import { Copy } from 'react-feather'
|
||||
import styled from 'styled-components'
|
||||
import { CopyToClipboard, ExternalLink, ThemedText } from 'theme/components'
|
||||
import { isSentryEnabled } from 'utils/env'
|
||||
|
||||
import { CopyToClipboard, ExternalLink, ThemedText } from '../../theme'
|
||||
import { Column } from '../Column'
|
||||
|
||||
const FallbackWrapper = styled.div`
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import Column from 'components/Column'
|
||||
import { BaseVariant, FeatureFlag, featureFlagSettings, useUpdateFlag } from 'featureFlags'
|
||||
import { useCurrencyConversionFlag } from 'featureFlags/flags/currencyConversion'
|
||||
import { useFallbackProviderEnabledFlag } from 'featureFlags/flags/fallbackProvider'
|
||||
import { useFotAdjustmentsFlag } from 'featureFlags/flags/fotAdjustments'
|
||||
import { useInfoExploreFlag } from 'featureFlags/flags/infoExplore'
|
||||
import { useInfoLiveViewsFlag } from 'featureFlags/flags/infoLiveViews'
|
||||
import { useInfoPoolPageFlag } from 'featureFlags/flags/infoPoolPage'
|
||||
import { useInfoTDPFlag } from 'featureFlags/flags/infoTDP'
|
||||
import { useMultichainUXFlag } from 'featureFlags/flags/multichainUx'
|
||||
import { useQuickRouteMainnetFlag } from 'featureFlags/flags/quickRouteMainnet'
|
||||
import { TraceJsonRpcVariant, useTraceJsonRpcFlag } from 'featureFlags/flags/traceJsonRpc'
|
||||
import { useUniswapXDefaultEnabledFlag } from 'featureFlags/flags/uniswapXDefault'
|
||||
import { useUniswapXEthOutputFlag } from 'featureFlags/flags/uniswapXEthOutput'
|
||||
@@ -229,6 +231,12 @@ export default function FeatureFlagModal() {
|
||||
<X size={24} />
|
||||
</CloseButton>
|
||||
</Header>
|
||||
<FeatureFlagOption
|
||||
variant={BaseVariant}
|
||||
value={useFallbackProviderEnabledFlag()}
|
||||
featureFlag={FeatureFlag.fallbackProvider}
|
||||
label="Enable fallback provider"
|
||||
/>
|
||||
<FeatureFlagOption
|
||||
variant={BaseVariant}
|
||||
value={useCurrencyConversionFlag()}
|
||||
@@ -247,6 +255,14 @@ export default function FeatureFlagModal() {
|
||||
featureFlag={FeatureFlag.fotAdjustedmentsEnabled}
|
||||
label="Enable fee-on-transfer UI and slippage adjustments"
|
||||
/>
|
||||
<FeatureFlagGroup name="Quick routes">
|
||||
<FeatureFlagOption
|
||||
variant={BaseVariant}
|
||||
value={useQuickRouteMainnetFlag()}
|
||||
featureFlag={FeatureFlag.quickRouteMainnet}
|
||||
label="Enable quick routes for Mainnet"
|
||||
/>
|
||||
</FeatureFlagGroup>
|
||||
<FeatureFlagGroup name="UniswapX Flags">
|
||||
<FeatureFlagOption
|
||||
variant={BaseVariant}
|
||||
|
||||
@@ -6,7 +6,7 @@ import { useFeeTierDistribution } from 'hooks/useFeeTierDistribution'
|
||||
import { PoolState } from 'hooks/usePools'
|
||||
import React from 'react'
|
||||
import styled from 'styled-components'
|
||||
import { ThemedText } from 'theme'
|
||||
import { ThemedText } from 'theme/components'
|
||||
|
||||
import { FeeTierPercentageBadge } from './FeeTierPercentageBadge'
|
||||
import { FEE_AMOUNT_DETAIL } from './shared'
|
||||
|
||||
@@ -4,7 +4,7 @@ import Badge from 'components/Badge'
|
||||
import { useFeeTierDistribution } from 'hooks/useFeeTierDistribution'
|
||||
import { PoolState } from 'hooks/usePools'
|
||||
import React from 'react'
|
||||
import { ThemedText } from 'theme'
|
||||
import { ThemedText } from 'theme/components'
|
||||
|
||||
export function FeeTierPercentageBadge({
|
||||
feeAmount,
|
||||
|
||||
@@ -15,7 +15,7 @@ import { DynamicSection } from 'pages/AddLiquidity/styled'
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { Box } from 'rebass'
|
||||
import styled, { keyframes } from 'styled-components'
|
||||
import { ThemedText } from 'theme'
|
||||
import { ThemedText } from 'theme/components'
|
||||
|
||||
import { FeeOption } from './FeeOption'
|
||||
import { FeeTierPercentageBadge } from './FeeTierPercentageBadge'
|
||||
|
||||
17
src/components/FiatOnrampModal/constants.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
export const MOONPAY_SUPPORTED_CURRENCY_CODES = [
|
||||
'eth',
|
||||
'eth_arbitrum',
|
||||
'eth_optimism',
|
||||
'eth_polygon',
|
||||
'weth',
|
||||
'wbtc',
|
||||
'matic_polygon',
|
||||
'polygon',
|
||||
'usdc_arbitrum',
|
||||
'usdc_optimism',
|
||||
'usdc_polygon',
|
||||
'usdc',
|
||||
'usdt',
|
||||
] as const
|
||||
|
||||
export type MoonpaySupportedCurrencyCode = (typeof MOONPAY_SUPPORTED_CURRENCY_CODES)[number]
|
||||