Compare commits

...

25 Commits

Author SHA1 Message Date
yyip-dev
95b9624bca feat: Update to app store link with tracking params (#6398)
Update to app store link with tracking params
2023-04-19 18:35:05 -04:00
Zach Pomerantz
04a0479236 test: add cypress-hardhat (#6394)
* chore: ignore hardhat cache files

* test: add forking hardhat config

* test: install cypress-hardhat

* build: add cypress-hardhat

* fix: lint

* build: add hardhat

* build: add @sentry/types

* fix: better origin
2023-04-19 10:50:25 -07:00
Jack Short
55f1e35ffc fix: pwat not setting correct spender (#6320)
* fix: pwat not setting correct spender

* todo

* responding to comments
2023-04-19 13:43:37 -04:00
lynn
01a3aa1c92 chore: add constants for testing (#6367)
* init

* use constants

* change address

* move noop

* add eslint rule

* return null in noop

* fixes
2023-04-19 12:21:30 -04:00
Zach Pomerantz
7121b4aa1c test: expand collectCoverageFrom (#6393)
* test: expand collectCoverageFrom

* fix: improve types coverage

* fix: target generated dirs

* fix: glob dirs
2023-04-19 09:04:41 -07:00
Zach Pomerantz
a538bf0b69 build: target ESNext for dev (#6379)
* build: add caching to eslint

* build: add caching to jest

* build: add caching to tsc

* build: add caching to actions

* fix: upgrade upload-artifact to v3

* build: update craco eslint cacheLocation

* build: target ESNext for dev

* merge again
2023-04-17 14:26:00 -07:00
Zach Pomerantz
135cb8fb34 fix: translate "Get started" on landing page (#6371)
* fix: translate "Get started" on landing page

* lint: html
2023-04-17 14:03:38 -07:00
cartcrom
bf50582d38 test: local activity tests (#6341)
* refactor: move MP files into subfolders

* refactor: consolidate MP subfolder file-naming scheme

* test: add tests for parseLocal.ts

* refactor: update existing parseLocal tests
2023-04-17 13:03:43 -07:00
lynn
110e23d6eb fix: fix padding for transaction toast notifications (#6373)
* padding fixes, special casing for txn

* add drop shadow, change width to 348px, remove debug code

* opacity animation

* address comments

* one more change

* respond to tina comments

* name change

* add $ to padding
2023-04-17 14:39:38 -04:00
Zach Pomerantz
7ab6a17b42 build: utilize the node_modules/.cache (#6364)
* build: add caching to eslint

* build: add caching to jest

* build: add caching to tsc

* build: add caching to actions

* fix: upgrade upload-artifact to v3

* build: update craco eslint cacheLocation

* fix: pr nits
2023-04-17 09:28:00 -07:00
lynn
7ad13c96a8 fix: fix slow yarn start (#6369)
init
2023-04-14 11:45:33 -05:00
Vignesh Mohankumar
4e99cc4d93 build: send url without hash to sentry (#6352)
* build: send url without hash to sentry

* comments

* Update src/tracing/index.ts

Co-authored-by: Zach Pomerantz <zzmp@uniswap.org>

* Update src/tracing/index.ts

Co-authored-by: Zach Pomerantz <zzmp@uniswap.org>

* add test

* move files around
2023-04-13 17:49:29 -04:00
Vignesh Mohankumar
6d29815f59 fix: ignore error caused by Cloudflare HTML response (#6356)
* fix: ignore error caused by Cloudflare HTML response

* add test

* Update src/tracing/errors.test.ts

Co-authored-by: Zach Pomerantz <zzmp@uniswap.org>

* Update src/tracing/errors.ts

Co-authored-by: Zach Pomerantz <zzmp@uniswap.org>

* Update src/tracing/errors.ts

Co-authored-by: Zach Pomerantz <zzmp@uniswap.org>

* multiline
2023-04-13 17:48:50 -04:00
Zach Pomerantz
4888fe23df test: add jest.asMock (#6310)
* test: add jest.asMock

* test: use mocked instead

* test: split test-utils to prevent interaction

* test: whoops missed one

* Merge but actually this time
2023-04-13 12:44:06 -07:00
Zach Pomerantz
ef9ecd9ce2 build: optimize build by splitting out typechecking and linting (#6323)
* build: move typecheck/lint out of build

* build: add typecheck to test action

* build: fix lint to use gitignore

* fix: correctly lint/check

* fix: simplify lint

* build: back out eslint array-ification

* test(lint): add comment RE config/typings

* build: clarify craco webpack plugin mods

* build: simplify craco webpack with functional methods

* build: rm unused IgnorePlugin

* test(lint): order imports
2023-04-13 12:43:47 -07:00
Jack Short
f5d0804c46 feat: creating feature flag for details v2 page (#6359)
* feat: creating feature flag for details v2 page

* eslint ignore

* moving details v2 under trace
2023-04-13 15:43:14 -04:00
Vignesh Mohankumar
0bac257254 fix: handle invalid chainId on position page (#6338)
* fix: handle invalid chainId on position page

* fix

* add test

* Revert "add test"

This reverts commit d18742aa50.

* pr comments

* rename

* fix
2023-04-13 15:40:31 -04:00
lynn
a77752ab83 fix: re-implement dark mode for connector icons (#6329)
* init

* init

* Update src/connection/index.test.tsx

Co-authored-by: Zach Pomerantz <zzmp@uniswap.org>

* Update src/components/Identicon/StatusIcon.test.tsx

Co-authored-by: Zach Pomerantz <zzmp@uniswap.org>

* Update src/components/Identicon/StatusIcon.test.tsx

Co-authored-by: Zach Pomerantz <zzmp@uniswap.org>

* address comments

* unit test + remove _url in names

---------

Co-authored-by: Zach Pomerantz <zzmp@uniswap.org>
2023-04-13 11:17:48 -04:00
cartcrom
bf31ca4f06 feat: updated banner (#6355)
* feat: updated banner

* fix: linted

* feat: add download link to overflow menu

* feat: hover animation

* fix: update landing screen hide logic

* feat: added descriptor comment for stopPropogation

* fix: translations & responsiveness of button text

* fix: icon sizing / padding + word casing

* fix: Learn more casing
2023-04-12 22:00:05 -04:00
Charles Bachmeier
ed8afbd851 fix: App crashing when changing sorting or toggling usd price on trneding NFTs table (#6354)
* fix app breaking when changing sorting or toggling usd price

* compare floor change correctly

---------

Co-authored-by: Charles Bachmeier <charlie@genie.xyz>
2023-04-12 15:49:04 -07:00
Vignesh Mohankumar
47b6a7c4d5 build: add chainId as tag in sentry ErrorBoundary (#6345)
* build: add chainId to sentry tags

* set it in web3provider

* set before sending
2023-04-12 14:43:37 -04:00
cartcrom
086fc65457 feat: remove /wallet route (#6350)
* feat: replace internal microsite routes with links to external site

* feat: use updated analytics events from events repo

* fix: remove unnused empty wallet page
2023-04-12 13:55:53 -04:00
cartcrom
7df53f30a0 feat: uniwallet banner (#6344)
* feat: update wallet banner display logic

* fix: linted

* fix: learn more link
2023-04-12 12:03:00 -04:00
Jack Short
66497a0108 fix: last sale nan (#6337)
* fix: last sale nan

* correct formatted price

* responding to comments

* refactoring for readability
2023-04-12 11:45:48 -04:00
cartcrom
e0eb701bc0 feat: log connection activation/errors (#6333)
* feat: log connection activation/errors in console.debug

* fix: remove now-redundant comment
2023-04-12 10:36:01 -04:00
79 changed files with 2188 additions and 609 deletions

View File

@@ -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: {

View File

@@ -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

View File

@@ -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
View File

@@ -17,6 +17,7 @@ schema.graphql
# testing
/coverage
/cache
# builds
/build

View File

@@ -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

View File

@@ -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
View 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,
},
},
},
}

View File

@@ -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

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 990 KiB

View File

@@ -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>
}

View File

@@ -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,
},
})
})
})

View File

@@ -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}`

View File

@@ -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 }

View File

@@ -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'

View File

@@ -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 />

View File

@@ -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>
)

View File

@@ -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>
)
}

View File

@@ -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()

View File

@@ -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}

View File

@@ -1,5 +1,5 @@
import { getConnections } from 'connection'
import { render } from 'test-utils'
import { render } from 'test-utils/render'
import StatusIcon from './StatusIcon'

View File

@@ -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>
)
}

View File

@@ -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>

View File

@@ -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>

View File

@@ -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
}

View File

@@ -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;

View File

@@ -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', () => {

View File

@@ -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'

View File

@@ -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 '.'

View File

@@ -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"
/>

View File

@@ -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;

View File

@@ -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 />}

View File

@@ -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,

View File

@@ -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'

View File

@@ -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)
})

View File

@@ -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

View File

@@ -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)

View File

@@ -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',
}

View File

@@ -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 }

View 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 }

View File

@@ -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 })
})
})

View File

@@ -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'

View File

@@ -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)

View File

@@ -1,4 +1,4 @@
import { render } from 'test-utils'
import { render } from 'test-utils/render'
import Bag from './Bag'

View File

@@ -1,5 +1,5 @@
import { Markets } from 'nft/types'
import { render } from 'test-utils'
import { render } from 'test-utils/render'
import { MarketplaceContainer } from './icons'

View File

@@ -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]

View 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>
)
}

View File

@@ -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

View File

@@ -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>) => {

View File

@@ -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} />
}

View File

@@ -1,4 +1,4 @@
import { render } from 'test-utils'
import { render } from 'test-utils/render'
import { EmptyWalletModule } from './EmptyWalletContent'

View File

@@ -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(

View 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)
})
})

View File

@@ -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)

View File

@@ -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>
</>
)

View File

@@ -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 />} />

View File

@@ -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>

View File

@@ -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'

View File

@@ -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 ? (

View File

@@ -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 '.'

View File

@@ -1,3 +0,0 @@
export default function Wallet() {
return <div>uniswap wallet pretty cool</div>
}

View File

@@ -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)
})
})

View File

@@ -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

View File

@@ -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', () => {

View File

@@ -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'

View 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
View 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>
}

View File

@@ -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,
})

View File

@@ -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%);',

View File

@@ -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)
})
})

View File

@@ -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

View File

@@ -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, {

View File

@@ -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 }) => {

View File

@@ -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
View File

@@ -0,0 +1,4 @@
/** No-op function. Returns `null` to satisfy most React typings. */
export default function noop() {
return null
}

View File

@@ -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))
})
})

View File

@@ -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"]
}

1113
yarn.lock

File diff suppressed because it is too large Load Diff