Compare commits
25 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
95b9624bca | ||
|
|
04a0479236 | ||
|
|
55f1e35ffc | ||
|
|
01a3aa1c92 | ||
|
|
7121b4aa1c | ||
|
|
a538bf0b69 | ||
|
|
135cb8fb34 | ||
|
|
bf50582d38 | ||
|
|
110e23d6eb | ||
|
|
7ab6a17b42 | ||
|
|
7ad13c96a8 | ||
|
|
4e99cc4d93 | ||
|
|
6d29815f59 | ||
|
|
4888fe23df | ||
|
|
ef9ecd9ce2 | ||
|
|
f5d0804c46 | ||
|
|
0bac257254 | ||
|
|
a77752ab83 | ||
|
|
bf31ca4f06 | ||
|
|
ed8afbd851 | ||
|
|
47b6a7c4d5 | ||
|
|
086fc65457 | ||
|
|
7df53f30a0 | ||
|
|
66497a0108 | ||
|
|
e0eb701bc0 |
@@ -5,6 +5,14 @@ require('@uniswap/eslint-config/load')
|
||||
module.exports = {
|
||||
extends: '@uniswap/eslint-config/react',
|
||||
overrides: [
|
||||
{
|
||||
// Configuration/typings typically export objects/definitions that are used outside of the transpiled package
|
||||
// (eg not captured by the tsconfig). Because it's typical and not exceptional, this is turned off entirely.
|
||||
files: ['**/*.config.*', '**/*.d.ts'],
|
||||
rules: {
|
||||
'import/no-unused-modules': 'off',
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['**/*.ts', '**/*.tsx'],
|
||||
rules: {
|
||||
|
||||
8
.github/actions/setup/action.yml
vendored
8
.github/actions/setup/action.yml
vendored
@@ -1,4 +1,6 @@
|
||||
name: Setup
|
||||
description: checkout repo, setup node, and install node_modules
|
||||
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
@@ -10,12 +12,14 @@ runs:
|
||||
registry-url: https://registry.npmjs.org
|
||||
cache: yarn
|
||||
|
||||
# node_modules/.cache is intentionally omitted, as this is used for build tool caches.
|
||||
- uses: actions/cache@v3
|
||||
id: install-cache
|
||||
with:
|
||||
path: node_modules/
|
||||
path: |
|
||||
node_modules
|
||||
!node_modules/.cache
|
||||
key: ${{ runner.os }}-install-${{ hashFiles('**/yarn.lock') }}
|
||||
|
||||
- if: steps.install-cache.outputs.cache-hit != 'true'
|
||||
run: yarn install --frozen-lockfile --ignore-scripts
|
||||
shell: bash
|
||||
|
||||
75
.github/workflows/test.yml
vendored
75
.github/workflows/test.yml
vendored
@@ -1,5 +1,9 @@
|
||||
name: Test
|
||||
|
||||
# Many build steps have their own caches, so each job has its own cache to improve subsequent build times.
|
||||
# Build tools are configured to cache cache to node_modules/.cache, so this is cached independently of node_modules.
|
||||
# See https://jongleberry.medium.com/speed-up-your-ci-and-dx-with-node-modules-cache-ac8df82b7bb0.
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
@@ -14,7 +18,27 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: ./.github/actions/setup
|
||||
- uses: actions/cache@v3
|
||||
id: eslint-cache
|
||||
with:
|
||||
path: node_modules/.cache
|
||||
key: ${{ runner.os }}-eslint-${{ hashFiles('**/yarn.lock') }}-${{ github.run_id }}
|
||||
restore-keys: ${{ runner.os }}-eslint-${{ hashFiles('**/yarn.lock') }}-
|
||||
- run: yarn lint
|
||||
|
||||
typecheck:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: ./.github/actions/setup
|
||||
- uses: actions/cache@v3
|
||||
id: tsc-cache
|
||||
with:
|
||||
path: node_modules/.cache
|
||||
key: ${{ runner.os }}-tsc-${{ hashFiles('**/yarn.lock') }}-${{ github.run_id }}
|
||||
restore-keys: ${{ runner.os }}-tsc-${{ hashFiles('**/yarn.lock') }}-
|
||||
- run: yarn prepare
|
||||
- run: yarn typecheck
|
||||
|
||||
deps-tests:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -28,6 +52,12 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: ./.github/actions/setup
|
||||
- uses: actions/cache@v3
|
||||
id: jest-cache
|
||||
with:
|
||||
path: node_modules/.cache
|
||||
key: ${{ runner.os }}-jest-${{ hashFiles('**/yarn.lock') }}-${{ github.run_id }}
|
||||
restore-keys: ${{ runner.os }}-jest-${{ hashFiles('**/yarn.lock') }}-
|
||||
- run: yarn prepare
|
||||
- run: yarn test
|
||||
- uses: codecov/codecov-action@v3
|
||||
@@ -41,9 +71,15 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: ./.github/actions/setup
|
||||
- uses: actions/cache@v3
|
||||
id: build-cache
|
||||
with:
|
||||
path: node_modules/.cache
|
||||
key: ${{ runner.os }}-build-${{ hashFiles('**/yarn.lock') }}-${{ github.run_id }}
|
||||
restore-keys: ${{ runner.os }}-build-${{ hashFiles('**/yarn.lock') }}-
|
||||
- run: yarn prepare
|
||||
- run: yarn build
|
||||
- uses: actions/upload-artifact@v2
|
||||
- uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: build
|
||||
path: build
|
||||
@@ -55,31 +91,15 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: ./.github/actions/setup
|
||||
- uses: actions/download-artifact@v2
|
||||
- uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: build
|
||||
path: build
|
||||
- run: yarn test:size
|
||||
|
||||
|
||||
cypress-build:
|
||||
runs-on: ubuntu-latest
|
||||
container: cypress/browsers:node-18.14.1-chrome-111.0.5563.64-1-ff-111.0-edge-111.0.1661.43-1
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: ./.github/actions/setup
|
||||
- uses: actions/cache@v3
|
||||
id: cypress-cache
|
||||
with:
|
||||
path: /root/.cache/Cypress
|
||||
key: ${{ runner.os }}-cypress-${{ hashFiles('yarn.lock') }}
|
||||
- if: steps.cypress-cache.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
yarn cypress install
|
||||
yarn cypress info
|
||||
|
||||
cypress-test-matrix:
|
||||
needs: [build, cypress-build]
|
||||
needs: [build]
|
||||
runs-on: ubuntu-latest
|
||||
container: cypress/browsers:node-18.14.1-chrome-111.0.5563.64-1-ff-111.0-edge-111.0.1661.43-1
|
||||
strategy:
|
||||
@@ -89,17 +109,20 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: ./.github/actions/setup
|
||||
- uses: actions/download-artifact@v2
|
||||
with:
|
||||
name: build
|
||||
path: build
|
||||
|
||||
- uses: actions/cache@v3
|
||||
id: cypress-cache
|
||||
with:
|
||||
path: /root/.cache/Cypress
|
||||
key: ${{ runner.os }}-cypress-${{ hashFiles('yarn.lock') }}
|
||||
- if: steps.cypress-cache.outputs.cache-hit != 'true'
|
||||
run: yarn cypress install
|
||||
key: ${{ runner.os }}-cypress
|
||||
- run: |
|
||||
yarn cypress install
|
||||
yarn cypress info
|
||||
|
||||
- uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: build
|
||||
path: build
|
||||
|
||||
- uses: cypress-io/github-action@v4
|
||||
with:
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -17,6 +17,7 @@ schema.graphql
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
/cache
|
||||
|
||||
# builds
|
||||
/build
|
||||
|
||||
@@ -1,9 +1,15 @@
|
||||
/* eslint-env node */
|
||||
const { VanillaExtractPlugin } = require('@vanilla-extract/webpack-plugin')
|
||||
const { execSync } = require('child_process')
|
||||
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
|
||||
const { DefinePlugin } = require('webpack')
|
||||
|
||||
const commitHash = require('child_process').execSync('git rev-parse HEAD')
|
||||
const commitHash = execSync('git rev-parse HEAD').toString().trim()
|
||||
const isProduction = process.env.NODE_ENV === 'production'
|
||||
|
||||
// Linting and type checking are only necessary as part of development and testing.
|
||||
// Omit them from production builds, as they slow down the feedback loop.
|
||||
const shouldLintOrTypeCheck = !isProduction
|
||||
|
||||
module.exports = {
|
||||
babel: {
|
||||
@@ -17,9 +23,22 @@ module.exports = {
|
||||
},
|
||||
},
|
||||
},
|
||||
eslint: {
|
||||
enable: shouldLintOrTypeCheck,
|
||||
pluginOptions(eslintConfig) {
|
||||
return Object.assign(eslintConfig, {
|
||||
cache: true,
|
||||
cacheLocation: 'node_modules/.cache/eslint/',
|
||||
})
|
||||
},
|
||||
},
|
||||
typescript: {
|
||||
enableTypeChecking: shouldLintOrTypeCheck,
|
||||
},
|
||||
jest: {
|
||||
configure(jestConfig) {
|
||||
return Object.assign({}, jestConfig, {
|
||||
return Object.assign(jestConfig, {
|
||||
cacheDirectory: 'node_modules/.cache/jest',
|
||||
transformIgnorePatterns: ['@uniswap/conedison/format', '@uniswap/conedison/provider'],
|
||||
moduleNameMapper: {
|
||||
'@uniswap/conedison/format': '@uniswap/conedison/dist/format',
|
||||
@@ -29,25 +48,29 @@ module.exports = {
|
||||
},
|
||||
},
|
||||
webpack: {
|
||||
plugins: [
|
||||
new VanillaExtractPlugin({ identifiers: 'short' }),
|
||||
new DefinePlugin({
|
||||
'process.env.REACT_APP_GIT_COMMIT_HASH': JSON.stringify(commitHash.toString()),
|
||||
}),
|
||||
],
|
||||
plugins: [new VanillaExtractPlugin({ identifiers: 'short' })],
|
||||
configure: (webpackConfig) => {
|
||||
const instanceOfMiniCssExtractPlugin = webpackConfig.plugins.find(
|
||||
(plugin) => plugin instanceof MiniCssExtractPlugin
|
||||
)
|
||||
if (instanceOfMiniCssExtractPlugin !== undefined) instanceOfMiniCssExtractPlugin.options.ignoreOrder = true
|
||||
webpackConfig.plugins = webpackConfig.plugins.map((plugin) => {
|
||||
// Extend process.env with dynamic values (eg commit hash).
|
||||
// This will make dynamic values available to JavaScript only, not to interpolated HTML (ie index.html).
|
||||
if (plugin instanceof DefinePlugin) {
|
||||
Object.assign(plugin.definitions['process.env'], {
|
||||
REACT_APP_GIT_COMMIT_HASH: JSON.stringify(commitHash),
|
||||
})
|
||||
}
|
||||
|
||||
// We're currently on Webpack 4.x that doesn't support the `exports` field in package.json.
|
||||
// CSS ordering is mitigated through scoping / naming conventions, so we can ignore order warnings.
|
||||
// See https://webpack.js.org/plugins/mini-css-extract-plugin/#remove-order-warnings.
|
||||
if (plugin instanceof MiniCssExtractPlugin) {
|
||||
plugin.options.ignoreOrder = true
|
||||
}
|
||||
|
||||
return plugin
|
||||
})
|
||||
|
||||
// We're currently on Webpack 4.x which doesn't support the `exports` field in package.json.
|
||||
// Instead, we need to manually map the import path to the correct exports path (eg dist or build folder).
|
||||
// See https://github.com/webpack/webpack/issues/9509.
|
||||
//
|
||||
// In case you need to add more modules, make sure to remap them to the correct path.
|
||||
//
|
||||
// Map @uniswap/conedison to its dist folder.
|
||||
// This is required because conedison uses * to redirect all imports to its dist.
|
||||
webpackConfig.resolve.alias['@uniswap/conedison'] = '@uniswap/conedison/dist'
|
||||
|
||||
return webpackConfig
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import codeCoverageTask from '@cypress/code-coverage/task'
|
||||
import { defineConfig } from 'cypress'
|
||||
import { setupHardhatEvents } from 'cypress-hardhat'
|
||||
|
||||
export default defineConfig({
|
||||
projectId: 'yp82ef',
|
||||
@@ -8,7 +9,8 @@ export default defineConfig({
|
||||
chromeWebSecurity: false,
|
||||
retries: { runMode: 2 },
|
||||
e2e: {
|
||||
setupNodeEvents(on, config) {
|
||||
async setupNodeEvents(on, config) {
|
||||
await setupHardhatEvents(on, config)
|
||||
codeCoverageTask(on, config)
|
||||
return {
|
||||
...config,
|
||||
|
||||
22
hardhat.config.js
Normal file
22
hardhat.config.js
Normal file
@@ -0,0 +1,22 @@
|
||||
/* eslint-env node */
|
||||
require('dotenv').config()
|
||||
|
||||
const mainnetFork = {
|
||||
url: `https://mainnet.infura.io/v3/${process.env.REACT_APP_INFURA_KEY}`,
|
||||
blockNumber: 17023328,
|
||||
httpHeaders: {
|
||||
Origin: 'localhost:3000', // infura allowlists requests by origin
|
||||
},
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
networks: {
|
||||
hardhat: {
|
||||
chainId: 1,
|
||||
forking: mainnetFork,
|
||||
accounts: {
|
||||
count: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
31
package.json
31
package.json
@@ -17,28 +17,28 @@
|
||||
"i18n:compile": "yarn i18n:extract && lingui compile",
|
||||
"i18n:pseudo": "lingui extract --locale pseudo && lingui compile",
|
||||
"prepare": "yarn contracts:compile && yarn graphql:fetch && yarn graphql:generate && yarn i18n:compile",
|
||||
"postinstall": "patch-package",
|
||||
"start": "craco start",
|
||||
"build": "craco build",
|
||||
"serve": "serve build -l 3000",
|
||||
"deduplicate": "yarn-deduplicate --strategy=highest",
|
||||
"lint": "yarn eslint .",
|
||||
"lint": "yarn eslint --ignore-path .gitignore --cache --cache-location node_modules/.cache/eslint/ .",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"test": "craco test --coverage",
|
||||
"test:size": "node scripts/test-size.js",
|
||||
"cypress:open": "cypress open --browser chrome --e2e",
|
||||
"cypress:run": "cypress run --browser chrome --e2e",
|
||||
"postinstall": "patch-package"
|
||||
"deduplicate": "yarn-deduplicate --strategy=highest"
|
||||
},
|
||||
"jest": {
|
||||
"collectCoverageFrom": [
|
||||
"src/components/**/*.ts*",
|
||||
"src/hooks/**/*.ts*",
|
||||
"src/lib/hooks/**/*.ts*",
|
||||
"src/lib/state/**/*.ts*",
|
||||
"src/lib/utils/**/*.ts*",
|
||||
"src/pages/**/*.ts*",
|
||||
"src/state/**/*.ts*",
|
||||
"src/tracing/**/*.ts*",
|
||||
"src/utils/**/*.ts*"
|
||||
"src/**/*.ts*",
|
||||
"!src/**/*.d.ts",
|
||||
"!src/abis/types/**",
|
||||
"!src/constants/**/*.ts",
|
||||
"!src/graphql/**/__generated__/**",
|
||||
"!src/locales/**",
|
||||
"!src/test-utils/**",
|
||||
"!src/types/v3/**"
|
||||
],
|
||||
"coveragePathIgnorePatterns": [
|
||||
".snap"
|
||||
@@ -99,9 +99,11 @@
|
||||
"@uniswap/eslint-config": "^1.1.1",
|
||||
"@vanilla-extract/babel-plugin": "^1.1.7",
|
||||
"@vanilla-extract/webpack-plugin": "^2.1.11",
|
||||
"cypress": "^10.3.1",
|
||||
"cypress": "10.3.1",
|
||||
"cypress-hardhat": "^1.0.0",
|
||||
"env-cmd": "^10.1.0",
|
||||
"eslint": "^7.11.0",
|
||||
"hardhat": "^2.14.0",
|
||||
"jest-fetch-mock": "^3.0.3",
|
||||
"jest-styled-components": "^7.0.8",
|
||||
"ms.macro": "^2.0.0",
|
||||
@@ -139,9 +141,10 @@
|
||||
"@reduxjs/toolkit": "^1.6.1",
|
||||
"@sentry/react": "^7.45.0",
|
||||
"@sentry/tracing": "^7.45.0",
|
||||
"@sentry/types": "^7.45.0",
|
||||
"@types/react-window-infinite-loader": "^1.0.6",
|
||||
"@uniswap/analytics": "^1.3.1",
|
||||
"@uniswap/analytics-events": "^2.9.0",
|
||||
"@uniswap/analytics-events": "^2.10.0",
|
||||
"@uniswap/conedison": "^1.4.0",
|
||||
"@uniswap/governance": "^1.0.2",
|
||||
"@uniswap/liquidity-staker": "^1.0.2",
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 558 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 306 KiB |
19
src/assets/svg/wallet_banner_phone_image.svg
Normal file
19
src/assets/svg/wallet_banner_phone_image.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 990 KiB |
@@ -1,7 +1,6 @@
|
||||
import { sendAnalyticsEvent } from '@uniswap/analytics'
|
||||
import { useMGTMMicrositeEnabled } from 'featureFlags/flags/mgtm'
|
||||
import { InterfaceElementName, InterfaceEventName, SharedEventName } from '@uniswap/analytics-events'
|
||||
import { PropsWithChildren, useCallback } from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import styled from 'styled-components/macro'
|
||||
import { ClickableStyle } from 'theme'
|
||||
import { isIOS } from 'utils/userAgent'
|
||||
@@ -33,22 +32,38 @@ function BaseButton({ onClick, branded, children }: PropsWithChildren<{ onClick?
|
||||
)
|
||||
}
|
||||
|
||||
const APP_STORE_LINK = 'https://apps.apple.com/us/app/uniswap-wallet/id6443944476'
|
||||
const APP_STORE_LINK = 'https://apps.apple.com/app/apple-store/id6443944476?pt=123625782&ct=In-App-Banners&mt=8'
|
||||
const MICROSITE_LINK = 'https://wallet.uniswap.org/'
|
||||
|
||||
const openAppStore = () => {
|
||||
window.open(APP_STORE_LINK, /* target = */ 'uniswap_wallet_appstore')
|
||||
}
|
||||
export const openWalletMicrosite = () => {
|
||||
sendAnalyticsEvent(InterfaceEventName.UNISWAP_WALLET_MICROSITE_OPENED)
|
||||
window.open(MICROSITE_LINK, /* target = */ 'uniswap_wallet_microsite')
|
||||
}
|
||||
|
||||
export function openDownloadApp(element: InterfaceElementName) {
|
||||
sendAnalyticsEvent(SharedEventName.ELEMENT_CLICKED, { element })
|
||||
if (isIOS) openAppStore()
|
||||
else openWalletMicrosite()
|
||||
}
|
||||
|
||||
// Launches App Store if on an iOS device, else navigates to Uniswap Wallet microsite
|
||||
export function DownloadButton({ onClick, text = 'Download' }: { onClick?: () => void; text?: string }) {
|
||||
const navigate = useNavigate()
|
||||
const micrositeEnabled = useMGTMMicrositeEnabled()
|
||||
|
||||
export function DownloadButton({
|
||||
onClick,
|
||||
text = 'Download',
|
||||
element,
|
||||
}: {
|
||||
onClick?: () => void
|
||||
text?: string
|
||||
element: InterfaceElementName
|
||||
}) {
|
||||
const onButtonClick = useCallback(() => {
|
||||
// handles any actions required by the parent, i.e. cancelling wallet connection attempt or dismissing an ad
|
||||
onClick?.()
|
||||
|
||||
if (isIOS || !micrositeEnabled) {
|
||||
sendAnalyticsEvent('Uniswap wallet download clicked')
|
||||
window.open(APP_STORE_LINK)
|
||||
} else navigate('/wallet')
|
||||
}, [onClick, micrositeEnabled, navigate])
|
||||
openDownloadApp(element)
|
||||
}, [element, onClick])
|
||||
|
||||
return (
|
||||
<BaseButton branded onClick={onButtonClick}>
|
||||
@@ -56,8 +71,3 @@ export function DownloadButton({ onClick, text = 'Download' }: { onClick?: () =>
|
||||
</BaseButton>
|
||||
)
|
||||
}
|
||||
|
||||
export function LearnMoreButton() {
|
||||
const navigate = useNavigate()
|
||||
return <BaseButton onClick={() => navigate('/wallet')}>Learn More</BaseButton>
|
||||
}
|
||||
|
||||
@@ -1,22 +1,19 @@
|
||||
// jest unit tests for the parseLocalActivity function
|
||||
|
||||
import { SupportedChainId, Token, TradeType as MockTradeType } from '@uniswap/sdk-core'
|
||||
import { DAI as MockDAI, USDC_MAINNET as MockUSDC_MAINNET } from 'constants/tokens'
|
||||
import { PERMIT2_ADDRESS } from '@uniswap/universal-router-sdk'
|
||||
import { DAI as MockDAI, nativeOnChain, USDC_MAINNET as MockUSDC_MAINNET } from 'constants/tokens'
|
||||
import { TransactionStatus as MockTxStatus } from 'graphql/data/__generated__/types-and-hooks'
|
||||
import { TokenAddressMap } from 'state/lists/hooks'
|
||||
import { WrappedTokenInfo } from 'state/lists/wrappedTokenInfo'
|
||||
import {
|
||||
ExactInputSwapTransactionInfo,
|
||||
ExactOutputSwapTransactionInfo,
|
||||
TransactionDetails,
|
||||
TransactionType,
|
||||
TransactionInfo,
|
||||
TransactionType as MockTxType,
|
||||
} from 'state/transactions/types'
|
||||
import { renderHook } from 'test-utils'
|
||||
import { renderHook } from 'test-utils/render'
|
||||
|
||||
import { parseLocalActivity, useLocalActivities } from './parseLocal'
|
||||
|
||||
const oneUSDCRaw = '1000000'
|
||||
const oneDAIRaw = '1000000000000000000'
|
||||
|
||||
function mockSwapInfo(
|
||||
type: MockTradeType,
|
||||
inputCurrency: Token,
|
||||
@@ -26,7 +23,7 @@ function mockSwapInfo(
|
||||
): ExactInputSwapTransactionInfo | ExactOutputSwapTransactionInfo {
|
||||
if (type === MockTradeType.EXACT_INPUT) {
|
||||
return {
|
||||
type: TransactionType.SWAP,
|
||||
type: MockTxType.SWAP,
|
||||
tradeType: MockTradeType.EXACT_INPUT,
|
||||
inputCurrencyId: inputCurrency.address,
|
||||
inputCurrencyAmountRaw,
|
||||
@@ -36,7 +33,7 @@ function mockSwapInfo(
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
type: TransactionType.SWAP,
|
||||
type: MockTxType.SWAP,
|
||||
tradeType: MockTradeType.EXACT_OUTPUT,
|
||||
inputCurrencyId: inputCurrency.address,
|
||||
expectedInputCurrencyAmountRaw: inputCurrencyAmountRaw,
|
||||
@@ -50,58 +47,195 @@ function mockSwapInfo(
|
||||
const mockAccount1 = '0x000000000000000000000000000000000000000001'
|
||||
const mockAccount2 = '0x000000000000000000000000000000000000000002'
|
||||
const mockChainId = SupportedChainId.MAINNET
|
||||
const mockSpenderAddress = PERMIT2_ADDRESS[mockChainId]
|
||||
const mockCurrencyAmountRaw = '1000000000000000000'
|
||||
const mockCurrencyAmountRawUSDC = '1000000'
|
||||
|
||||
function mockHash(id: string, status: MockTxStatus = MockTxStatus.Confirmed) {
|
||||
return id + status
|
||||
}
|
||||
|
||||
function mockCommonFields(id: string, account = mockAccount2, status: MockTxStatus) {
|
||||
const hash = mockHash(id, status)
|
||||
return {
|
||||
hash,
|
||||
from: account,
|
||||
receipt:
|
||||
status === MockTxStatus.Pending
|
||||
? undefined
|
||||
: {
|
||||
transactionHash: hash,
|
||||
status: status === MockTxStatus.Confirmed ? 1 : 0,
|
||||
},
|
||||
addedTime: 0,
|
||||
}
|
||||
}
|
||||
|
||||
function mockMultiStatus(info: TransactionInfo, id: string): [TransactionDetails, number][] {
|
||||
// Mocks a transaction with multiple statuses
|
||||
return [
|
||||
[
|
||||
{ info, ...mockCommonFields(id, mockAccount2, MockTxStatus.Pending) } as unknown as TransactionDetails,
|
||||
mockChainId,
|
||||
],
|
||||
[
|
||||
{ info, ...mockCommonFields(id, mockAccount2, MockTxStatus.Confirmed) } as unknown as TransactionDetails,
|
||||
mockChainId,
|
||||
],
|
||||
[
|
||||
{ info, ...mockCommonFields(id, mockAccount2, MockTxStatus.Failed) } as unknown as TransactionDetails,
|
||||
mockChainId,
|
||||
],
|
||||
]
|
||||
}
|
||||
|
||||
const mockTokenAddressMap: TokenAddressMap = {
|
||||
[mockChainId]: {
|
||||
[MockDAI.address]: { token: MockDAI },
|
||||
[MockUSDC_MAINNET.address]: { token: MockUSDC_MAINNET },
|
||||
} as TokenAddressMap[number],
|
||||
}
|
||||
|
||||
jest.mock('../../../../state/lists/hooks', () => ({
|
||||
useCombinedActiveList: () => mockTokenAddressMap,
|
||||
}))
|
||||
|
||||
jest.mock('../../../../state/transactions/hooks', () => {
|
||||
return {
|
||||
useMultichainTransactions: () => {
|
||||
useMultichainTransactions: (): [TransactionDetails, number][] => {
|
||||
return [
|
||||
[
|
||||
{
|
||||
info: mockSwapInfo(MockTradeType.EXACT_INPUT, MockUSDC_MAINNET, oneUSDCRaw, MockDAI, oneDAIRaw),
|
||||
hash: '0x123',
|
||||
from: mockAccount1,
|
||||
info: mockSwapInfo(
|
||||
MockTradeType.EXACT_INPUT,
|
||||
MockUSDC_MAINNET,
|
||||
mockCurrencyAmountRawUSDC,
|
||||
MockDAI,
|
||||
mockCurrencyAmountRaw
|
||||
),
|
||||
...mockCommonFields('0x123', mockAccount1, MockTxStatus.Confirmed),
|
||||
} as TransactionDetails,
|
||||
mockChainId,
|
||||
],
|
||||
[
|
||||
...mockMultiStatus(
|
||||
mockSwapInfo(
|
||||
MockTradeType.EXACT_OUTPUT,
|
||||
MockUSDC_MAINNET,
|
||||
mockCurrencyAmountRawUSDC,
|
||||
MockDAI,
|
||||
mockCurrencyAmountRaw
|
||||
),
|
||||
'0xswap_exact_input'
|
||||
),
|
||||
...mockMultiStatus(
|
||||
mockSwapInfo(
|
||||
MockTradeType.EXACT_INPUT,
|
||||
MockUSDC_MAINNET,
|
||||
mockCurrencyAmountRawUSDC,
|
||||
MockDAI,
|
||||
mockCurrencyAmountRaw
|
||||
),
|
||||
'0xswap_exact_output'
|
||||
),
|
||||
...mockMultiStatus(
|
||||
{
|
||||
info: mockSwapInfo(MockTradeType.EXACT_INPUT, MockUSDC_MAINNET, oneUSDCRaw, MockDAI, oneDAIRaw),
|
||||
hash: '0x456',
|
||||
from: mockAccount2,
|
||||
} as TransactionDetails,
|
||||
mockChainId,
|
||||
],
|
||||
[
|
||||
type: MockTxType.APPROVAL,
|
||||
tokenAddress: MockDAI.address,
|
||||
spender: mockSpenderAddress,
|
||||
},
|
||||
'0xapproval'
|
||||
),
|
||||
...mockMultiStatus(
|
||||
{
|
||||
info: mockSwapInfo(MockTradeType.EXACT_INPUT, MockUSDC_MAINNET, oneUSDCRaw, MockDAI, oneDAIRaw),
|
||||
hash: '0x789',
|
||||
from: mockAccount2,
|
||||
} as TransactionDetails,
|
||||
mockChainId,
|
||||
],
|
||||
type: MockTxType.WRAP,
|
||||
unwrapped: false,
|
||||
currencyAmountRaw: mockCurrencyAmountRaw,
|
||||
chainId: mockChainId,
|
||||
},
|
||||
'0xwrap'
|
||||
),
|
||||
...mockMultiStatus(
|
||||
{
|
||||
type: MockTxType.WRAP,
|
||||
unwrapped: true,
|
||||
currencyAmountRaw: mockCurrencyAmountRaw,
|
||||
chainId: mockChainId,
|
||||
},
|
||||
'0xunwrap'
|
||||
),
|
||||
...mockMultiStatus(
|
||||
{
|
||||
type: MockTxType.ADD_LIQUIDITY_V3_POOL,
|
||||
createPool: false,
|
||||
baseCurrencyId: MockUSDC_MAINNET.address,
|
||||
quoteCurrencyId: MockDAI.address,
|
||||
feeAmount: 500,
|
||||
expectedAmountBaseRaw: mockCurrencyAmountRawUSDC,
|
||||
expectedAmountQuoteRaw: mockCurrencyAmountRaw,
|
||||
},
|
||||
'0xadd_liquidity_v3'
|
||||
),
|
||||
...mockMultiStatus(
|
||||
{
|
||||
type: MockTxType.REMOVE_LIQUIDITY_V3,
|
||||
baseCurrencyId: MockUSDC_MAINNET.address,
|
||||
quoteCurrencyId: MockDAI.address,
|
||||
expectedAmountBaseRaw: mockCurrencyAmountRawUSDC,
|
||||
expectedAmountQuoteRaw: mockCurrencyAmountRaw,
|
||||
},
|
||||
'0xremove_liquidity_v3'
|
||||
),
|
||||
...mockMultiStatus(
|
||||
{
|
||||
type: MockTxType.ADD_LIQUIDITY_V2_POOL,
|
||||
baseCurrencyId: MockUSDC_MAINNET.address,
|
||||
quoteCurrencyId: MockDAI.address,
|
||||
expectedAmountBaseRaw: mockCurrencyAmountRawUSDC,
|
||||
expectedAmountQuoteRaw: mockCurrencyAmountRaw,
|
||||
},
|
||||
'0xadd_liquidity_v2'
|
||||
),
|
||||
...mockMultiStatus(
|
||||
{
|
||||
type: MockTxType.COLLECT_FEES,
|
||||
currencyId0: MockUSDC_MAINNET.address,
|
||||
currencyId1: MockDAI.address,
|
||||
expectedCurrencyOwed0: mockCurrencyAmountRawUSDC,
|
||||
expectedCurrencyOwed1: mockCurrencyAmountRaw,
|
||||
},
|
||||
'0xcollect_fees'
|
||||
),
|
||||
...mockMultiStatus(
|
||||
{
|
||||
type: MockTxType.MIGRATE_LIQUIDITY_V3,
|
||||
baseCurrencyId: MockUSDC_MAINNET.address,
|
||||
quoteCurrencyId: MockDAI.address,
|
||||
isFork: false,
|
||||
},
|
||||
'0xmigrate_v3_liquidity'
|
||||
),
|
||||
]
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
function mockTokenAddressMap(...tokens: WrappedTokenInfo[]): TokenAddressMap {
|
||||
return {
|
||||
[SupportedChainId.MAINNET]: Object.fromEntries(tokens.map((token) => [token.address, { token }])),
|
||||
}
|
||||
}
|
||||
|
||||
describe('parseLocalActivity', () => {
|
||||
it('returns swap activity fields with known tokens, exact input', () => {
|
||||
const details = {
|
||||
info: mockSwapInfo(MockTradeType.EXACT_INPUT, MockUSDC_MAINNET, oneUSDCRaw, MockDAI, oneDAIRaw),
|
||||
info: mockSwapInfo(
|
||||
MockTradeType.EXACT_INPUT,
|
||||
MockUSDC_MAINNET,
|
||||
mockCurrencyAmountRawUSDC,
|
||||
MockDAI,
|
||||
mockCurrencyAmountRaw
|
||||
),
|
||||
receipt: {
|
||||
transactionHash: '0x123',
|
||||
status: 1,
|
||||
},
|
||||
} as TransactionDetails
|
||||
const chainId = SupportedChainId.MAINNET
|
||||
const tokens = mockTokenAddressMap(MockUSDC_MAINNET as WrappedTokenInfo, MockDAI as WrappedTokenInfo)
|
||||
expect(parseLocalActivity(details, chainId, tokens)).toEqual({
|
||||
expect(parseLocalActivity(details, chainId, mockTokenAddressMap)).toEqual({
|
||||
chainId: 1,
|
||||
currencies: [MockUSDC_MAINNET, MockDAI],
|
||||
descriptor: '1.00 USDC for 1.00 DAI',
|
||||
@@ -112,10 +246,10 @@ describe('parseLocalActivity', () => {
|
||||
type: 1,
|
||||
tradeType: MockTradeType.EXACT_INPUT,
|
||||
inputCurrencyId: MockUSDC_MAINNET.address,
|
||||
inputCurrencyAmountRaw: oneUSDCRaw,
|
||||
inputCurrencyAmountRaw: mockCurrencyAmountRawUSDC,
|
||||
outputCurrencyId: MockDAI.address,
|
||||
expectedOutputCurrencyAmountRaw: oneDAIRaw,
|
||||
minimumOutputCurrencyAmountRaw: oneDAIRaw,
|
||||
expectedOutputCurrencyAmountRaw: mockCurrencyAmountRaw,
|
||||
minimumOutputCurrencyAmountRaw: mockCurrencyAmountRaw,
|
||||
},
|
||||
receipt: { status: 1, transactionHash: '0x123' },
|
||||
status: 'CONFIRMED',
|
||||
@@ -129,43 +263,37 @@ describe('parseLocalActivity', () => {
|
||||
|
||||
it('returns swap activity fields with known tokens, exact output', () => {
|
||||
const details = {
|
||||
info: mockSwapInfo(MockTradeType.EXACT_OUTPUT, MockUSDC_MAINNET, oneUSDCRaw, MockDAI, oneDAIRaw),
|
||||
info: mockSwapInfo(
|
||||
MockTradeType.EXACT_OUTPUT,
|
||||
MockUSDC_MAINNET,
|
||||
mockCurrencyAmountRawUSDC,
|
||||
MockDAI,
|
||||
mockCurrencyAmountRaw
|
||||
),
|
||||
receipt: {
|
||||
transactionHash: '0x123',
|
||||
status: 1,
|
||||
},
|
||||
} as TransactionDetails
|
||||
const chainId = SupportedChainId.MAINNET
|
||||
const tokens = mockTokenAddressMap(MockUSDC_MAINNET as WrappedTokenInfo, MockDAI as WrappedTokenInfo)
|
||||
expect(parseLocalActivity(details, chainId, tokens)).toEqual({
|
||||
expect(parseLocalActivity(details, chainId, mockTokenAddressMap)).toMatchObject({
|
||||
chainId: 1,
|
||||
currencies: [MockUSDC_MAINNET, MockDAI],
|
||||
descriptor: '1.00 USDC for 1.00 DAI',
|
||||
hash: undefined,
|
||||
receipt: {
|
||||
id: '0x123',
|
||||
info: {
|
||||
type: 1,
|
||||
tradeType: MockTradeType.EXACT_OUTPUT,
|
||||
inputCurrencyId: MockUSDC_MAINNET.address,
|
||||
expectedInputCurrencyAmountRaw: oneUSDCRaw,
|
||||
maximumInputCurrencyAmountRaw: oneUSDCRaw,
|
||||
outputCurrencyId: MockDAI.address,
|
||||
outputCurrencyAmountRaw: oneDAIRaw,
|
||||
},
|
||||
receipt: { status: 1, transactionHash: '0x123' },
|
||||
status: 'CONFIRMED',
|
||||
transactionHash: '0x123',
|
||||
},
|
||||
status: 'CONFIRMED',
|
||||
timestamp: NaN,
|
||||
title: 'Swapped',
|
||||
})
|
||||
})
|
||||
|
||||
it('returns swap activity fields with unknown tokens', () => {
|
||||
const details = {
|
||||
info: mockSwapInfo(MockTradeType.EXACT_INPUT, MockUSDC_MAINNET, oneUSDCRaw, MockDAI, oneDAIRaw),
|
||||
info: mockSwapInfo(
|
||||
MockTradeType.EXACT_INPUT,
|
||||
MockUSDC_MAINNET,
|
||||
mockCurrencyAmountRawUSDC,
|
||||
MockDAI,
|
||||
mockCurrencyAmountRaw
|
||||
),
|
||||
receipt: {
|
||||
transactionHash: '0x123',
|
||||
status: 1,
|
||||
@@ -173,28 +301,11 @@ describe('parseLocalActivity', () => {
|
||||
} as TransactionDetails
|
||||
const chainId = SupportedChainId.MAINNET
|
||||
const tokens = {} as TokenAddressMap
|
||||
expect(parseLocalActivity(details, chainId, tokens)).toEqual({
|
||||
expect(parseLocalActivity(details, chainId, tokens)).toMatchObject({
|
||||
chainId: 1,
|
||||
currencies: [undefined, undefined],
|
||||
descriptor: 'Unknown for Unknown',
|
||||
hash: undefined,
|
||||
receipt: {
|
||||
id: '0x123',
|
||||
info: {
|
||||
type: 1,
|
||||
tradeType: MockTradeType.EXACT_INPUT,
|
||||
inputCurrencyId: MockUSDC_MAINNET.address,
|
||||
inputCurrencyAmountRaw: oneUSDCRaw,
|
||||
outputCurrencyId: MockDAI.address,
|
||||
expectedOutputCurrencyAmountRaw: oneDAIRaw,
|
||||
minimumOutputCurrencyAmountRaw: oneDAIRaw,
|
||||
},
|
||||
receipt: { status: 1, transactionHash: '0x123' },
|
||||
status: 'CONFIRMED',
|
||||
transactionHash: '0x123',
|
||||
},
|
||||
status: 'CONFIRMED',
|
||||
timestamp: NaN,
|
||||
title: 'Swapped',
|
||||
})
|
||||
})
|
||||
@@ -204,6 +315,198 @@ describe('parseLocalActivity', () => {
|
||||
const account2Activites = renderHook(() => useLocalActivities(mockAccount2)).result.current
|
||||
|
||||
expect(Object.values(account1Activites)).toHaveLength(1)
|
||||
expect(Object.values(account2Activites)).toHaveLength(2)
|
||||
expect(Object.values(account2Activites)).toHaveLength(30)
|
||||
})
|
||||
|
||||
it('Properly uses correct tense of activity title based on tx status', () => {
|
||||
const activities = renderHook(() => useLocalActivities(mockAccount2)).result.current
|
||||
|
||||
expect(activities[mockHash('0xswap_exact_input', MockTxStatus.Pending)]?.title).toEqual('Swapping')
|
||||
expect(activities[mockHash('0xswap_exact_input', MockTxStatus.Confirmed)]?.title).toEqual('Swapped')
|
||||
expect(activities[mockHash('0xswap_exact_input', MockTxStatus.Failed)]?.title).toEqual('Swap failed')
|
||||
})
|
||||
|
||||
it('Adapts Swap exact input to Activity type', () => {
|
||||
const hash = mockHash('0xswap_exact_input')
|
||||
const activity = renderHook(() => useLocalActivities(mockAccount2)).result.current[hash]
|
||||
|
||||
expect(activity).toMatchObject({
|
||||
chainId: mockChainId,
|
||||
currencies: [MockUSDC_MAINNET, MockDAI],
|
||||
title: 'Swapped',
|
||||
descriptor: `1.00 ${MockUSDC_MAINNET.symbol} for 1.00 ${MockDAI.symbol}`,
|
||||
hash,
|
||||
status: MockTxStatus.Confirmed,
|
||||
receipt: {
|
||||
id: hash,
|
||||
status: MockTxStatus.Confirmed,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('Adapts Swap exact output to Activity type', () => {
|
||||
const hash = mockHash('0xswap_exact_output')
|
||||
const activity = renderHook(() => useLocalActivities(mockAccount2)).result.current[hash]
|
||||
|
||||
expect(activity).toMatchObject({
|
||||
chainId: mockChainId,
|
||||
currencies: [MockUSDC_MAINNET, MockDAI],
|
||||
title: 'Swapped',
|
||||
descriptor: `1.00 ${MockUSDC_MAINNET.symbol} for 1.00 ${MockDAI.symbol}`,
|
||||
hash,
|
||||
status: MockTxStatus.Confirmed,
|
||||
receipt: {
|
||||
id: hash,
|
||||
status: MockTxStatus.Confirmed,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('Adapts Approval to Activity type', () => {
|
||||
const hash = mockHash('0xapproval')
|
||||
const activity = renderHook(() => useLocalActivities(mockAccount2)).result.current[hash]
|
||||
|
||||
expect(activity).toMatchObject({
|
||||
chainId: mockChainId,
|
||||
currencies: [MockDAI],
|
||||
title: 'Approved',
|
||||
descriptor: MockDAI.symbol,
|
||||
hash,
|
||||
status: MockTxStatus.Confirmed,
|
||||
receipt: {
|
||||
id: hash,
|
||||
status: MockTxStatus.Confirmed,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('Adapts Wrap to Activity type', () => {
|
||||
const hash = mockHash('0xwrap')
|
||||
const activity = renderHook(() => useLocalActivities(mockAccount2)).result.current[hash]
|
||||
|
||||
const native = nativeOnChain(mockChainId)
|
||||
|
||||
expect(activity).toMatchObject({
|
||||
chainId: mockChainId,
|
||||
currencies: [native, native.wrapped],
|
||||
title: 'Wrapped',
|
||||
descriptor: `1.00 ${native.symbol} for 1.00 ${native.wrapped.symbol}`,
|
||||
hash,
|
||||
status: MockTxStatus.Confirmed,
|
||||
receipt: {
|
||||
id: hash,
|
||||
status: MockTxStatus.Confirmed,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('Adapts Unwrap to Activity type', () => {
|
||||
const hash = mockHash('0xunwrap')
|
||||
const activity = renderHook(() => useLocalActivities(mockAccount2)).result.current[hash]
|
||||
|
||||
const native = nativeOnChain(mockChainId)
|
||||
|
||||
expect(activity).toMatchObject({
|
||||
chainId: mockChainId,
|
||||
currencies: [native.wrapped, native],
|
||||
title: 'Unwrapped',
|
||||
descriptor: `1.00 ${native.wrapped.symbol} for 1.00 ${native.symbol}`,
|
||||
hash,
|
||||
status: MockTxStatus.Confirmed,
|
||||
receipt: {
|
||||
id: hash,
|
||||
status: MockTxStatus.Confirmed,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('Adapts AddLiquidityV3 to Activity type', () => {
|
||||
const hash = mockHash('0xadd_liquidity_v3')
|
||||
const activity = renderHook(() => useLocalActivities(mockAccount2)).result.current[hash]
|
||||
|
||||
expect(activity).toMatchObject({
|
||||
chainId: mockChainId,
|
||||
currencies: [MockUSDC_MAINNET, MockDAI],
|
||||
title: 'Added liquidity',
|
||||
descriptor: `1.00 ${MockUSDC_MAINNET.symbol} and 1.00 ${MockDAI.symbol}`,
|
||||
hash,
|
||||
status: MockTxStatus.Confirmed,
|
||||
receipt: {
|
||||
id: hash,
|
||||
status: MockTxStatus.Confirmed,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('Adapts RemoveLiquidityV3 to Activity type', () => {
|
||||
const hash = mockHash('0xremove_liquidity_v3')
|
||||
const activity = renderHook(() => useLocalActivities(mockAccount2)).result.current[hash]
|
||||
|
||||
expect(activity).toMatchObject({
|
||||
chainId: mockChainId,
|
||||
currencies: [MockUSDC_MAINNET, MockDAI],
|
||||
title: 'Removed liquidity',
|
||||
descriptor: `1.00 ${MockUSDC_MAINNET.symbol} and 1.00 ${MockDAI.symbol}`,
|
||||
hash,
|
||||
status: MockTxStatus.Confirmed,
|
||||
receipt: {
|
||||
id: hash,
|
||||
status: MockTxStatus.Confirmed,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('Adapts RemoveLiquidityV2 to Activity type', () => {
|
||||
const hash = mockHash('0xadd_liquidity_v2')
|
||||
const activity = renderHook(() => useLocalActivities(mockAccount2)).result.current[hash]
|
||||
|
||||
expect(activity).toMatchObject({
|
||||
chainId: mockChainId,
|
||||
currencies: [MockUSDC_MAINNET, MockDAI],
|
||||
title: 'Added V2 liquidity',
|
||||
descriptor: `1.00 ${MockUSDC_MAINNET.symbol} and 1.00 ${MockDAI.symbol}`,
|
||||
hash,
|
||||
status: MockTxStatus.Confirmed,
|
||||
receipt: {
|
||||
id: hash,
|
||||
status: MockTxStatus.Confirmed,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('Adapts CollectFees to Activity type', () => {
|
||||
const hash = mockHash('0xcollect_fees')
|
||||
const activity = renderHook(() => useLocalActivities(mockAccount2)).result.current[hash]
|
||||
|
||||
expect(activity).toMatchObject({
|
||||
chainId: mockChainId,
|
||||
currencies: [MockUSDC_MAINNET, MockDAI],
|
||||
title: 'Collected fees',
|
||||
descriptor: `1.00 ${MockUSDC_MAINNET.symbol} and 1.00 ${MockDAI.symbol}`,
|
||||
hash,
|
||||
status: MockTxStatus.Confirmed,
|
||||
receipt: {
|
||||
id: hash,
|
||||
status: MockTxStatus.Confirmed,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('Adapts MigrateLiquidityV3 to Activity type', () => {
|
||||
const hash = mockHash('0xmigrate_v3_liquidity')
|
||||
const activity = renderHook(() => useLocalActivities(mockAccount2)).result.current[hash]
|
||||
|
||||
expect(activity).toMatchObject({
|
||||
chainId: mockChainId,
|
||||
currencies: [MockUSDC_MAINNET, MockDAI],
|
||||
title: 'Migrated liquidity',
|
||||
descriptor: `${MockUSDC_MAINNET.symbol} and ${MockDAI.symbol}`,
|
||||
hash,
|
||||
status: MockTxStatus.Confirmed,
|
||||
receipt: {
|
||||
id: hash,
|
||||
status: MockTxStatus.Confirmed,
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -122,7 +122,7 @@ function parseMigrateCreateV3(
|
||||
): Partial<Activity> {
|
||||
const baseCurrency = getCurrency(lp.baseCurrencyId, chainId, tokens)
|
||||
const baseSymbol = baseCurrency?.symbol ?? t`Unknown`
|
||||
const quoteCurrency = getCurrency(lp.baseCurrencyId, chainId, tokens)
|
||||
const quoteCurrency = getCurrency(lp.quoteCurrencyId, chainId, tokens)
|
||||
const quoteSymbol = quoteCurrency?.symbol ?? t`Unknown`
|
||||
const descriptor = t`${baseSymbol} and ${quoteSymbol}`
|
||||
|
||||
|
||||
@@ -2,13 +2,13 @@ import { BigNumber } from '@ethersproject/bignumber'
|
||||
import { SupportedChainId, WETH9 } from '@uniswap/sdk-core'
|
||||
import { FeeAmount, Pool, Position } from '@uniswap/v3-sdk'
|
||||
import { USDC_MAINNET } from 'constants/tokens'
|
||||
import { render } from 'test-utils'
|
||||
import { mocked } from 'test-utils/mocked'
|
||||
import { render } from 'test-utils/render'
|
||||
|
||||
import Pools from '.'
|
||||
import useMultiChainPositions from './useMultiChainPositions'
|
||||
|
||||
jest.mock('./useMultiChainPositions')
|
||||
const mockUseMultiChainPositions = useMultiChainPositions as jest.MockedFunction<typeof useMultiChainPositions>
|
||||
|
||||
const owner = '0xf5b6bb25f5beaea03dd014c6ef9fa9f3926bf36c'
|
||||
|
||||
@@ -58,7 +58,7 @@ const useMultiChainPositionsReturnValue = {
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
mockUseMultiChainPositions.mockReturnValue(useMultiChainPositionsReturnValue)
|
||||
mocked(useMultiChainPositions).mockReturnValue(useMultiChainPositionsReturnValue)
|
||||
})
|
||||
test('Pools should render LP positions', () => {
|
||||
const props = { account: owner }
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { SupportedChainId } from '@uniswap/sdk-core'
|
||||
import { DAI_ARBITRUM } from '@uniswap/smart-order-router'
|
||||
import { DAI, USDC_ARBITRUM, USDC_MAINNET } from 'constants/tokens'
|
||||
import { render } from 'test-utils'
|
||||
import { render } from 'test-utils/render'
|
||||
|
||||
import { PortfolioLogo } from './PortfolioLogo'
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import { LOCALE_LABEL, SUPPORTED_LOCALES, SupportedLocale } from 'constants/loca
|
||||
import { useActiveLocale } from 'hooks/useActiveLocale'
|
||||
import { useLocationLinkProps } from 'hooks/useLocationLinkProps'
|
||||
import { Check } from 'react-feather'
|
||||
import { Link, useLocation } from 'react-router-dom'
|
||||
import { Link } from 'react-router-dom'
|
||||
import styled, { useTheme } from 'styled-components/macro'
|
||||
import { ClickableStyle, ThemedText } from 'theme'
|
||||
import ThemeToggle from 'theme/components/ThemeToggle'
|
||||
@@ -56,16 +56,13 @@ const BalanceToggleContainer = styled.div`
|
||||
export default function SettingsMenu({ onClose }: { onClose: () => void }) {
|
||||
const activeLocale = useActiveLocale()
|
||||
|
||||
const { pathname } = useLocation()
|
||||
const isWalletPage = pathname.includes('/wallet')
|
||||
|
||||
return (
|
||||
<SlideOutMenu title={<Trans>Settings</Trans>} onClose={onClose}>
|
||||
<SectionTitle>
|
||||
<Trans>Preferences</Trans>
|
||||
</SectionTitle>
|
||||
<ThemeToggleContainer>
|
||||
<ThemeToggle disabled={isWalletPage} />
|
||||
<ThemeToggle />
|
||||
</ThemeToggleContainer>
|
||||
<BalanceToggleContainer>
|
||||
<SmallBalanceToggle />
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import { sendAnalyticsEvent } from '@uniswap/analytics'
|
||||
import { InterfaceElementName } from '@uniswap/analytics-events'
|
||||
import { useWeb3React } from '@web3-react/core'
|
||||
import { WalletConnect } from '@web3-react/walletconnect'
|
||||
import Column, { AutoColumn } from 'components/Column'
|
||||
@@ -95,7 +96,7 @@ export default function UniwalletModal() {
|
||||
)}
|
||||
</QRCodeWrapper>
|
||||
<Divider />
|
||||
<InfoSection onClose={onClose} />
|
||||
<InfoSection />
|
||||
</UniwalletConnectWrapper>
|
||||
</Modal>
|
||||
)
|
||||
@@ -108,7 +109,7 @@ const InfoSectionWrapper = styled(RowBetween)`
|
||||
gap: 20px;
|
||||
`
|
||||
|
||||
function InfoSection({ onClose }: { onClose: () => void }) {
|
||||
function InfoSection() {
|
||||
return (
|
||||
<InfoSectionWrapper>
|
||||
<AutoColumn gap="4px">
|
||||
@@ -122,7 +123,7 @@ function InfoSection({ onClose }: { onClose: () => void }) {
|
||||
</ThemedText.Caption>
|
||||
</AutoColumn>
|
||||
<Column>
|
||||
<DownloadButton onClick={onClose} />
|
||||
<DownloadButton element={InterfaceElementName.UNISWAP_WALLET_MODAL_DOWNLOAD_BUTTON} />
|
||||
</Column>
|
||||
</InfoSectionWrapper>
|
||||
)
|
||||
|
||||
@@ -1,111 +1,147 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import { useAccountDrawer } from 'components/AccountDrawer'
|
||||
import { DownloadButton, LearnMoreButton } from 'components/AccountDrawer/DownloadButton'
|
||||
import { InterfaceElementName } from '@uniswap/analytics-events'
|
||||
import { openDownloadApp, openWalletMicrosite } from 'components/AccountDrawer/DownloadButton'
|
||||
import { BaseButton } from 'components/Button'
|
||||
import { AutoColumn } from 'components/Column'
|
||||
import Row, { RowBetween } from 'components/Row'
|
||||
import { OpacityHoverState } from 'components/Common'
|
||||
import Row from 'components/Row'
|
||||
import { useMgtmEnabled } from 'featureFlags/flags/mgtm'
|
||||
import { useScreenSize } from 'hooks/useScreenSize'
|
||||
import { X } from 'react-feather'
|
||||
import { useLocation } from 'react-router-dom'
|
||||
import { useHideUniswapWalletBanner } from 'state/user/hooks'
|
||||
import styled, { useTheme } from 'styled-components/macro'
|
||||
import styled from 'styled-components/macro'
|
||||
import { ThemedText } from 'theme'
|
||||
import { Z_INDEX } from 'theme/zIndex'
|
||||
import { isIOS } from 'utils/userAgent'
|
||||
|
||||
import bannerImageDark from '../../assets/images/uniswapWalletBannerDark.png'
|
||||
import bannerImageLight from '../../assets/images/uniswapWalletBannerLight.png'
|
||||
import { ReactComponent as AppleLogo } from '../../assets/svg/apple_logo.svg'
|
||||
import walletBannerPhoneImageSrc from '../../assets/svg/wallet_banner_phone_image.svg'
|
||||
|
||||
const PopupContainer = styled.div<{ show: boolean }>`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
|
||||
${({ show }) => !show && 'display: none'};
|
||||
|
||||
box-shadow: ${({ theme }) =>
|
||||
theme.darkMode
|
||||
? '0px -16px 24px rgba(0, 0, 0, 0.4), 0px -8px 12px rgba(0, 0, 0, 0.4), 0px -4px 8px rgba(0, 0, 0, 0.32)'
|
||||
: '0px -12px 20px rgba(51, 53, 72, 0.04), 0px -6px 12px rgba(51, 53, 72, 0.02), 0px -4px 8px rgba(51, 53, 72, 0.04)'};
|
||||
|
||||
background-image: ${({ theme }) => (theme.darkMode ? `url(${bannerImageDark})` : `url(${bannerImageLight})`)};
|
||||
background: url(${walletBannerPhoneImageSrc});
|
||||
background-repeat: no-repeat;
|
||||
background-size: cover;
|
||||
background-position: bottom -1px right 15px;
|
||||
background-size: 166px;
|
||||
|
||||
cursor: pointer;
|
||||
:hover {
|
||||
background-size: 170px;
|
||||
}
|
||||
transition: background-size ${({ theme }) => theme.transition.duration.medium}
|
||||
${({ theme }) => theme.transition.timing.inOut};
|
||||
|
||||
background-color: ${({ theme }) => theme.promotional};
|
||||
color: ${({ theme }) => theme.textPrimary};
|
||||
position: fixed;
|
||||
z-index: ${Z_INDEX.sticky};
|
||||
transition: ${({
|
||||
theme: {
|
||||
transition: { duration, timing },
|
||||
},
|
||||
}) => `${duration.slow} opacity ${timing.in}`};
|
||||
width: 100%;
|
||||
bottom: 56px;
|
||||
height: 20%;
|
||||
`
|
||||
|
||||
const InnerContainer = styled.div`
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
justify-content: space-between;
|
||||
height: 100%;
|
||||
padding: 24px 16px;
|
||||
padding: 24px 16px 16px;
|
||||
|
||||
border-radius: 20px;
|
||||
bottom: 20px;
|
||||
right: 20px;
|
||||
width: 390px;
|
||||
height: 164px;
|
||||
|
||||
border: 1px solid ${({ theme }) => theme.backgroundOutline};
|
||||
|
||||
box-shadow: ${({ theme }) => theme.deepShadow};
|
||||
|
||||
@media only screen and (max-width: ${({ theme }) => `${theme.breakpoint.md}px`}) {
|
||||
bottom: 62px;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: ${({ theme }) => `${theme.breakpoint.sm}px`}) {
|
||||
width: unset;
|
||||
right: 10px;
|
||||
left: 10px;
|
||||
}
|
||||
|
||||
user-select: none;
|
||||
`
|
||||
|
||||
const ButtonRow = styled(Row)`
|
||||
gap: 16px;
|
||||
`
|
||||
|
||||
const StyledXButton = styled(X)`
|
||||
color: ${({ theme }) => theme.textSecondary};
|
||||
&:hover {
|
||||
opacity: ${({ theme }) => theme.opacity.hover};
|
||||
}
|
||||
&:active {
|
||||
opacity: ${({ theme }) => theme.opacity.click};
|
||||
}
|
||||
cursor: pointer;
|
||||
position: absolute;
|
||||
top: 21px;
|
||||
right: 17px;
|
||||
|
||||
color: ${({ theme }) => theme.white};
|
||||
${OpacityHoverState};
|
||||
`
|
||||
|
||||
const BannerButton = styled(BaseButton)`
|
||||
height: 40px;
|
||||
border-radius: 16px;
|
||||
padding: 10px;
|
||||
${OpacityHoverState};
|
||||
`
|
||||
|
||||
export default function UniswapWalletBanner() {
|
||||
const [hideUniswapWalletBanner, toggleHideUniswapWalletBanner] = useHideUniswapWalletBanner()
|
||||
const [walletDrawerOpen] = useAccountDrawer()
|
||||
const mgtmEnabled = useMgtmEnabled()
|
||||
const location = useLocation()
|
||||
const isLandingScreen = location.search === '?intro=true' || location.pathname === '/'
|
||||
|
||||
const theme = useTheme()
|
||||
const shouldDisplay = Boolean(mgtmEnabled && !hideUniswapWalletBanner && !isLandingScreen)
|
||||
|
||||
const { pathname } = useLocation()
|
||||
// hardcodeToFalse hardcodes the banner to never display, temporarily:
|
||||
const hardcodeToFalse = false
|
||||
const shouldDisplay = Boolean(
|
||||
!walletDrawerOpen && !hideUniswapWalletBanner && isIOS && !pathname.startsWith('/wallet') && hardcodeToFalse
|
||||
)
|
||||
const screenSize = useScreenSize()
|
||||
|
||||
return (
|
||||
<PopupContainer show={shouldDisplay}>
|
||||
<InnerContainer>
|
||||
<AutoColumn gap="8px">
|
||||
<RowBetween>
|
||||
<ThemedText.SubHeader>
|
||||
<Trans>Get the power of Uniswap in your pocket</Trans>
|
||||
</ThemedText.SubHeader>
|
||||
<StyledXButton
|
||||
data-testid="uniswap-wallet-banner"
|
||||
color={theme.textSecondary}
|
||||
size={20}
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
toggleHideUniswapWalletBanner()
|
||||
}}
|
||||
/>
|
||||
</RowBetween>
|
||||
<ThemedText.BodySmall>
|
||||
<Trans>Download in the App Store today.</Trans>{' '}
|
||||
</ThemedText.BodySmall>
|
||||
</AutoColumn>
|
||||
<StyledXButton
|
||||
data-testid="uniswap-wallet-banner"
|
||||
size={20}
|
||||
onClick={(e) => {
|
||||
// prevent click from bubbling to UI on the page underneath, i.e. clicking a token row
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
toggleHideUniswapWalletBanner()
|
||||
}}
|
||||
/>
|
||||
|
||||
<ButtonRow>
|
||||
<LearnMoreButton />
|
||||
<DownloadButton onClick={() => toggleHideUniswapWalletBanner()} />
|
||||
</ButtonRow>
|
||||
</InnerContainer>
|
||||
<AutoColumn gap="8px">
|
||||
<ThemedText.HeadlineMedium fontSize="24px" lineHeight="28px" color="white" maxWidth="60%">
|
||||
<Trans>Uniswap in your pocket</Trans>
|
||||
</ThemedText.HeadlineMedium>
|
||||
</AutoColumn>
|
||||
|
||||
<ButtonRow>
|
||||
{isIOS ? (
|
||||
<>
|
||||
<BannerButton
|
||||
backgroundColor="white"
|
||||
onClick={() => openDownloadApp(InterfaceElementName.UNISWAP_WALLET_BANNER_DOWNLOAD_BUTTON)}
|
||||
>
|
||||
<AppleLogo width={14} height={14} />
|
||||
<ThemedText.LabelSmall color="black" marginLeft="5px">
|
||||
{!screenSize['xs'] ? <Trans>Download</Trans> : <Trans>Download app</Trans>}
|
||||
</ThemedText.LabelSmall>
|
||||
</BannerButton>
|
||||
|
||||
<BannerButton backgroundColor="black" onClick={openWalletMicrosite}>
|
||||
<ThemedText.LabelSmall color="white">
|
||||
<Trans>Learn more</Trans>
|
||||
</ThemedText.LabelSmall>
|
||||
</BannerButton>
|
||||
</>
|
||||
) : (
|
||||
<BannerButton backgroundColor="white" width="125px" onClick={openWalletMicrosite}>
|
||||
<ThemedText.LabelSmall color="black">
|
||||
<Trans>Learn more</Trans>
|
||||
</ThemedText.LabelSmall>
|
||||
</BannerButton>
|
||||
)}
|
||||
</ButtonRow>
|
||||
</PopupContainer>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import * as Sentry from '@sentry/react'
|
||||
import { useWeb3React } from '@web3-react/core'
|
||||
import { ButtonLight, SmallButtonPrimary } from 'components/Button'
|
||||
import { ChevronUpIcon } from 'nft/components/icons'
|
||||
import { useIsMobile } from 'nft/hooks'
|
||||
@@ -217,11 +218,13 @@ const updateServiceWorkerInBackground = async () => {
|
||||
}
|
||||
|
||||
export default function ErrorBoundary({ children }: PropsWithChildren): JSX.Element {
|
||||
const { chainId } = useWeb3React()
|
||||
return (
|
||||
<Sentry.ErrorBoundary
|
||||
fallback={({ error, eventId }) => <Fallback error={error} eventId={eventId} />}
|
||||
beforeCapture={(scope) => {
|
||||
scope.setLevel('fatal')
|
||||
scope.setTag('chain_id', chainId)
|
||||
}}
|
||||
onError={() => {
|
||||
updateServiceWorkerInBackground()
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { BaseVariant, FeatureFlag, featureFlagSettings, useBaseFlag, useUpdateFlag } from 'featureFlags'
|
||||
import { BaseVariant, FeatureFlag, featureFlagSettings, useUpdateFlag } from 'featureFlags'
|
||||
import { MgtmVariant, useMgtmFlag } from 'featureFlags/flags/mgtm'
|
||||
import { useMiniPortfolioFlag } from 'featureFlags/flags/miniPortfolio'
|
||||
import { DetailsV2Variant, useDetailsV2Flag } from 'featureFlags/flags/nftDetails'
|
||||
import { NftGraphqlVariant, useNftGraphqlFlag } from 'featureFlags/flags/nftlGraphql'
|
||||
import { PayWithAnyTokenVariant, usePayWithAnyTokenFlag } from 'featureFlags/flags/payWithAnyToken'
|
||||
import { SwapWidgetVariant, useSwapWidgetFlag } from 'featureFlags/flags/swapWidget'
|
||||
@@ -211,12 +212,6 @@ export default function FeatureFlagModal() {
|
||||
featureFlag={FeatureFlag.mgtm}
|
||||
label="Mobile Wallet go-to-market assets"
|
||||
/>
|
||||
<FeatureFlagOption
|
||||
variant={BaseVariant}
|
||||
value={useBaseFlag(FeatureFlag.walletMicrosite)}
|
||||
featureFlag={FeatureFlag.walletMicrosite}
|
||||
label="Mobile Wallet microsite (requires mgtm to also be enabled)"
|
||||
/>
|
||||
<FeatureFlagOption
|
||||
variant={BaseVariant}
|
||||
value={useMiniPortfolioFlag()}
|
||||
@@ -241,6 +236,12 @@ export default function FeatureFlagModal() {
|
||||
featureFlag={FeatureFlag.nftGraphql}
|
||||
label="Migrate NFT read endpoints to GQL"
|
||||
/>
|
||||
<FeatureFlagOption
|
||||
variant={DetailsV2Variant}
|
||||
value={useDetailsV2Flag()}
|
||||
featureFlag={FeatureFlag.detailsV2}
|
||||
label="Use the new details page for nfts"
|
||||
/>
|
||||
<FeatureFlagGroup name="Debug">
|
||||
<FeatureFlagOption
|
||||
variant={TraceJsonRpcVariant}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { getConnections } from 'connection'
|
||||
import { render } from 'test-utils'
|
||||
import { render } from 'test-utils/render'
|
||||
|
||||
import StatusIcon from './StatusIcon'
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Unicon } from 'components/Unicon'
|
||||
import { Connection, ConnectionType } from 'connection'
|
||||
import useENSAvatar from 'hooks/useENSAvatar'
|
||||
import styled from 'styled-components/macro'
|
||||
import { useIsDarkMode } from 'theme/components/ThemeToggle'
|
||||
import { flexColumnNoWrap } from 'theme/styles'
|
||||
|
||||
import sockImg from '../../assets/svg/socks.svg'
|
||||
@@ -58,9 +59,10 @@ const Socks = () => {
|
||||
}
|
||||
|
||||
const MiniWalletIcon = ({ connection, side }: { connection: Connection; side: 'left' | 'right' }) => {
|
||||
const isDarkMode = useIsDarkMode()
|
||||
return (
|
||||
<MiniIconContainer side={side}>
|
||||
<MiniImg src={connection.getIcon?.()} alt={`${connection.getName()} icon`} />
|
||||
<MiniImg src={connection.getIcon?.(isDarkMode)} alt={`${connection.getName()} icon`} />
|
||||
</MiniIconContainer>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { t, Trans } from '@lingui/macro'
|
||||
import { InterfaceElementName } from '@uniswap/analytics-events'
|
||||
import { openDownloadApp } from 'components/AccountDrawer/DownloadButton'
|
||||
import FeatureFlagModal from 'components/FeatureFlagModal/FeatureFlagModal'
|
||||
import { PrivacyPolicyModal } from 'components/PrivacyPolicy'
|
||||
import { useMgtmEnabled } from 'featureFlags/flags/mgtm'
|
||||
import { useOnClickOutside } from 'hooks/useOnClickOutside'
|
||||
import { Box } from 'nft/components/Box'
|
||||
import { Column, Row } from 'nft/components/Flex'
|
||||
@@ -21,6 +24,7 @@ import { useToggleModal } from 'state/application/hooks'
|
||||
import styled, { useTheme } from 'styled-components/macro'
|
||||
import { isDevelopmentEnv, isStagingEnv } from 'utils/env'
|
||||
|
||||
import { ReactComponent as AppleLogo } from '../../assets/svg/apple_logo.svg'
|
||||
import { ApplicationModal } from '../../state/application/reducer'
|
||||
import * as styles from './MenuDropdown.css'
|
||||
import { NavDropdown } from './NavDropdown'
|
||||
@@ -126,6 +130,8 @@ export const MenuDropdown = () => {
|
||||
const ref = useRef<HTMLDivElement>(null)
|
||||
useOnClickOutside(ref, isOpen ? toggleOpen : undefined)
|
||||
|
||||
const mgtmEnabled = useMgtmEnabled()
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box position="relative" ref={ref}>
|
||||
@@ -140,16 +146,29 @@ export const MenuDropdown = () => {
|
||||
<Box display={{ sm: 'none', lg: 'flex', xxl: 'none' }}>
|
||||
<PrimaryMenuRow to="/pool" close={toggleOpen}>
|
||||
<Icon>
|
||||
<PoolIcon width={24} height={24} color={theme.textSecondary} />
|
||||
<PoolIcon width={24} height={24} fill={theme.textPrimary} />
|
||||
</Icon>
|
||||
<PrimaryMenuRow.Text>
|
||||
<Trans>Pool</Trans>
|
||||
</PrimaryMenuRow.Text>
|
||||
</PrimaryMenuRow>
|
||||
</Box>
|
||||
<Box
|
||||
display={mgtmEnabled ? 'flex' : 'none'}
|
||||
onClick={() => openDownloadApp(InterfaceElementName.UNISWAP_WALLET_MODAL_DOWNLOAD_BUTTON)}
|
||||
>
|
||||
<PrimaryMenuRow close={toggleOpen}>
|
||||
<Icon>
|
||||
<AppleLogo width="24px" height="24px" fill={theme.textPrimary} />
|
||||
</Icon>
|
||||
<PrimaryMenuRow.Text>
|
||||
<Trans>Download Uniswap Wallet</Trans>
|
||||
</PrimaryMenuRow.Text>
|
||||
</PrimaryMenuRow>
|
||||
</Box>
|
||||
<PrimaryMenuRow to="/vote" close={toggleOpen}>
|
||||
<Icon>
|
||||
<GovernanceIcon width={24} height={24} color={theme.textSecondary} />
|
||||
<GovernanceIcon width={24} height={24} color={theme.textPrimary} />
|
||||
</Icon>
|
||||
<PrimaryMenuRow.Text>
|
||||
<Trans>Vote in governance</Trans>
|
||||
@@ -157,7 +176,7 @@ export const MenuDropdown = () => {
|
||||
</PrimaryMenuRow>
|
||||
<PrimaryMenuRow href="https://info.uniswap.org/#/">
|
||||
<Icon>
|
||||
<BarChartIcon width={24} height={24} color={theme.textSecondary} />
|
||||
<BarChartIcon width={24} height={24} color={theme.textPrimary} />
|
||||
</Icon>
|
||||
<PrimaryMenuRow.Text>
|
||||
<Trans>View more analytics</Trans>
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import { useWeb3React } from '@web3-react/core'
|
||||
import NewBadge from 'components/WalletModal/NewBadge'
|
||||
import Web3Status from 'components/Web3Status'
|
||||
import { useMGTMMicrositeEnabled } from 'featureFlags/flags/mgtm'
|
||||
import { chainIdToBackendName } from 'graphql/data/util'
|
||||
import { useIsNftPage } from 'hooks/useIsNftPage'
|
||||
import { useIsPoolsPage } from 'hooks/useIsPoolsPage'
|
||||
@@ -60,7 +58,6 @@ export const PageTabs = () => {
|
||||
|
||||
const isPoolActive = useIsPoolsPage()
|
||||
const isNftPage = useIsNftPage()
|
||||
const micrositeEnabled = useMGTMMicrositeEnabled()
|
||||
|
||||
const shouldDisableNFTRoutes = useAtomValue(shouldDisableNFTRoutesAtom)
|
||||
|
||||
@@ -82,14 +79,6 @@ export const PageTabs = () => {
|
||||
<Trans>Pools</Trans>
|
||||
</MenuItem>
|
||||
</Box>
|
||||
{micrositeEnabled && (
|
||||
<Box display={{ sm: 'none', xxxl: 'flex' }}>
|
||||
<MenuItem href="/wallet" isActive={pathname.startsWith('/wallet')}>
|
||||
<Trans>Wallet</Trans>
|
||||
<NewBadge />
|
||||
</MenuItem>
|
||||
</Box>
|
||||
)}
|
||||
<Box marginY={{ sm: '4', md: 'unset' }}>
|
||||
<MenuDropdown />
|
||||
</Box>
|
||||
|
||||
@@ -1,38 +1,49 @@
|
||||
import { useCallback, useEffect } from 'react'
|
||||
import { useEffect } from 'react'
|
||||
import { X } from 'react-feather'
|
||||
import styled, { useTheme } from 'styled-components/macro'
|
||||
import styled, { css, useTheme } from 'styled-components/macro'
|
||||
|
||||
import { useRemovePopup } from '../../state/application/hooks'
|
||||
import { PopupContent } from '../../state/application/reducer'
|
||||
import FailedNetworkSwitchPopup from './FailedNetworkSwitchPopup'
|
||||
import TransactionPopup from './TransactionPopup'
|
||||
|
||||
const StyledClose = styled(X)`
|
||||
const StyledClose = styled(X)<{ $padding: number }>`
|
||||
position: absolute;
|
||||
right: 20px;
|
||||
top: 20px;
|
||||
right: ${({ $padding }) => `${$padding}px`};
|
||||
top: ${({ $padding }) => `${$padding}px`};
|
||||
|
||||
:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
`
|
||||
const Popup = styled.div`
|
||||
const PopupCss = css<{ show: boolean }>`
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
padding: 1em;
|
||||
visibility: ${({ show }) => (show ? 'visible' : 'hidden')};
|
||||
background-color: ${({ theme }) => theme.backgroundSurface};
|
||||
position: relative;
|
||||
border: 1px solid ${({ theme }) => theme.backgroundOutline};
|
||||
border-radius: 16px;
|
||||
padding: 20px;
|
||||
padding-right: 35px;
|
||||
overflow: hidden;
|
||||
box-shadow: ${({ theme }) => theme.deepShadow};
|
||||
transition: ${({ theme }) => `visibility ${theme.transition.duration.fast} ease-in-out`};
|
||||
|
||||
${({ theme }) => theme.deprecated_mediaWidth.deprecated_upToSmall`
|
||||
min-width: 290px;
|
||||
&:not(:last-of-type) {
|
||||
margin-right: 20px;
|
||||
}
|
||||
`}
|
||||
min-width: 290px;
|
||||
&:not(:last-of-type) {
|
||||
margin-right: 20px;
|
||||
}
|
||||
`}
|
||||
`
|
||||
|
||||
const TransactionPopupContainer = styled.div`
|
||||
${PopupCss}
|
||||
padding: 2px 0px;
|
||||
`
|
||||
|
||||
const FailedSwitchNetworkPopupContainer = styled.div<{ show: boolean }>`
|
||||
${PopupCss}
|
||||
padding: 20px 35px 20px 20px;
|
||||
`
|
||||
|
||||
export default function PopupItem({
|
||||
@@ -45,32 +56,34 @@ export default function PopupItem({
|
||||
popKey: string
|
||||
}) {
|
||||
const removePopup = useRemovePopup()
|
||||
const removeThisPopup = useCallback(() => removePopup(popKey), [popKey, removePopup])
|
||||
const theme = useTheme()
|
||||
|
||||
useEffect(() => {
|
||||
if (removeAfterMs === null) return undefined
|
||||
|
||||
const timeout = setTimeout(() => {
|
||||
removeThisPopup()
|
||||
removePopup(popKey)
|
||||
}, removeAfterMs)
|
||||
|
||||
return () => {
|
||||
clearTimeout(timeout)
|
||||
}
|
||||
}, [removeAfterMs, removeThisPopup])
|
||||
}, [popKey, removeAfterMs, removePopup])
|
||||
|
||||
const theme = useTheme()
|
||||
|
||||
let popupContent
|
||||
if ('txn' in content) {
|
||||
popupContent = <TransactionPopup hash={content.txn.hash} />
|
||||
return (
|
||||
<TransactionPopupContainer show={true}>
|
||||
<StyledClose $padding={16} color={theme.textSecondary} onClick={() => removePopup(popKey)} />
|
||||
<TransactionPopup hash={content.txn.hash} />
|
||||
</TransactionPopupContainer>
|
||||
)
|
||||
} else if ('failedSwitchNetwork' in content) {
|
||||
popupContent = <FailedNetworkSwitchPopup chainId={content.failedSwitchNetwork} />
|
||||
return (
|
||||
<FailedSwitchNetworkPopupContainer show={true}>
|
||||
<StyledClose $padding={20} color={theme.textSecondary} onClick={() => removePopup(popKey)} />
|
||||
<FailedNetworkSwitchPopup chainId={content.failedSwitchNetwork} />
|
||||
</FailedSwitchNetworkPopupContainer>
|
||||
)
|
||||
}
|
||||
|
||||
return popupContent ? (
|
||||
<Popup>
|
||||
<StyledClose color={theme.textSecondary} onClick={removeThisPopup} />
|
||||
{popupContent}
|
||||
</Popup>
|
||||
) : null
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -41,7 +41,7 @@ const FixedPopupColumn = styled(AutoColumn)<{ extraPadding: boolean; xlPadding:
|
||||
position: fixed;
|
||||
top: ${({ extraPadding }) => (extraPadding ? '72px' : '64px')};
|
||||
right: 1rem;
|
||||
max-width: 376px !important;
|
||||
max-width: 348px !important;
|
||||
width: 100%;
|
||||
z-index: 3;
|
||||
|
||||
|
||||
@@ -5,19 +5,17 @@ import { USDC_MAINNET } from 'constants/tokens'
|
||||
import { useToken } from 'hooks/Tokens'
|
||||
import { usePool } from 'hooks/usePools'
|
||||
import { PoolState } from 'hooks/usePools'
|
||||
import { render } from 'test-utils'
|
||||
import { mocked } from 'test-utils/mocked'
|
||||
import { render } from 'test-utils/render'
|
||||
import { unwrappedToken } from 'utils/unwrappedToken'
|
||||
|
||||
import PositionListItem from '.'
|
||||
|
||||
jest.mock('utils/unwrappedToken')
|
||||
const mockUnwrappedToken = unwrappedToken as jest.MockedFunction<typeof unwrappedToken>
|
||||
|
||||
jest.mock('hooks/usePools')
|
||||
const mockUsePool = usePool as jest.MockedFunction<typeof usePool>
|
||||
|
||||
jest.mock('hooks/Tokens')
|
||||
const mockUseToken = useToken as jest.MockedFunction<typeof useToken>
|
||||
|
||||
// eslint-disable-next-line react/display-name
|
||||
jest.mock('components/DoubleLogo', () => () => <div />)
|
||||
@@ -36,16 +34,16 @@ const susToken0Address = '0x39AA39c021dfbaE8faC545936693aC917d5E7563'
|
||||
|
||||
beforeEach(() => {
|
||||
const susToken0 = new Token(1, susToken0Address, 8, 'https://www.example.com', 'example.com coin')
|
||||
mockUseToken.mockImplementation((tokenAddress?: string | null | undefined) => {
|
||||
mocked(useToken).mockImplementation((tokenAddress?: string | null | undefined) => {
|
||||
if (!tokenAddress) return null
|
||||
if (tokenAddress === susToken0.address) return susToken0
|
||||
return new Token(1, tokenAddress, 8, 'symbol', 'name')
|
||||
})
|
||||
mockUsePool.mockReturnValue([
|
||||
mocked(usePool).mockReturnValue([
|
||||
PoolState.EXISTS,
|
||||
new Pool(susToken0, USDC_MAINNET, FeeAmount.HIGH, '2437312313659959819381354528', '10272714736694327408', -69633),
|
||||
])
|
||||
mockUnwrappedToken.mockReturnValue(susToken0)
|
||||
mocked(unwrappedToken).mockReturnValue(susToken0)
|
||||
})
|
||||
|
||||
test('PositionListItem should not render when token0 symbol contains a url', () => {
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Currency, Percent } from '@uniswap/sdk-core'
|
||||
import { FeeAmount } from '@uniswap/v3-sdk'
|
||||
import { RoutingDiagramEntry } from 'components/swap/SwapRoute'
|
||||
import { DAI, USDC_MAINNET, WBTC } from 'constants/tokens'
|
||||
import { render } from 'test-utils'
|
||||
import { render } from 'test-utils/render'
|
||||
|
||||
import RoutingDiagram from './RoutingDiagram'
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import { screen } from '@testing-library/react'
|
||||
import { Currency, CurrencyAmount as mockCurrencyAmount, Token as mockToken } from '@uniswap/sdk-core'
|
||||
import { DAI, USDC_MAINNET, WBTC } from 'constants/tokens'
|
||||
import * as mockJSBI from 'jsbi'
|
||||
import { render } from 'test-utils'
|
||||
import { render } from 'test-utils/render'
|
||||
|
||||
import CurrencyList from '.'
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { fireEvent, render, screen } from 'test-utils'
|
||||
import { fireEvent, render, screen } from 'test-utils/render'
|
||||
import noop from 'utils/noop'
|
||||
|
||||
import { ResizingTextArea, TextInput } from './'
|
||||
|
||||
@@ -8,7 +9,7 @@ describe('TextInput', () => {
|
||||
<TextInput
|
||||
className="testing"
|
||||
value="My test input"
|
||||
onUserInput={() => null}
|
||||
onUserInput={noop}
|
||||
placeholder="Test Placeholder"
|
||||
fontSize="12"
|
||||
/>
|
||||
@@ -41,7 +42,7 @@ describe('ResizableTextArea', () => {
|
||||
<ResizingTextArea
|
||||
className="testing"
|
||||
value="My test input"
|
||||
onUserInput={() => null}
|
||||
onUserInput={noop}
|
||||
placeholder="Test Placeholder"
|
||||
fontSize="12"
|
||||
/>
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
import { transparentize } from 'polished'
|
||||
import { ReactNode, useEffect, useState } from 'react'
|
||||
import styled from 'styled-components/macro'
|
||||
import noop from 'utils/noop'
|
||||
|
||||
import Popover, { PopoverProps } from '../Popover'
|
||||
|
||||
// TODO(WEB-3163): migrate noops throughout web to a shared util file.
|
||||
const noop = () => null
|
||||
|
||||
export const TooltipContainer = styled.div`
|
||||
max-width: 256px;
|
||||
cursor: default;
|
||||
|
||||
@@ -3,6 +3,7 @@ import { BrowserEvent, InterfaceElementName, InterfaceEventName } from '@uniswap
|
||||
import Loader from 'components/Icons/LoadingSpinner'
|
||||
import { Connection, ConnectionType } from 'connection'
|
||||
import styled from 'styled-components/macro'
|
||||
import { useIsDarkMode } from 'theme/components/ThemeToggle'
|
||||
import { flexColumnNoWrap, flexRowNoWrap } from 'theme/styles'
|
||||
|
||||
import NewBadge from './NewBadge'
|
||||
@@ -68,6 +69,7 @@ type OptionProps = {
|
||||
}
|
||||
export default function Option({ connection, pendingConnectionType, activate }: OptionProps) {
|
||||
const isPending = pendingConnectionType === connection.type
|
||||
const isDarkMode = useIsDarkMode()
|
||||
const content = (
|
||||
<TraceEvent
|
||||
events={[BrowserEvent.onClick]}
|
||||
@@ -83,7 +85,7 @@ export default function Option({ connection, pendingConnectionType, activate }:
|
||||
>
|
||||
<OptionCardLeft>
|
||||
<IconWrapper>
|
||||
<img src={connection.getIcon?.()} alt="Icon" />
|
||||
<img src={connection.getIcon?.(isDarkMode)} alt="Icon" />
|
||||
</IconWrapper>
|
||||
<HeaderText>{connection.getName()}</HeaderText>
|
||||
{connection.isNew && <NewBadge />}
|
||||
|
||||
@@ -156,15 +156,16 @@ export default function WalletModal({ openSettings }: { openSettings: () => void
|
||||
setPendingError(undefined)
|
||||
|
||||
await connection.connector.activate()
|
||||
console.debug(`connection activated: ${connection.getName()}`)
|
||||
dispatch(updateSelectedWallet({ wallet: connection.type }))
|
||||
if (drawerOpenRef.current) toggleWalletDrawer()
|
||||
} catch (error) {
|
||||
console.debug(`web3-react connection error: ${JSON.stringify(error)}`)
|
||||
// TODO(WEB-3162): re-add special treatment for already-pending injected errors
|
||||
if (didUserReject(connection, error)) {
|
||||
setPendingConnection(undefined)
|
||||
} // Prevents showing error caused by MetaMask being prompted twice
|
||||
else if (error?.code !== ErrorCode.MM_ALREADY_PENDING) {
|
||||
console.debug(`web3-react connection error: ${error}`)
|
||||
setPendingError(error.message)
|
||||
} else {
|
||||
setPendingError(error)
|
||||
|
||||
sendAnalyticsEvent(InterfaceEventName.WALLET_CONNECT_TXN_COMPLETED, {
|
||||
result: WalletConnectionResult.FAILED,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { useWeb3React } from '@web3-react/core'
|
||||
import { useAccountDrawer } from 'components/AccountDrawer'
|
||||
import { fireEvent, render, screen } from 'test-utils'
|
||||
import { fireEvent, render, screen } from 'test-utils/render'
|
||||
|
||||
import { useFiatOnrampAvailability, useOpenModal } from '../../state/application/hooks'
|
||||
import SwapBuyFiatButton, { MOONPAY_REGION_AVAILABILITY_ARTICLE } from './SwapBuyFiatButton'
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import INJECTED_DARK_ICON from 'assets/svg/browser-wallet-dark.svg'
|
||||
import INJECTED_LIGHT_ICON from 'assets/svg/browser-wallet-light.svg'
|
||||
import { ConnectionType, getConnections, useGetConnection } from 'connection'
|
||||
import { renderHook } from 'test-utils'
|
||||
import { renderHook } from 'test-utils/render'
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetModules()
|
||||
@@ -108,6 +110,9 @@ describe('connection utility/metadata tests', () => {
|
||||
expect(injected.getName()).toBe('Browser Wallet')
|
||||
expect(injected.overrideActivate?.()).toBeFalsy()
|
||||
|
||||
expect(injected.getIcon?.(/* isDarkMode */ false)).toBe(INJECTED_LIGHT_ICON)
|
||||
expect(injected.getIcon?.(/* isDarkMode */ true)).toBe(INJECTED_DARK_ICON)
|
||||
|
||||
// Ensures we provide multiple connection options if in an unknown injected browser
|
||||
expect(displayed.length).toEqual(4)
|
||||
})
|
||||
|
||||
@@ -4,13 +4,14 @@ import { GnosisSafe } from '@web3-react/gnosis-safe'
|
||||
import { MetaMask } from '@web3-react/metamask'
|
||||
import { Network } from '@web3-react/network'
|
||||
import { Connector } from '@web3-react/types'
|
||||
import COINBASE_ICON_URL from 'assets/images/coinbaseWalletIcon.svg'
|
||||
import GNOSIS_ICON_URL from 'assets/images/gnosis.png'
|
||||
import METAMASK_ICON_URL from 'assets/images/metamask.svg'
|
||||
import UNIWALLET_ICON_URL from 'assets/images/uniwallet.svg'
|
||||
import WALLET_CONNECT_ICON_URL from 'assets/images/walletConnectIcon.svg'
|
||||
import INJECTED_LIGHT_ICON_URL from 'assets/svg/browser-wallet-light.svg'
|
||||
import UNISWAP_LOGO_URL from 'assets/svg/logo.svg'
|
||||
import COINBASE_ICON from 'assets/images/coinbaseWalletIcon.svg'
|
||||
import GNOSIS_ICON from 'assets/images/gnosis.png'
|
||||
import METAMASK_ICON from 'assets/images/metamask.svg'
|
||||
import UNIWALLET_ICON from 'assets/images/uniwallet.svg'
|
||||
import WALLET_CONNECT_ICON from 'assets/images/walletConnectIcon.svg'
|
||||
import INJECTED_DARK_ICON from 'assets/svg/browser-wallet-dark.svg'
|
||||
import INJECTED_LIGHT_ICON from 'assets/svg/browser-wallet-light.svg'
|
||||
import UNISWAP_LOGO from 'assets/svg/logo.svg'
|
||||
import { SupportedChainId } from 'constants/chains'
|
||||
import { useCallback } from 'react'
|
||||
import { isMobile, isNonIOSPhone } from 'utils/userAgent'
|
||||
@@ -34,8 +35,7 @@ export interface Connection {
|
||||
connector: Connector
|
||||
hooks: Web3ReactHooks
|
||||
type: ConnectionType
|
||||
// TODO(WEB-3130): add darkmode check for icons
|
||||
getIcon?(): string
|
||||
getIcon?(isDarkMode: boolean): string
|
||||
shouldDisplay(): boolean
|
||||
overrideActivate?: () => boolean
|
||||
isNew?: boolean
|
||||
@@ -65,13 +65,15 @@ const getShouldAdvertiseMetaMask = () =>
|
||||
const getIsGenericInjector = () => getIsInjected() && !getIsMetaMaskWallet() && !getIsCoinbaseWallet()
|
||||
|
||||
const [web3Injected, web3InjectedHooks] = initializeConnector<MetaMask>((actions) => new MetaMask({ actions, onError }))
|
||||
|
||||
const injectedConnection: Connection = {
|
||||
// TODO(WEB-3131) re-add "Install MetaMask" string when no injector is present
|
||||
getName: () => (getIsGenericInjector() ? 'Browser Wallet' : 'MetaMask'),
|
||||
connector: web3Injected,
|
||||
hooks: web3InjectedHooks,
|
||||
type: ConnectionType.INJECTED,
|
||||
getIcon: () => (getIsGenericInjector() ? INJECTED_LIGHT_ICON_URL : METAMASK_ICON_URL),
|
||||
getIcon: (isDarkMode: boolean) =>
|
||||
getIsGenericInjector() ? (isDarkMode ? INJECTED_DARK_ICON : INJECTED_LIGHT_ICON) : METAMASK_ICON,
|
||||
shouldDisplay: () => getIsMetaMaskWallet() || getShouldAdvertiseMetaMask() || getIsGenericInjector(),
|
||||
// If on non-injected, non-mobile browser, prompt user to install Metamask
|
||||
overrideActivate: () => {
|
||||
@@ -82,14 +84,13 @@ const injectedConnection: Connection = {
|
||||
return false
|
||||
},
|
||||
}
|
||||
|
||||
const [web3GnosisSafe, web3GnosisSafeHooks] = initializeConnector<GnosisSafe>((actions) => new GnosisSafe({ actions }))
|
||||
export const gnosisSafeConnection: Connection = {
|
||||
getName: () => 'Gnosis Safe',
|
||||
connector: web3GnosisSafe,
|
||||
hooks: web3GnosisSafeHooks,
|
||||
type: ConnectionType.GNOSIS_SAFE,
|
||||
getIcon: () => GNOSIS_ICON_URL,
|
||||
getIcon: () => GNOSIS_ICON,
|
||||
shouldDisplay: () => false,
|
||||
}
|
||||
|
||||
@@ -101,7 +102,7 @@ export const walletConnectConnection: Connection = {
|
||||
connector: web3WalletConnect,
|
||||
hooks: web3WalletConnectHooks,
|
||||
type: ConnectionType.WALLET_CONNECT,
|
||||
getIcon: () => WALLET_CONNECT_ICON_URL,
|
||||
getIcon: () => WALLET_CONNECT_ICON,
|
||||
shouldDisplay: () => !getIsInjectedMobileBrowser(),
|
||||
}
|
||||
|
||||
@@ -113,7 +114,7 @@ export const uniwalletConnectConnection: Connection = {
|
||||
connector: web3UniwalletConnect,
|
||||
hooks: web3UniwalletConnectHooks,
|
||||
type: ConnectionType.UNIWALLET,
|
||||
getIcon: () => UNIWALLET_ICON_URL,
|
||||
getIcon: () => UNIWALLET_ICON,
|
||||
shouldDisplay: () => Boolean(!getIsInjectedMobileBrowser() && !isNonIOSPhone),
|
||||
isNew: true,
|
||||
}
|
||||
@@ -125,7 +126,7 @@ const [web3CoinbaseWallet, web3CoinbaseWalletHooks] = initializeConnector<Coinba
|
||||
options: {
|
||||
url: RPC_URLS[SupportedChainId.MAINNET][0],
|
||||
appName: 'Uniswap',
|
||||
appLogoUrl: UNISWAP_LOGO_URL,
|
||||
appLogoUrl: UNISWAP_LOGO,
|
||||
reloadOnDisconnect: false,
|
||||
},
|
||||
onError,
|
||||
@@ -137,7 +138,7 @@ const coinbaseWalletConnection: Connection = {
|
||||
connector: web3CoinbaseWallet,
|
||||
hooks: web3CoinbaseWalletHooks,
|
||||
type: ConnectionType.COINBASE_WALLET,
|
||||
getIcon: () => COINBASE_ICON_URL,
|
||||
getIcon: () => COINBASE_ICON,
|
||||
shouldDisplay: () =>
|
||||
Boolean((isMobile && !getIsInjectedMobileBrowser()) || !isMobile || getIsCoinbaseWalletBrowser()),
|
||||
// If on a mobile browser that isn't the coinbase wallet browser, deeplink to the coinbase wallet app
|
||||
|
||||
@@ -8,7 +8,7 @@ export const DEFAULT_DEADLINE_FROM_NOW = 60 * 30
|
||||
export const L2_DEADLINE_FROM_NOW = 60 * 5
|
||||
|
||||
// transaction popup dismisal amounts
|
||||
export const DEFAULT_TXN_DISMISS_MS = 25000
|
||||
export const DEFAULT_TXN_DISMISS_MS = 10000
|
||||
export const L2_TXN_DISMISS_MS = 5000
|
||||
|
||||
export const BIG_INT_ZERO = JSBI.BigInt(0)
|
||||
|
||||
@@ -10,6 +10,6 @@ export enum FeatureFlag {
|
||||
statsigDummy = 'web_dummy_gate_amplitude_id',
|
||||
nftGraphql = 'nft_graphql_migration',
|
||||
mgtm = 'web_mobile_go_to_market_enabled',
|
||||
walletMicrosite = 'walletMicrosite',
|
||||
miniPortfolio = 'miniPortfolio',
|
||||
detailsV2 = 'details_v2',
|
||||
}
|
||||
|
||||
@@ -10,9 +10,4 @@ export function useMgtmEnabled(): boolean {
|
||||
return useMgtmFlag() === BaseVariant.Enabled
|
||||
}
|
||||
|
||||
export function useMGTMMicrositeEnabled() {
|
||||
const mgtmEnabled = useMgtmEnabled()
|
||||
return useBaseFlag(FeatureFlag.walletMicrosite) === BaseVariant.Enabled && mgtmEnabled
|
||||
}
|
||||
|
||||
export { BaseVariant as MgtmVariant }
|
||||
|
||||
11
src/featureFlags/flags/nftDetails.ts
Normal file
11
src/featureFlags/flags/nftDetails.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { BaseVariant, FeatureFlag, useBaseFlag } from '../index'
|
||||
|
||||
export function useDetailsV2Flag(): BaseVariant {
|
||||
return useBaseFlag(FeatureFlag.detailsV2)
|
||||
}
|
||||
|
||||
export function useDetailsV2Enabled(): boolean {
|
||||
return useDetailsV2Flag() === BaseVariant.Enabled
|
||||
}
|
||||
|
||||
export { BaseVariant as DetailsV2Variant }
|
||||
@@ -4,6 +4,7 @@ import { DAI, USDC_MAINNET } from 'constants/tokens'
|
||||
import { RouterPreference } from 'state/routing/slice'
|
||||
import { TradeState } from 'state/routing/types'
|
||||
import { useClientSideRouter } from 'state/user/hooks'
|
||||
import { mocked } from 'test-utils/mocked'
|
||||
|
||||
import { useRoutingAPITrade } from '../state/routing/useRoutingAPITrade'
|
||||
import useAutoRouterSupported from './useAutoRouterSupported'
|
||||
@@ -29,53 +30,45 @@ jest.mock('./useIsWindowVisible')
|
||||
jest.mock('state/routing/useRoutingAPITrade')
|
||||
jest.mock('state/user/hooks')
|
||||
|
||||
const mockUseDebounce = useDebounce as jest.MockedFunction<typeof useDebounce>
|
||||
const mockUseAutoRouterSupported = useAutoRouterSupported as jest.MockedFunction<typeof useAutoRouterSupported>
|
||||
const mockUseIsWindowVisible = useIsWindowVisible as jest.MockedFunction<typeof useIsWindowVisible>
|
||||
|
||||
const mockUseRoutingAPITrade = useRoutingAPITrade as jest.MockedFunction<typeof useRoutingAPITrade>
|
||||
const mockUseClientSideRouter = useClientSideRouter as jest.MockedFunction<typeof useClientSideRouter>
|
||||
const mockUseClientSideV3Trade = useClientSideV3Trade as jest.MockedFunction<typeof useClientSideV3Trade>
|
||||
|
||||
// helpers to set mock expectations
|
||||
const expectRouterMock = (state: TradeState) => {
|
||||
mockUseRoutingAPITrade.mockReturnValue({ state, trade: undefined })
|
||||
mocked(useRoutingAPITrade).mockReturnValue({ state, trade: undefined })
|
||||
}
|
||||
|
||||
const expectClientSideMock = (state: TradeState) => {
|
||||
mockUseClientSideV3Trade.mockReturnValue({ state, trade: undefined })
|
||||
mocked(useClientSideV3Trade).mockReturnValue({ state, trade: undefined })
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
// ignore debounced value
|
||||
mockUseDebounce.mockImplementation((value) => value)
|
||||
mocked(useDebounce).mockImplementation((value) => value)
|
||||
|
||||
mockUseIsWindowVisible.mockReturnValue(true)
|
||||
mockUseAutoRouterSupported.mockReturnValue(true)
|
||||
mockUseClientSideRouter.mockReturnValue([true, () => undefined])
|
||||
mocked(useIsWindowVisible).mockReturnValue(true)
|
||||
mocked(useAutoRouterSupported).mockReturnValue(true)
|
||||
mocked(useClientSideRouter).mockReturnValue([true, () => undefined])
|
||||
})
|
||||
|
||||
describe('#useBestV3Trade ExactIn', () => {
|
||||
it('does not compute routing api trade when routing API is not supported', async () => {
|
||||
mockUseAutoRouterSupported.mockReturnValue(false)
|
||||
mocked(useAutoRouterSupported).mockReturnValue(false)
|
||||
expectRouterMock(TradeState.INVALID)
|
||||
expectClientSideMock(TradeState.VALID)
|
||||
|
||||
const { result } = renderHook(() => useBestTrade(TradeType.EXACT_INPUT, USDCAmount, DAI))
|
||||
|
||||
expect(mockUseRoutingAPITrade).toHaveBeenCalledWith(TradeType.EXACT_INPUT, undefined, DAI, RouterPreference.CLIENT)
|
||||
expect(mockUseClientSideV3Trade).toHaveBeenCalledWith(TradeType.EXACT_INPUT, USDCAmount, DAI)
|
||||
expect(useRoutingAPITrade).toHaveBeenCalledWith(TradeType.EXACT_INPUT, undefined, DAI, RouterPreference.CLIENT)
|
||||
expect(useClientSideV3Trade).toHaveBeenCalledWith(TradeType.EXACT_INPUT, USDCAmount, DAI)
|
||||
expect(result.current).toEqual({ state: TradeState.VALID, trade: undefined })
|
||||
})
|
||||
|
||||
it('does not compute routing api trade when window is not focused', async () => {
|
||||
mockUseIsWindowVisible.mockReturnValue(false)
|
||||
mocked(useIsWindowVisible).mockReturnValue(false)
|
||||
expectRouterMock(TradeState.NO_ROUTE_FOUND)
|
||||
expectClientSideMock(TradeState.VALID)
|
||||
|
||||
const { result } = renderHook(() => useBestTrade(TradeType.EXACT_INPUT, USDCAmount, DAI))
|
||||
|
||||
expect(mockUseRoutingAPITrade).toHaveBeenCalledWith(TradeType.EXACT_INPUT, undefined, DAI, RouterPreference.CLIENT)
|
||||
expect(useRoutingAPITrade).toHaveBeenCalledWith(TradeType.EXACT_INPUT, undefined, DAI, RouterPreference.CLIENT)
|
||||
expect(result.current).toEqual({ state: TradeState.NO_ROUTE_FOUND, trade: undefined })
|
||||
})
|
||||
|
||||
@@ -85,7 +78,7 @@ describe('#useBestV3Trade ExactIn', () => {
|
||||
|
||||
const { result } = renderHook(() => useBestTrade(TradeType.EXACT_INPUT, USDCAmount, DAI))
|
||||
|
||||
expect(mockUseClientSideV3Trade).toHaveBeenCalledWith(TradeType.EXACT_INPUT, undefined, undefined)
|
||||
expect(useClientSideV3Trade).toHaveBeenCalledWith(TradeType.EXACT_INPUT, undefined, undefined)
|
||||
expect(result.current).toEqual({ state: TradeState.LOADING, trade: undefined })
|
||||
})
|
||||
|
||||
@@ -94,7 +87,7 @@ describe('#useBestV3Trade ExactIn', () => {
|
||||
|
||||
const { result } = renderHook(() => useBestTrade(TradeType.EXACT_INPUT, USDCAmount, DAI))
|
||||
|
||||
expect(mockUseClientSideV3Trade).toHaveBeenCalledWith(TradeType.EXACT_INPUT, undefined, undefined)
|
||||
expect(useClientSideV3Trade).toHaveBeenCalledWith(TradeType.EXACT_INPUT, undefined, undefined)
|
||||
expect(result.current).toEqual({ state: TradeState.VALID, trade: undefined })
|
||||
})
|
||||
|
||||
@@ -103,7 +96,7 @@ describe('#useBestV3Trade ExactIn', () => {
|
||||
|
||||
const { result } = renderHook(() => useBestTrade(TradeType.EXACT_INPUT, USDCAmount, DAI))
|
||||
|
||||
expect(mockUseClientSideV3Trade).toHaveBeenCalledWith(TradeType.EXACT_INPUT, undefined, undefined)
|
||||
expect(useClientSideV3Trade).toHaveBeenCalledWith(TradeType.EXACT_INPUT, undefined, undefined)
|
||||
expect(result.current).toEqual({ state: TradeState.SYNCING, trade: undefined })
|
||||
})
|
||||
})
|
||||
@@ -115,7 +108,7 @@ describe('#useBestV3Trade ExactIn', () => {
|
||||
|
||||
renderHook(() => useBestTrade(TradeType.EXACT_INPUT, USDCAmount, DAI))
|
||||
|
||||
expect(mockUseClientSideV3Trade).toHaveBeenCalledWith(TradeType.EXACT_INPUT, undefined, undefined)
|
||||
expect(useClientSideV3Trade).toHaveBeenCalledWith(TradeType.EXACT_INPUT, undefined, undefined)
|
||||
})
|
||||
|
||||
it('computes client side v3 trade if routing api is NO_ROUTE_FOUND', () => {
|
||||
@@ -124,7 +117,7 @@ describe('#useBestV3Trade ExactIn', () => {
|
||||
|
||||
const { result } = renderHook(() => useBestTrade(TradeType.EXACT_INPUT, USDCAmount, DAI))
|
||||
|
||||
expect(mockUseClientSideV3Trade).toHaveBeenCalledWith(TradeType.EXACT_INPUT, USDCAmount, DAI)
|
||||
expect(useClientSideV3Trade).toHaveBeenCalledWith(TradeType.EXACT_INPUT, USDCAmount, DAI)
|
||||
expect(result.current).toEqual({ state: TradeState.VALID, trade: undefined })
|
||||
})
|
||||
})
|
||||
@@ -132,30 +125,30 @@ describe('#useBestV3Trade ExactIn', () => {
|
||||
|
||||
describe('#useBestV3Trade ExactOut', () => {
|
||||
it('does not compute routing api trade when routing API is not supported', () => {
|
||||
mockUseAutoRouterSupported.mockReturnValue(false)
|
||||
mocked(useAutoRouterSupported).mockReturnValue(false)
|
||||
expectRouterMock(TradeState.INVALID)
|
||||
expectClientSideMock(TradeState.VALID)
|
||||
|
||||
const { result } = renderHook(() => useBestTrade(TradeType.EXACT_OUTPUT, DAIAmount, USDC_MAINNET))
|
||||
|
||||
expect(mockUseRoutingAPITrade).toHaveBeenCalledWith(
|
||||
expect(useRoutingAPITrade).toHaveBeenCalledWith(
|
||||
TradeType.EXACT_OUTPUT,
|
||||
undefined,
|
||||
USDC_MAINNET,
|
||||
RouterPreference.CLIENT
|
||||
)
|
||||
expect(mockUseClientSideV3Trade).toHaveBeenCalledWith(TradeType.EXACT_OUTPUT, DAIAmount, USDC_MAINNET)
|
||||
expect(useClientSideV3Trade).toHaveBeenCalledWith(TradeType.EXACT_OUTPUT, DAIAmount, USDC_MAINNET)
|
||||
expect(result.current).toEqual({ state: TradeState.VALID, trade: undefined })
|
||||
})
|
||||
|
||||
it('does not compute routing api trade when window is not focused', () => {
|
||||
mockUseIsWindowVisible.mockReturnValue(false)
|
||||
mocked(useIsWindowVisible).mockReturnValue(false)
|
||||
expectRouterMock(TradeState.NO_ROUTE_FOUND)
|
||||
expectClientSideMock(TradeState.VALID)
|
||||
|
||||
const { result } = renderHook(() => useBestTrade(TradeType.EXACT_OUTPUT, DAIAmount, USDC_MAINNET))
|
||||
|
||||
expect(mockUseRoutingAPITrade).toHaveBeenCalledWith(
|
||||
expect(useRoutingAPITrade).toHaveBeenCalledWith(
|
||||
TradeType.EXACT_OUTPUT,
|
||||
undefined,
|
||||
USDC_MAINNET,
|
||||
@@ -169,7 +162,7 @@ describe('#useBestV3Trade ExactOut', () => {
|
||||
|
||||
const { result } = renderHook(() => useBestTrade(TradeType.EXACT_OUTPUT, DAIAmount, USDC_MAINNET))
|
||||
|
||||
expect(mockUseClientSideV3Trade).toHaveBeenCalledWith(TradeType.EXACT_OUTPUT, undefined, undefined)
|
||||
expect(useClientSideV3Trade).toHaveBeenCalledWith(TradeType.EXACT_OUTPUT, undefined, undefined)
|
||||
expect(result.current).toEqual({ state: TradeState.LOADING, trade: undefined })
|
||||
})
|
||||
|
||||
@@ -178,7 +171,7 @@ describe('#useBestV3Trade ExactOut', () => {
|
||||
|
||||
const { result } = renderHook(() => useBestTrade(TradeType.EXACT_OUTPUT, DAIAmount, USDC_MAINNET))
|
||||
|
||||
expect(mockUseClientSideV3Trade).toHaveBeenCalledWith(TradeType.EXACT_OUTPUT, undefined, undefined)
|
||||
expect(useClientSideV3Trade).toHaveBeenCalledWith(TradeType.EXACT_OUTPUT, undefined, undefined)
|
||||
expect(result.current).toEqual({ state: TradeState.VALID, trade: undefined })
|
||||
})
|
||||
|
||||
@@ -187,7 +180,7 @@ describe('#useBestV3Trade ExactOut', () => {
|
||||
|
||||
const { result } = renderHook(() => useBestTrade(TradeType.EXACT_OUTPUT, DAIAmount, USDC_MAINNET))
|
||||
|
||||
expect(mockUseClientSideV3Trade).toHaveBeenCalledWith(TradeType.EXACT_OUTPUT, undefined, undefined)
|
||||
expect(useClientSideV3Trade).toHaveBeenCalledWith(TradeType.EXACT_OUTPUT, undefined, undefined)
|
||||
expect(result.current).toEqual({ state: TradeState.SYNCING, trade: undefined })
|
||||
})
|
||||
})
|
||||
@@ -199,7 +192,7 @@ describe('#useBestV3Trade ExactOut', () => {
|
||||
|
||||
renderHook(() => useBestTrade(TradeType.EXACT_OUTPUT, DAIAmount, USDC_MAINNET))
|
||||
|
||||
expect(mockUseClientSideV3Trade).toHaveBeenCalledWith(TradeType.EXACT_OUTPUT, undefined, undefined)
|
||||
expect(useClientSideV3Trade).toHaveBeenCalledWith(TradeType.EXACT_OUTPUT, undefined, undefined)
|
||||
})
|
||||
|
||||
it('computes client side v3 trade if routing api is NO_ROUTE_FOUND', () => {
|
||||
@@ -208,7 +201,7 @@ describe('#useBestV3Trade ExactOut', () => {
|
||||
|
||||
const { result } = renderHook(() => useBestTrade(TradeType.EXACT_OUTPUT, DAIAmount, USDC_MAINNET))
|
||||
|
||||
expect(mockUseClientSideV3Trade).toHaveBeenCalledWith(TradeType.EXACT_OUTPUT, DAIAmount, USDC_MAINNET)
|
||||
expect(useClientSideV3Trade).toHaveBeenCalledWith(TradeType.EXACT_OUTPUT, DAIAmount, USDC_MAINNET)
|
||||
expect(result.current).toEqual({ state: TradeState.VALID, trade: undefined })
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { WARNING_LEVEL } from 'constants/tokenSafety'
|
||||
import { renderHook } from 'test-utils'
|
||||
import { renderHook } from 'test-utils/render'
|
||||
import { lightTheme } from 'theme/colors'
|
||||
|
||||
import { useTokenWarningColor, useTokenWarningTextColor } from './useTokenWarningColor'
|
||||
|
||||
@@ -334,7 +334,8 @@ export const BagFooter = ({ setModalIsOpen, eventProperties }: BagFooterProps) =
|
||||
const { allowance, isAllowancePending, isApprovalLoading, updateAllowance } = usePermit2Approval(
|
||||
trade?.inputAmount.currency.isToken ? (trade?.inputAmount as CurrencyAmount<Token>) : undefined,
|
||||
maximumAmountIn,
|
||||
shouldUsePayWithAnyToken
|
||||
shouldUsePayWithAnyToken,
|
||||
true
|
||||
)
|
||||
usePayWithAnyTokenSwap(trade, allowance, allowedSlippage)
|
||||
const priceImpact = usePriceImpact(trade)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { render } from 'test-utils'
|
||||
import { render } from 'test-utils/render'
|
||||
|
||||
import Bag from './Bag'
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Markets } from 'nft/types'
|
||||
import { render } from 'test-utils'
|
||||
import { render } from 'test-utils/render'
|
||||
|
||||
import { MarketplaceContainer } from './icons'
|
||||
|
||||
|
||||
@@ -313,18 +313,16 @@ export const AssetDetails = ({ asset, collection }: AssetDetailsProps) => {
|
||||
'no-cache'
|
||||
)
|
||||
|
||||
// TODO simplify typecasting when removing graphql flag
|
||||
const lastSalePrice = isNftGraphqlEnabled ? gqlPriceData?.[0]?.price : priceData?.events[0]?.price
|
||||
const formattedEthprice = isNftGraphqlEnabled
|
||||
? formatEth(parseFloat(lastSalePrice ?? ''))
|
||||
: formatEthPrice(lastSalePrice) || 0
|
||||
const formattedPrice = isNftGraphqlEnabled
|
||||
? formattedEthprice
|
||||
: lastSalePrice
|
||||
? putCommas(parseFloat(formattedEthprice.toString())).toString()
|
||||
: null
|
||||
const [activeFilters, filtersDispatch] = useReducer(reduceFilters, initialFilterState)
|
||||
let formattedPrice
|
||||
if (isNftGraphqlEnabled) {
|
||||
const weiPrice = gqlPriceData?.[0]?.price
|
||||
formattedPrice = weiPrice ? formatEth(parseFloat(weiPrice)) : undefined
|
||||
} else {
|
||||
const ethPrice = priceData?.events[0]?.price
|
||||
formattedPrice = ethPrice ? putCommas(formatEthPrice(priceData?.events[0]?.price)).toString() : undefined
|
||||
}
|
||||
|
||||
const [activeFilters, filtersDispatch] = useReducer(reduceFilters, initialFilterState)
|
||||
const Filter = useCallback(
|
||||
function ActivityFilter({ eventType }: { eventType: ActivityEventType }) {
|
||||
const isActive = activeFilters[eventType]
|
||||
|
||||
14
src/nft/components/details/NftDetails.tsx
Normal file
14
src/nft/components/details/NftDetails.tsx
Normal file
@@ -0,0 +1,14 @@
|
||||
import { CollectionInfoForAsset, GenieAsset } from 'nft/types'
|
||||
|
||||
interface NftDetailsProps {
|
||||
asset: GenieAsset
|
||||
collection: CollectionInfoForAsset
|
||||
}
|
||||
|
||||
export const NftDetails = ({ asset, collection }: NftDetailsProps) => {
|
||||
return (
|
||||
<div>
|
||||
Details page for {asset.name} from {collection.collectionName}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -114,8 +114,8 @@ export const EthCell = ({
|
||||
denomination: Denomination
|
||||
usdPrice?: number
|
||||
}) => {
|
||||
const denominatedValue = getDenominatedValue(denomination, true, value, usdPrice)
|
||||
const isNftGraphqlEnabled = useNftGraphqlEnabled()
|
||||
const denominatedValue = getDenominatedValue(denomination, !isNftGraphqlEnabled, value, usdPrice)
|
||||
const formattedValue = denominatedValue
|
||||
? denomination === Denomination.ETH
|
||||
? isNftGraphqlEnabled
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { BigNumber } from '@ethersproject/bignumber'
|
||||
import { useNftGraphqlEnabled } from 'featureFlags/flags/nftlGraphql'
|
||||
import { CollectionTableColumn, TimePeriod } from 'nft/types'
|
||||
import { useMemo } from 'react'
|
||||
import { CellProps, Column, Row } from 'react-table'
|
||||
@@ -26,14 +27,19 @@ const compareFloats = (a?: number, b?: number): 1 | -1 => {
|
||||
}
|
||||
|
||||
const CollectionTable = ({ data, timePeriod }: { data: CollectionTableColumn[]; timePeriod: TimePeriod }) => {
|
||||
const isNftGraphqlEnabled = useNftGraphqlEnabled()
|
||||
const floorSort = useMemo(() => {
|
||||
return (rowA: Row<CollectionTableColumn>, rowB: Row<CollectionTableColumn>) => {
|
||||
const aFloor = BigNumber.from(rowA.original.floor.value ?? 0)
|
||||
const bFloor = BigNumber.from(rowB.original.floor.value ?? 0)
|
||||
if (isNftGraphqlEnabled) {
|
||||
return compareFloats(rowA.original.floor.value, rowB.original.floor.value)
|
||||
} else {
|
||||
const aFloor = BigNumber.from(rowA.original.floor.value ?? 0)
|
||||
const bFloor = BigNumber.from(rowB.original.floor.value ?? 0)
|
||||
|
||||
return aFloor.gte(bFloor) ? 1 : -1
|
||||
return aFloor.gte(bFloor) ? 1 : -1
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
}, [isNftGraphqlEnabled])
|
||||
|
||||
const floorChangeSort = useMemo(() => {
|
||||
return (rowA: Row<CollectionTableColumn>, rowB: Row<CollectionTableColumn>) => {
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import noop from 'utils/noop'
|
||||
|
||||
import { Box } from '../Box'
|
||||
import * as styles from './Overlay.css'
|
||||
|
||||
@@ -10,6 +12,6 @@ export const stopPropagation = (event: React.SyntheticEvent<HTMLElement>) => {
|
||||
event.nativeEvent.stopImmediatePropagation()
|
||||
}
|
||||
|
||||
export const Overlay = ({ onClick = () => null }: OverlayProps) => {
|
||||
export const Overlay = ({ onClick = noop }: OverlayProps) => {
|
||||
return <Box className={styles.overlay} onClick={onClick} />
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { render } from 'test-utils'
|
||||
import { render } from 'test-utils/render'
|
||||
|
||||
import { EmptyWalletModule } from './EmptyWalletContent'
|
||||
|
||||
|
||||
@@ -28,6 +28,7 @@ import InfiniteLoader from 'react-window-infinite-loader'
|
||||
import styled from 'styled-components/macro'
|
||||
import { ThemedText } from 'theme'
|
||||
import { TRANSITION_DURATIONS } from 'theme/styles'
|
||||
import noop from 'utils/noop'
|
||||
|
||||
import { WALLET_COLLECTIONS_PAGINATION_LIMIT } from './ProfilePage'
|
||||
import * as styles from './ProfilePage.css'
|
||||
@@ -190,7 +191,7 @@ const CollectionSelect = ({
|
||||
|
||||
// Only load 1 page of items at a time.
|
||||
// Pass an empty callback to InfiniteLoader in case it asks us to load more than once.
|
||||
const loadMoreItems = isFetchingNextPage ? () => null : fetchNextPage
|
||||
const loadMoreItems = isFetchingNextPage ? noop : fetchNextPage
|
||||
|
||||
// Every row is loaded except for our loading indicator row.
|
||||
const isItemLoaded = useCallback(
|
||||
|
||||
28
src/nft/hooks/usePermit2Approval.test.ts
Normal file
28
src/nft/hooks/usePermit2Approval.test.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { renderHook } from '@testing-library/react'
|
||||
import { CurrencyAmount } from '@uniswap/sdk-core'
|
||||
import { USDC_MAINNET } from '@uniswap/smart-order-router'
|
||||
import usePermit2Allowance, { AllowanceState } from 'hooks/usePermit2Allowance'
|
||||
|
||||
import usePermit2Approval from './usePermit2Approval'
|
||||
|
||||
const USDCAmount = CurrencyAmount.fromRawAmount(USDC_MAINNET, '10000')
|
||||
const NFT_UNIVERSAL_ROUTER_MAINNET_ADDRESS = '0x4c60051384bd2d3c01bfc845cf5f4b44bcbe9de5'
|
||||
|
||||
jest.mock('@web3-react/core', () => {
|
||||
return {
|
||||
useWeb3React: () => ({
|
||||
chainId: 1,
|
||||
}),
|
||||
}
|
||||
})
|
||||
jest.mock('hooks/usePermit2Allowance')
|
||||
|
||||
const mockUsePermit2Allowance = usePermit2Allowance as jest.MockedFunction<typeof usePermit2Allowance>
|
||||
|
||||
describe('usePermit2Approval', () => {
|
||||
it('sets spender of the correct UR contract from NFT side', async () => {
|
||||
mockUsePermit2Allowance.mockReturnValue({ state: AllowanceState.LOADING })
|
||||
renderHook(() => usePermit2Approval(USDCAmount, undefined, true, true))
|
||||
expect(mockUsePermit2Allowance).toHaveBeenCalledWith(USDCAmount, NFT_UNIVERSAL_ROUTER_MAINNET_ADDRESS)
|
||||
})
|
||||
})
|
||||
@@ -7,16 +7,24 @@ import usePermit2Allowance, { AllowanceState } from 'hooks/usePermit2Allowance'
|
||||
import { useCallback, useMemo, useState } from 'react'
|
||||
import invariant from 'tiny-invariant'
|
||||
|
||||
// TODO: This should be removed when the sdk is updated to include the new UR address
|
||||
const NFT_UNIVERSAL_ROUTER_MAINNET_ADDRESS = '0x4c60051384bd2d3c01bfc845cf5f4b44bcbe9de5'
|
||||
|
||||
export default function usePermit2Approval(
|
||||
amount?: CurrencyAmount<Token>,
|
||||
maximumAmount?: CurrencyAmount<Token>,
|
||||
enabled?: boolean
|
||||
enabled?: boolean,
|
||||
shouldUseNftRouter?: boolean
|
||||
) {
|
||||
const { chainId } = useWeb3React()
|
||||
|
||||
const allowance = usePermit2Allowance(
|
||||
enabled ? maximumAmount ?? (amount?.currency.isToken ? (amount as CurrencyAmount<Token>) : undefined) : undefined,
|
||||
enabled && chainId ? UNIVERSAL_ROUTER_ADDRESS(chainId) : undefined
|
||||
enabled && chainId
|
||||
? shouldUseNftRouter && chainId === 1
|
||||
? NFT_UNIVERSAL_ROUTER_MAINNET_ADDRESS
|
||||
: UNIVERSAL_ROUTER_ADDRESS(chainId)
|
||||
: undefined
|
||||
)
|
||||
const isApprovalLoading = allowance.state === AllowanceState.REQUIRED && allowance.isApprovalLoading
|
||||
const [isAllowancePending, setIsAllowancePending] = useState(false)
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { Trace } from '@uniswap/analytics'
|
||||
import { InterfacePageName } from '@uniswap/analytics-events'
|
||||
import { useDetailsV2Enabled } from 'featureFlags/flags/nftDetails'
|
||||
import { useNftAssetDetails } from 'graphql/data/nft/Details'
|
||||
import { AssetDetails } from 'nft/components/details/AssetDetails'
|
||||
import { AssetDetailsLoading } from 'nft/components/details/AssetDetailsLoading'
|
||||
import { AssetPriceDetails } from 'nft/components/details/AssetPriceDetails'
|
||||
import { NftDetails } from 'nft/components/details/NftDetails'
|
||||
import { useParams } from 'react-router-dom'
|
||||
import styled from 'styled-components/macro'
|
||||
|
||||
@@ -37,11 +39,11 @@ const AssetPriceDetailsContainer = styled.div`
|
||||
const AssetPage = () => {
|
||||
const { tokenId = '', contractAddress = '' } = useParams()
|
||||
const { data, loading } = useNftAssetDetails(contractAddress, tokenId)
|
||||
const detailsV2Enabled = useDetailsV2Enabled()
|
||||
|
||||
const [asset, collection] = data
|
||||
|
||||
if (loading) return <AssetDetailsLoading />
|
||||
|
||||
if (loading && !detailsV2Enabled) return <AssetDetailsLoading />
|
||||
return (
|
||||
<>
|
||||
<Trace
|
||||
@@ -49,14 +51,18 @@ const AssetPage = () => {
|
||||
properties={{ collection_address: contractAddress, token_id: tokenId }}
|
||||
shouldLogImpression
|
||||
>
|
||||
{!!asset && !!collection && (
|
||||
<AssetContainer>
|
||||
<AssetDetails collection={collection} asset={asset} />
|
||||
<AssetPriceDetailsContainer>
|
||||
<AssetPriceDetails collection={collection} asset={asset} />
|
||||
</AssetPriceDetailsContainer>
|
||||
</AssetContainer>
|
||||
)}
|
||||
{!!asset && !!collection ? (
|
||||
detailsV2Enabled ? (
|
||||
<NftDetails asset={asset} collection={collection} />
|
||||
) : (
|
||||
<AssetContainer>
|
||||
<AssetDetails collection={collection} asset={asset} />
|
||||
<AssetPriceDetailsContainer>
|
||||
<AssetPriceDetails collection={collection} asset={asset} />
|
||||
</AssetPriceDetailsContainer>
|
||||
</AssetContainer>
|
||||
)
|
||||
) : null}
|
||||
</Trace>
|
||||
</>
|
||||
)
|
||||
|
||||
@@ -4,7 +4,6 @@ import { useWeb3React } from '@web3-react/core'
|
||||
import Loader from 'components/Icons/LoadingSpinner'
|
||||
import TopLevelModals from 'components/TopLevelModals'
|
||||
import { useFeatureFlagsIsLoaded } from 'featureFlags'
|
||||
import { useMGTMMicrositeEnabled } from 'featureFlags/flags/mgtm'
|
||||
import ApeModeQueryParamReader from 'hooks/useApeModeQueryParamReader'
|
||||
import { useAtom } from 'jotai'
|
||||
import { useBag } from 'nft/hooks/useBag'
|
||||
@@ -37,7 +36,7 @@ import MigrateV2 from './MigrateV2'
|
||||
import MigrateV2Pair from './MigrateV2/MigrateV2Pair'
|
||||
import NotFound from './NotFound'
|
||||
import Pool from './Pool'
|
||||
import { PositionPage } from './Pool/PositionPage'
|
||||
import PositionPage from './Pool/PositionPage'
|
||||
import PoolV2 from './Pool/v2'
|
||||
import PoolFinder from './PoolFinder'
|
||||
import RemoveLiquidity from './RemoveLiquidity'
|
||||
@@ -48,7 +47,6 @@ import Tokens from './Tokens'
|
||||
|
||||
const TokenDetails = lazy(() => import('./TokenDetails'))
|
||||
const Vote = lazy(() => import('./Vote'))
|
||||
const Wallet = lazy(() => import('./Wallet'))
|
||||
const NftExplore = lazy(() => import('nft/pages/explore'))
|
||||
const Collection = lazy(() => import('nft/pages/collection'))
|
||||
const Profile = lazy(() => import('nft/pages/profile/profile'))
|
||||
@@ -192,9 +190,7 @@ export default function App() {
|
||||
}, [])
|
||||
|
||||
const isBagExpanded = useBag((state) => state.bagExpanded)
|
||||
const isOnWalletPage = useLocation().pathname === '/wallet'
|
||||
const micrositeEnabled = useMGTMMicrositeEnabled()
|
||||
const isHeaderTransparent = (!scrolledState && !isBagExpanded) || isOnWalletPage
|
||||
const isHeaderTransparent = !scrolledState && !isBagExpanded
|
||||
|
||||
const { account } = useWeb3React()
|
||||
const statsigUser: StatsigUser = useMemo(
|
||||
@@ -245,7 +241,6 @@ export default function App() {
|
||||
}
|
||||
/>
|
||||
<Route path="create-proposal" element={<Navigate to="/vote/create-proposal" replace />} />
|
||||
{micrositeEnabled && <Route path="wallet" element={<Wallet />} />}
|
||||
<Route path="send" element={<RedirectPathToSwapOnly />} />
|
||||
<Route path="swap" element={<Swap />} />
|
||||
|
||||
|
||||
@@ -369,7 +369,9 @@ export default function Landing() {
|
||||
element={InterfaceElementName.CONTINUE_BUTTON}
|
||||
>
|
||||
<ButtonCTA as={Link} to="/swap">
|
||||
<ButtonCTAText>Get started</ButtonCTAText>
|
||||
<ButtonCTAText>
|
||||
<Trans>Get started</Trans>
|
||||
</ButtonCTAText>
|
||||
</ButtonCTA>
|
||||
</TraceEvent>
|
||||
</ActionsContainer>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as useV3Positions from 'hooks/useV3Positions'
|
||||
import { render, screen } from 'test-utils'
|
||||
import { render, screen } from 'test-utils/render'
|
||||
|
||||
import CTACards from './CTACards'
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ import { RowBetween, RowFixed } from 'components/Row'
|
||||
import { Dots } from 'components/swap/styleds'
|
||||
import Toggle from 'components/Toggle'
|
||||
import TransactionConfirmationModal, { ConfirmationModalContent } from 'components/TransactionConfirmationModal'
|
||||
import { CHAIN_IDS_TO_NAMES } from 'constants/chains'
|
||||
import { CHAIN_IDS_TO_NAMES, isSupportedChain } from 'constants/chains'
|
||||
import { isGqlSupportedChain } from 'graphql/data/util'
|
||||
import { useToken } from 'hooks/Tokens'
|
||||
import { useV3NFTPositionManagerContract } from 'hooks/useContract'
|
||||
@@ -346,7 +346,34 @@ const useInverter = ({
|
||||
}
|
||||
}
|
||||
|
||||
export function PositionPage() {
|
||||
function PositionPageUnsupportedContent() {
|
||||
return (
|
||||
<PageWrapper>
|
||||
<div style={{ display: 'flex', alignItems: 'center', flexDirection: 'column' }}>
|
||||
<ThemedText.HeadlineLarge style={{ marginBottom: '8px' }}>
|
||||
<Trans>Position unavailable</Trans>
|
||||
</ThemedText.HeadlineLarge>
|
||||
<ThemedText.BodyPrimary style={{ marginBottom: '32px' }}>
|
||||
<Trans>To view a position, you must be connected to the network it belongs to.</Trans>
|
||||
</ThemedText.BodyPrimary>
|
||||
<PositionPageButtonPrimary as={Link} to="/pools" width="fit-content">
|
||||
<Trans>Back to Pools</Trans>
|
||||
</PositionPageButtonPrimary>
|
||||
</div>
|
||||
</PageWrapper>
|
||||
)
|
||||
}
|
||||
|
||||
export default function PositionPage() {
|
||||
const { chainId } = useWeb3React()
|
||||
if (isSupportedChain(chainId)) {
|
||||
return <PositionPageContent />
|
||||
} else {
|
||||
return <PositionPageUnsupportedContent />
|
||||
}
|
||||
}
|
||||
|
||||
function PositionPageContent() {
|
||||
const { tokenId: tokenIdFromUrl } = useParams<{ tokenId?: string }>()
|
||||
const { chainId, account, provider } = useWeb3React()
|
||||
const theme = useTheme()
|
||||
@@ -588,21 +615,7 @@ export function PositionPage() {
|
||||
)
|
||||
|
||||
if (!positionDetails && !loading) {
|
||||
return (
|
||||
<PageWrapper>
|
||||
<div style={{ display: 'flex', alignItems: 'center', flexDirection: 'column' }}>
|
||||
<ThemedText.HeadlineLarge style={{ marginBottom: '8px' }}>
|
||||
<Trans>Position unavailable</Trans>
|
||||
</ThemedText.HeadlineLarge>
|
||||
<ThemedText.BodyPrimary style={{ marginBottom: '32px' }}>
|
||||
<Trans>To view a position, you must be connected to the network it belongs to.</Trans>
|
||||
</ThemedText.BodyPrimary>
|
||||
<PositionPageButtonPrimary as={Link} to="/pools" width="fit-content">
|
||||
<Trans>Back to Pools</Trans>
|
||||
</PositionPageButtonPrimary>
|
||||
</div>
|
||||
</PageWrapper>
|
||||
)
|
||||
return <PositionPageUnsupportedContent />
|
||||
}
|
||||
|
||||
return loading || poolState === PoolState.LOADING || !feeAmount ? (
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import * as chains from 'constants/chains'
|
||||
import * as useV3Positions from 'hooks/useV3Positions'
|
||||
import { render, screen } from 'test-utils'
|
||||
import { render, screen } from 'test-utils/render'
|
||||
|
||||
import Pool from '.'
|
||||
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
export default function Wallet() {
|
||||
return <div>uniswap wallet pretty cool</div>
|
||||
}
|
||||
@@ -14,6 +14,7 @@ describe('application reducer', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
store = createStore(reducer, {
|
||||
fiatOnramp: { available: false, availabilityChecked: false },
|
||||
chainId: null,
|
||||
openModal: null,
|
||||
popupList: [],
|
||||
@@ -28,7 +29,7 @@ describe('application reducer', () => {
|
||||
expect(typeof list[0].key).toEqual('string')
|
||||
expect(list[0].show).toEqual(true)
|
||||
expect(list[0].content).toEqual({ txn: { hash: 'abc' } })
|
||||
expect(list[0].removeAfterMs).toEqual(25000)
|
||||
expect(list[0].removeAfterMs).toEqual(10000)
|
||||
})
|
||||
|
||||
it('replaces any existing popups with the same key', () => {
|
||||
@@ -39,7 +40,7 @@ describe('application reducer', () => {
|
||||
expect(list[0].key).toEqual('abc')
|
||||
expect(list[0].show).toEqual(true)
|
||||
expect(list[0].content).toEqual({ txn: { hash: 'def' } })
|
||||
expect(list[0].removeAfterMs).toEqual(25000)
|
||||
expect(list[0].removeAfterMs).toEqual(10000)
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import * as Sentry from '@sentry/react'
|
||||
import noop from 'utils/noop'
|
||||
|
||||
import { AppState } from './types'
|
||||
|
||||
@@ -17,7 +18,7 @@ export const sentryEnhancer = Sentry.createReduxEnhancer({
|
||||
/**
|
||||
* We don't want to store actions as breadcrumbs in Sentry, so we return null to disable the default behavior.
|
||||
*/
|
||||
actionTransformer: () => null,
|
||||
actionTransformer: noop,
|
||||
/**
|
||||
* We only want to store a subset of the state in Sentry, containing only the relevant parts for debugging.
|
||||
* Note: This function runs on every state update, so we're keeping it as fast as possible by avoiding any function
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { parse } from 'qs'
|
||||
import { TEST_RECIPIENT_ADDRESS } from 'test-utils/constants'
|
||||
|
||||
import { Field } from './actions'
|
||||
import { queryParametersToSwapState } from './hooks'
|
||||
@@ -65,7 +66,7 @@ describe('hooks', () => {
|
||||
test('valid recipient', () => {
|
||||
expect(
|
||||
queryParametersToSwapState(
|
||||
parse('?outputCurrency=eth&exactAmount=20.5&recipient=0x0fF2D1eFd7A57B7562b2bf27F3f37899dB27F4a5', {
|
||||
parse(`?outputCurrency=eth&exactAmount=20.5&recipient=${TEST_RECIPIENT_ADDRESS}`, {
|
||||
parseArrays: false,
|
||||
ignoreQueryPrefix: true,
|
||||
})
|
||||
@@ -75,7 +76,7 @@ describe('hooks', () => {
|
||||
[Field.INPUT]: { currencyId: null },
|
||||
typedValue: '20.5',
|
||||
independentField: Field.INPUT,
|
||||
recipient: '0x0fF2D1eFd7A57B7562b2bf27F3f37899dB27F4a5',
|
||||
recipient: TEST_RECIPIENT_ADDRESS,
|
||||
})
|
||||
})
|
||||
test('accepts any recipient', () => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { act, renderHook } from 'test-utils'
|
||||
import { act, renderHook } from 'test-utils/render'
|
||||
|
||||
import { useConnectedWallets } from './hooks'
|
||||
import { Wallet } from './types'
|
||||
|
||||
29
src/test-utils/constants.ts
Normal file
29
src/test-utils/constants.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { CurrencyAmount, Token } from '@uniswap/sdk-core'
|
||||
import { FeeAmount, Pool } from '@uniswap/v3-sdk'
|
||||
import JSBI from 'jsbi'
|
||||
|
||||
export const TEST_TOKEN_1 = new Token(1, '0x0000000000000000000000000000000000000001', 18, 'ABC', 'Abc')
|
||||
export const TEST_TOKEN_2 = new Token(1, '0x0000000000000000000000000000000000000002', 18, 'DEF', 'Def')
|
||||
export const TEST_TOKEN_3 = new Token(1, '0x0000000000000000000000000000000000000003', 18, 'GHI', 'Ghi')
|
||||
export const TEST_RECIPIENT_ADDRESS = '0x0000000000000000000000000000000000000004'
|
||||
|
||||
export const TEST_POOL_12 = new Pool(
|
||||
TEST_TOKEN_1,
|
||||
TEST_TOKEN_2,
|
||||
FeeAmount.HIGH,
|
||||
'2437312313659959819381354528',
|
||||
'10272714736694327408',
|
||||
-69633
|
||||
)
|
||||
|
||||
export const TEST_POOL_13 = new Pool(
|
||||
TEST_TOKEN_1,
|
||||
TEST_TOKEN_3,
|
||||
FeeAmount.MEDIUM,
|
||||
'2437312313659959819381354528',
|
||||
'10272714736694327408',
|
||||
-69633
|
||||
)
|
||||
|
||||
export const toCurrencyAmount = (token: Token, amount: number) =>
|
||||
CurrencyAmount.fromRawAmount(token, JSBI.BigInt(amount))
|
||||
18
src/test-utils/mocked.tsx
Normal file
18
src/test-utils/mocked.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
/**
|
||||
* Casts the passed function as a jest.Mock.
|
||||
* Use this in combination with jest.mock() to safely access functions from mocked modules.
|
||||
*
|
||||
* @example
|
||||
*
|
||||
* import { useExample } from 'example'
|
||||
* jest.mock('example', () => ({ useExample: jest.fn() }))
|
||||
* beforeEach(() => {
|
||||
* asMock(useExample).mockImplementation(() => ...)
|
||||
* })
|
||||
*/
|
||||
// jest expects mocks to be coerced (eg fn as jest.MockedFunction<T>), but this is not ergonomic when using ASI.
|
||||
// Instead, we use this utility function to improve readability and add a check to ensure the function is a mock.
|
||||
export function mocked<T extends (...args: any) => any>(fn: T) {
|
||||
if (!jest.isMockFunction(fn)) throw new Error('fn is not a mock')
|
||||
return fn as jest.MockedFunction<T>
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import { render, renderHook } from '@testing-library/react'
|
||||
import Web3Provider from 'components/Web3Provider'
|
||||
import { DEFAULT_LOCALE } from 'constants/locales'
|
||||
import { BlockNumberProvider } from 'lib/hooks/useBlockNumber'
|
||||
import catalog from 'locales/en-US'
|
||||
import { en } from 'make-plural/plurals'
|
||||
import { ReactElement, ReactNode } from 'react'
|
||||
import { QueryClient, QueryClientProvider } from 'react-query'
|
||||
@@ -13,8 +14,6 @@ import { HashRouter } from 'react-router-dom'
|
||||
import store from 'state'
|
||||
import ThemeProvider from 'theme'
|
||||
|
||||
import catalog from './locales/en-US'
|
||||
|
||||
i18n.load({
|
||||
[DEFAULT_LOCALE]: catalog.messages,
|
||||
})
|
||||
@@ -83,6 +83,7 @@ export const colors = {
|
||||
blue900: '#040E34',
|
||||
blueVibrant: '#587BFF',
|
||||
// TODO: add magenta 50-900
|
||||
magenta300: '#FD82FF',
|
||||
magentaVibrant: '#FC72FF',
|
||||
purple300: '#8440F2',
|
||||
purple900: '#1C0337',
|
||||
@@ -119,6 +120,7 @@ const commonTheme = {
|
||||
chain_10_background: colors.red900,
|
||||
chain_42161_background: colors.blue900,
|
||||
chain_56_background: colors.networkBsc,
|
||||
promotional: colors.magenta300,
|
||||
|
||||
brandedGradient: 'linear-gradient(139.57deg, #FF79C9 4.35%, #FFB8E2 96.44%);',
|
||||
promotionalGradient: 'radial-gradient(101.8% 4091.31% at 0% 0%, #4673FA 0%, #9646FA 100%);',
|
||||
|
||||
@@ -24,4 +24,9 @@ describe('filterKnownErrors', () => {
|
||||
const originalException = new Error('user rejected transaction')
|
||||
expect(filterKnownErrors(ERROR, { originalException })).toBe(null)
|
||||
})
|
||||
|
||||
it('filters invalid HTML response errors', () => {
|
||||
const originalException = new SyntaxError("Unexpected token '<'")
|
||||
expect(filterKnownErrors(ERROR, { originalException })).toBe(null)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -6,6 +6,20 @@ function isEthersRequestError(error: Error): error is Error & { requestBody: str
|
||||
return 'requestBody' in error && typeof (error as unknown as Record<'requestBody', unknown>).requestBody === 'string'
|
||||
}
|
||||
|
||||
export function beforeSend(event: ErrorEvent, hint: EventHint) {
|
||||
/*
|
||||
* Since the interface currently uses HashRouter, URLs will have a # before the path.
|
||||
* This leads to issues when we send the URL into Sentry, as the path gets parsed as a "fragment".
|
||||
* Instead, this logic removes the # part of the URL.
|
||||
* See https://romain-clement.net/articles/sentry-url-fragments/#url-fragments
|
||||
**/
|
||||
if (event.request?.url) {
|
||||
event.request.url = event.request.url.replace('/#', '')
|
||||
}
|
||||
|
||||
return filterKnownErrors(event, hint)
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters known (ignorable) errors out before sending them to Sentry.
|
||||
* Intended as a {@link ClientOptions.beforeSend} callback. Returning null filters the error from Sentry.
|
||||
@@ -25,6 +39,13 @@ export const filterKnownErrors: Required<ClientOptions>['beforeSend'] = (event:
|
||||
|
||||
// If the error is based on a user rejecting, it should not be considered an exception.
|
||||
if (didUserReject(error)) return null
|
||||
|
||||
/*
|
||||
* This is caused by HTML being returned for a chunk from Cloudflare.
|
||||
* Usually, it's the result of a 499 exception right before it, which should be handled.
|
||||
* Therefore, this can be ignored.
|
||||
*/
|
||||
if (error.message.match(/Unexpected token '<'/)) return null
|
||||
}
|
||||
|
||||
return event
|
||||
|
||||
@@ -7,7 +7,7 @@ import { SharedEventName } from '@uniswap/analytics-events'
|
||||
import { isSentryEnabled } from 'utils/env'
|
||||
import { getEnvName, isProductionEnv } from 'utils/env'
|
||||
|
||||
import { filterKnownErrors } from './errors'
|
||||
import { beforeSend } from './errors'
|
||||
|
||||
export { trace } from './trace'
|
||||
|
||||
@@ -30,7 +30,7 @@ Sentry.init({
|
||||
startTransactionOnPageLoad: true,
|
||||
}),
|
||||
],
|
||||
beforeSend: filterKnownErrors,
|
||||
beforeSend,
|
||||
})
|
||||
|
||||
initializeAnalytics(AMPLITUDE_DUMMY_KEY, OriginApplication.INTERFACE, {
|
||||
|
||||
@@ -2,8 +2,11 @@ import '@sentry/tracing' // required to populate Sentry.startTransaction, which
|
||||
|
||||
import * as Sentry from '@sentry/react'
|
||||
import { Transaction } from '@sentry/tracing'
|
||||
import { ErrorEvent, EventHint } from '@sentry/types'
|
||||
import assert from 'assert'
|
||||
import { mocked } from 'test-utils/mocked'
|
||||
|
||||
import { beforeSend } from './errors'
|
||||
import { trace } from './trace'
|
||||
|
||||
jest.mock('@sentry/react', () => {
|
||||
@@ -11,10 +14,9 @@ jest.mock('@sentry/react', () => {
|
||||
startTransaction: jest.fn(),
|
||||
}
|
||||
})
|
||||
const startTransaction = Sentry.startTransaction as jest.Mock
|
||||
|
||||
function getTransaction(index = 0): Transaction {
|
||||
const transactions = startTransaction.mock.results.map(({ value }) => value)
|
||||
const transactions = mocked(Sentry.startTransaction).mock.results.map(({ value }) => value)
|
||||
expect(transactions).toHaveLength(index + 1)
|
||||
const transaction = transactions[index]
|
||||
expect(transaction).toBeDefined()
|
||||
@@ -23,12 +25,13 @@ function getTransaction(index = 0): Transaction {
|
||||
|
||||
describe('trace', () => {
|
||||
beforeEach(() => {
|
||||
const Sentry = jest.requireActual('@sentry/react')
|
||||
startTransaction.mockReset().mockImplementation((context) => {
|
||||
const transaction: Transaction = Sentry.startTransaction(context)
|
||||
transaction.initSpanRecorder()
|
||||
return transaction
|
||||
})
|
||||
mocked(Sentry.startTransaction)
|
||||
.mockReset()
|
||||
.mockImplementation((context) => {
|
||||
const transaction: Transaction = jest.requireActual('@sentry/react').startTransaction(context)
|
||||
transaction.initSpanRecorder()
|
||||
return transaction
|
||||
})
|
||||
})
|
||||
|
||||
it('propagates callback', async () => {
|
||||
@@ -84,6 +87,41 @@ describe('trace', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('beforeSend', () => {
|
||||
it('handles no path', async () => {
|
||||
const errorEvent: ErrorEvent = {
|
||||
type: undefined,
|
||||
request: {
|
||||
url: 'https://app.uniswap.org',
|
||||
},
|
||||
}
|
||||
const eventHint: EventHint = {}
|
||||
expect((beforeSend(errorEvent, eventHint) as ErrorEvent)?.request?.url).toEqual('https://app.uniswap.org')
|
||||
})
|
||||
|
||||
it('handles hash with path', async () => {
|
||||
const errorEvent: ErrorEvent = {
|
||||
type: undefined,
|
||||
request: {
|
||||
url: 'https://app.uniswap.org/#/pools',
|
||||
},
|
||||
}
|
||||
const eventHint: EventHint = {}
|
||||
expect((beforeSend(errorEvent, eventHint) as ErrorEvent)?.request?.url).toEqual('https://app.uniswap.org/pools')
|
||||
})
|
||||
|
||||
it('handles just hash', async () => {
|
||||
const errorEvent: ErrorEvent = {
|
||||
type: undefined,
|
||||
request: {
|
||||
url: 'https://app.uniswap.org/#',
|
||||
},
|
||||
}
|
||||
const eventHint: EventHint = {}
|
||||
expect((beforeSend(errorEvent, eventHint) as ErrorEvent)?.request?.url).toEqual('https://app.uniswap.org')
|
||||
})
|
||||
})
|
||||
|
||||
describe('setTraceStatus', () => {
|
||||
it('sets a transaction status with a string', async () => {
|
||||
await trace('test', ({ setTraceStatus }) => {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Token } from '@uniswap/sdk-core'
|
||||
import { FeeAmount, TICK_SPACINGS } from '@uniswap/v3-sdk'
|
||||
import { TickData, TickProcessed } from 'hooks/usePoolTickData'
|
||||
import { TickData } from 'graphql/thegraph/AllV3TicksQuery'
|
||||
import { TickProcessed } from 'hooks/usePoolTickData'
|
||||
import JSBI from 'jsbi'
|
||||
|
||||
import computeSurroundingTicks from './computeSurroundingTicks'
|
||||
@@ -8,7 +9,8 @@ import computeSurroundingTicks from './computeSurroundingTicks'
|
||||
const getV3Tick = (tick: number, liquidityNet: number): TickData => ({
|
||||
tick,
|
||||
liquidityNet: JSBI.BigInt(liquidityNet),
|
||||
liquidityGross: JSBI.BigInt(liquidityNet),
|
||||
price0: undefined,
|
||||
price1: undefined,
|
||||
})
|
||||
|
||||
describe('#computeSurroundingTicks', () => {
|
||||
|
||||
4
src/utils/noop.ts
Normal file
4
src/utils/noop.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
/** No-op function. Returns `null` to satisfy most React typings. */
|
||||
export default function noop() {
|
||||
return null
|
||||
}
|
||||
@@ -1,36 +1,28 @@
|
||||
import { Trade } from '@uniswap/router-sdk'
|
||||
import { CurrencyAmount, Percent, Token, TradeType } from '@uniswap/sdk-core'
|
||||
import { CurrencyAmount, Percent, TradeType } from '@uniswap/sdk-core'
|
||||
import { Pair, Route as V2Route } from '@uniswap/v2-sdk'
|
||||
import { FeeAmount, Pool, Route as V3Route } from '@uniswap/v3-sdk'
|
||||
import { Route as V3Route } from '@uniswap/v3-sdk'
|
||||
import JSBI from 'jsbi'
|
||||
import {
|
||||
TEST_POOL_12,
|
||||
TEST_POOL_13,
|
||||
TEST_TOKEN_1,
|
||||
TEST_TOKEN_2,
|
||||
TEST_TOKEN_3,
|
||||
toCurrencyAmount,
|
||||
} from 'test-utils/constants'
|
||||
|
||||
import { computeRealizedLPFeeAmount, warningSeverity } from './prices'
|
||||
|
||||
const token1 = new Token(1, '0x0000000000000000000000000000000000000001', 18)
|
||||
const token2 = new Token(1, '0x0000000000000000000000000000000000000002', 18)
|
||||
const token3 = new Token(1, '0x0000000000000000000000000000000000000003', 18)
|
||||
|
||||
const pair12 = new Pair(
|
||||
CurrencyAmount.fromRawAmount(token1, JSBI.BigInt(10000)),
|
||||
CurrencyAmount.fromRawAmount(token2, JSBI.BigInt(20000))
|
||||
CurrencyAmount.fromRawAmount(TEST_TOKEN_1, JSBI.BigInt(10000)),
|
||||
CurrencyAmount.fromRawAmount(TEST_TOKEN_2, JSBI.BigInt(20000))
|
||||
)
|
||||
const pair23 = new Pair(
|
||||
CurrencyAmount.fromRawAmount(token2, JSBI.BigInt(20000)),
|
||||
CurrencyAmount.fromRawAmount(token3, JSBI.BigInt(30000))
|
||||
CurrencyAmount.fromRawAmount(TEST_TOKEN_2, JSBI.BigInt(20000)),
|
||||
CurrencyAmount.fromRawAmount(TEST_TOKEN_3, JSBI.BigInt(30000))
|
||||
)
|
||||
|
||||
const pool12 = new Pool(token1, token2, FeeAmount.HIGH, '2437312313659959819381354528', '10272714736694327408', -69633)
|
||||
const pool13 = new Pool(
|
||||
token1,
|
||||
token3,
|
||||
FeeAmount.MEDIUM,
|
||||
'2437312313659959819381354528',
|
||||
'10272714736694327408',
|
||||
-69633
|
||||
)
|
||||
|
||||
const currencyAmount = (token: Token, amount: number) => CurrencyAmount.fromRawAmount(token, JSBI.BigInt(amount))
|
||||
|
||||
describe('prices', () => {
|
||||
describe('#computeRealizedLPFeeAmount', () => {
|
||||
it('returns undefined for undefined', () => {
|
||||
@@ -44,16 +36,16 @@ describe('prices', () => {
|
||||
new Trade({
|
||||
v2Routes: [
|
||||
{
|
||||
routev2: new V2Route([pair12], token1, token2),
|
||||
inputAmount: currencyAmount(token1, 1000),
|
||||
outputAmount: currencyAmount(token2, 1000),
|
||||
routev2: new V2Route([pair12], TEST_TOKEN_1, TEST_TOKEN_2),
|
||||
inputAmount: toCurrencyAmount(TEST_TOKEN_1, 1000),
|
||||
outputAmount: toCurrencyAmount(TEST_TOKEN_2, 1000),
|
||||
},
|
||||
],
|
||||
v3Routes: [],
|
||||
tradeType: TradeType.EXACT_INPUT,
|
||||
})
|
||||
)
|
||||
).toEqual(currencyAmount(token1, 3)) // 3% realized fee
|
||||
).toEqual(toCurrencyAmount(TEST_TOKEN_1, 3)) // 3% realized fee
|
||||
})
|
||||
|
||||
it('correct realized lp fee for single hop on v3', () => {
|
||||
@@ -63,16 +55,16 @@ describe('prices', () => {
|
||||
new Trade({
|
||||
v3Routes: [
|
||||
{
|
||||
routev3: new V3Route([pool12], token1, token2),
|
||||
inputAmount: currencyAmount(token1, 1000),
|
||||
outputAmount: currencyAmount(token2, 1000),
|
||||
routev3: new V3Route([TEST_POOL_12], TEST_TOKEN_1, TEST_TOKEN_2),
|
||||
inputAmount: toCurrencyAmount(TEST_TOKEN_1, 1000),
|
||||
outputAmount: toCurrencyAmount(TEST_TOKEN_2, 1000),
|
||||
},
|
||||
],
|
||||
v2Routes: [],
|
||||
tradeType: TradeType.EXACT_INPUT,
|
||||
})
|
||||
)
|
||||
).toEqual(currencyAmount(token1, 10)) // 3% realized fee
|
||||
).toEqual(toCurrencyAmount(TEST_TOKEN_1, 10)) // 3% realized fee
|
||||
})
|
||||
|
||||
it('correct realized lp fee for double hop', () => {
|
||||
@@ -81,16 +73,16 @@ describe('prices', () => {
|
||||
new Trade({
|
||||
v2Routes: [
|
||||
{
|
||||
routev2: new V2Route([pair12, pair23], token1, token3),
|
||||
inputAmount: currencyAmount(token1, 1000),
|
||||
outputAmount: currencyAmount(token3, 1000),
|
||||
routev2: new V2Route([pair12, pair23], TEST_TOKEN_1, TEST_TOKEN_3),
|
||||
inputAmount: toCurrencyAmount(TEST_TOKEN_1, 1000),
|
||||
outputAmount: toCurrencyAmount(TEST_TOKEN_3, 1000),
|
||||
},
|
||||
],
|
||||
v3Routes: [],
|
||||
tradeType: TradeType.EXACT_INPUT,
|
||||
})
|
||||
)
|
||||
).toEqual(currencyAmount(token1, 5))
|
||||
).toEqual(toCurrencyAmount(TEST_TOKEN_1, 5))
|
||||
})
|
||||
|
||||
it('correct realized lp fee for multi route v2+v3', () => {
|
||||
@@ -99,22 +91,22 @@ describe('prices', () => {
|
||||
new Trade({
|
||||
v2Routes: [
|
||||
{
|
||||
routev2: new V2Route([pair12, pair23], token1, token3),
|
||||
inputAmount: currencyAmount(token1, 1000),
|
||||
outputAmount: currencyAmount(token3, 1000),
|
||||
routev2: new V2Route([pair12, pair23], TEST_TOKEN_1, TEST_TOKEN_3),
|
||||
inputAmount: toCurrencyAmount(TEST_TOKEN_1, 1000),
|
||||
outputAmount: toCurrencyAmount(TEST_TOKEN_3, 1000),
|
||||
},
|
||||
],
|
||||
v3Routes: [
|
||||
{
|
||||
routev3: new V3Route([pool13], token1, token3),
|
||||
inputAmount: currencyAmount(token1, 1000),
|
||||
outputAmount: currencyAmount(token3, 1000),
|
||||
routev3: new V3Route([TEST_POOL_13], TEST_TOKEN_1, TEST_TOKEN_3),
|
||||
inputAmount: toCurrencyAmount(TEST_TOKEN_1, 1000),
|
||||
outputAmount: toCurrencyAmount(TEST_TOKEN_3, 1000),
|
||||
},
|
||||
],
|
||||
tradeType: TradeType.EXACT_INPUT,
|
||||
})
|
||||
)
|
||||
).toEqual(currencyAmount(token1, 8))
|
||||
).toEqual(toCurrencyAmount(TEST_TOKEN_1, 8))
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -4,12 +4,13 @@
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"alwaysStrict": true,
|
||||
"baseUrl": "src",
|
||||
"composite": true,
|
||||
"downlevelIteration": true,
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "react-jsx",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"lib": ["DOM", "DOM.Iterable", "ESNext"],
|
||||
"module": "esnext",
|
||||
"moduleResolution": "node",
|
||||
"noEmit": true,
|
||||
@@ -22,10 +23,11 @@
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"strictNullChecks": true,
|
||||
"target": "es5",
|
||||
"target": "ESNext",
|
||||
"tsBuildInfoFile": "node_modules/.cache/.tsbuildinfo",
|
||||
"types": ["jest"],
|
||||
"useUnknownInCatchVariables": false
|
||||
},
|
||||
"exclude": ["node_modules", "cypress"],
|
||||
"include": ["src/**/*"]
|
||||
"include": ["src/**/*", "src/**/*.json"]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user