Compare commits
24 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2ffc8a0bdf | ||
|
|
5ec9cdc5c4 | ||
|
|
4d85775d90 | ||
|
|
c1c59ca692 | ||
|
|
f29d97413e | ||
|
|
a078d94a38 | ||
|
|
c9c3329bc3 | ||
|
|
13d0b70fa8 | ||
|
|
b852e4e64a | ||
|
|
55bd3555be | ||
|
|
972a65066c | ||
|
|
39a212f762 | ||
|
|
c362f4fe39 | ||
|
|
271ef580e1 | ||
|
|
81ced4cb8b | ||
|
|
ab214a8133 | ||
|
|
1b2d86ae3a | ||
|
|
40cac44e07 | ||
|
|
4e6d28cff4 | ||
|
|
709fad0804 | ||
|
|
573f4c873a | ||
|
|
d300db669f | ||
|
|
fb8217ddea | ||
|
|
7b9a23d920 |
53
.github/pull_request_template.md
vendored
53
.github/pull_request_template.md
vendored
@@ -1,22 +1,45 @@
|
||||
<!-- Your PR title must follow conventional commits: https://github.com/Uniswap/interface#pr-title -->
|
||||
|
||||
## Description
|
||||
|
||||
_[Summary of change, motivation, and context.]_
|
||||
|
||||
- _Link to JIRA ticket, slack thread, or relevant docs helpful for providing context to reviewers._
|
||||
|
||||
- _Note: Your PR title must follow conventions [outlined here](https://github.com/Uniswap/interface#contributions)._
|
||||
|
||||
## Screen Capture
|
||||
| Before | After |
|
||||
| ---------------- |-----------------|
|
||||
| _insert_before_ | _insert_after_ |
|
||||
<!-- Summary of change, including motivation and context. -->
|
||||
<!-- Use verb-driven language: "Fixes XYZ" instead of "This change fixes XYZ" -->
|
||||
|
||||
|
||||
## Test Plan
|
||||
#### Manual
|
||||
<!-- Delete inapplicable lines: -->
|
||||
_JIRA ticket:_
|
||||
_Slack thread:_
|
||||
_Relevant docs:_
|
||||
|
||||
_[Steps of how you are testing the change and ensuring no regression.]_
|
||||
|
||||
#### Automated
|
||||
<!-- Delete this section if your change does not affect UI. -->
|
||||
## Screen capture
|
||||
|
||||
| Before | After (Desktop) | After (Mobile) |
|
||||
| ------------ |---------------- | -------------- |
|
||||
| paste_before | past_after | paste_after |
|
||||
|
||||
|
||||
## Test plan
|
||||
|
||||
<!-- Delete this section if your change is not a bug fix. -->
|
||||
### Reproducing the error
|
||||
|
||||
<!-- Include steps to reproduce the bug. -->
|
||||
1.
|
||||
|
||||
### QA (ie manual testing)
|
||||
|
||||
<!-- Include steps to test the change, ensuring no regression. -->
|
||||
- [ ] N/A
|
||||
|
||||
|
||||
#### Devices
|
||||
<!-- If applicable, include different devices and screen sizes that may be affected, and how you've tested them. -->
|
||||
|
||||
|
||||
### Automated testing
|
||||
|
||||
<!-- If N/A, check and note so it is obvious to your reviewers and does not show up as an incomplete task. -->
|
||||
<!-- eg - [x] Unit test N/A -->
|
||||
- [ ] Unit test
|
||||
- [ ] Integration/E2E test
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/* eslint-disable @typescript-eslint/no-var-requires */
|
||||
/* eslint-env node */
|
||||
const { VanillaExtractPlugin } = require('@vanilla-extract/webpack-plugin')
|
||||
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
|
||||
const { DefinePlugin } = require('webpack')
|
||||
|
||||
@@ -6,6 +6,7 @@ export default defineConfig({
|
||||
videoUploadOnPasses: false,
|
||||
defaultCommandTimeout: 24000, // 2x average block time
|
||||
chromeWebSecurity: false,
|
||||
retries: { runMode: 2 },
|
||||
e2e: {
|
||||
setupNodeEvents(on, config) {
|
||||
codeCoverageTask(on, config)
|
||||
|
||||
@@ -26,7 +26,15 @@ describe('Landing Page', () => {
|
||||
|
||||
it('allows navigation to pool', () => {
|
||||
cy.viewport(2000, 1600)
|
||||
cy.visit('/swap')
|
||||
cy.get(getTestSelector('pool-nav-link')).first().click()
|
||||
cy.url().should('include', '/pools')
|
||||
})
|
||||
|
||||
it('allows navigation to pool on mobile', () => {
|
||||
cy.viewport('iphone-6')
|
||||
cy.visit('/swap')
|
||||
cy.get(getTestSelector('pool-nav-link')).last().click()
|
||||
cy.url().should('include', '/pools')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -77,4 +77,12 @@ describe('Wallet Dropdown', () => {
|
||||
cy.get(getTestSelector('theme-auto')).click()
|
||||
cy.get(getTestSelector('wallet-header')).should('have.css', 'color', 'rgb(119, 128, 160)')
|
||||
})
|
||||
|
||||
it('should use a bottom sheet and dismiss when on a mobile screen size', () => {
|
||||
visit(true)
|
||||
cy.viewport('iphone-6')
|
||||
cy.get(getTestSelector('web3-status-connected')).click()
|
||||
cy.root().click(15, 40)
|
||||
cy.get(getTestSelector('wallet-settings')).should('not.be.visible')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -37,8 +37,12 @@
|
||||
"src/lib/utils/**/*.ts*",
|
||||
"src/pages/**/*.ts*",
|
||||
"src/state/**/*.ts*",
|
||||
"src/tracing/**/*.ts*",
|
||||
"src/utils/**/*.ts*"
|
||||
],
|
||||
"coveragePathIgnorePatterns": [
|
||||
".snap"
|
||||
],
|
||||
"coverageThreshold": {
|
||||
"global": {
|
||||
"branches": 4,
|
||||
@@ -66,6 +70,7 @@
|
||||
"@lingui/cli": "^3.9.0",
|
||||
"@testing-library/jest-dom": "^5.16.4",
|
||||
"@testing-library/react": "^13.1",
|
||||
"@testing-library/user-event": "^14.4.3",
|
||||
"@typechain/ethers-v5": "^7.0.0",
|
||||
"@types/array.prototype.flat": "^1.2.1",
|
||||
"@types/array.prototype.flatmap": "^1.2.2",
|
||||
@@ -136,7 +141,7 @@
|
||||
"@sentry/tracing": "^7.45.0",
|
||||
"@types/react-window-infinite-loader": "^1.0.6",
|
||||
"@uniswap/analytics": "^1.3.1",
|
||||
"@uniswap/analytics-events": "^2.8.0",
|
||||
"@uniswap/analytics-events": "^2.9.0",
|
||||
"@uniswap/conedison": "^1.4.0",
|
||||
"@uniswap/governance": "^1.0.2",
|
||||
"@uniswap/liquidity-staker": "^1.0.2",
|
||||
|
||||
@@ -30,13 +30,13 @@ import { useCloseModal, useFiatOnrampAvailability, useOpenModal, useToggleModal
|
||||
import { ApplicationModal } from '../../state/application/reducer'
|
||||
import { useUserHasAvailableClaim, useUserUnclaimedAmount } from '../../state/claim/hooks'
|
||||
import StatusIcon from '../Identicon/StatusIcon'
|
||||
import { useToggleWalletDrawer } from '.'
|
||||
import { useToggleAccountDrawer } from '.'
|
||||
import IconButton, { IconHoverText } from './IconButton'
|
||||
import MiniPortfolio from './MiniPortfolio'
|
||||
import { portfolioFadeInAnimation } from './MiniPortfolio/PortfolioRow'
|
||||
|
||||
const AuthenticatedHeaderWrapper = styled.div`
|
||||
padding: 14px 12px 16px 16px;
|
||||
padding: 20px 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
@@ -184,7 +184,7 @@ export default function AuthenticatedHeader({ account, openSettings }: { account
|
||||
dispatch(updateSelectedWallet({ wallet: undefined }))
|
||||
}, [connector, dispatch])
|
||||
|
||||
const toggleWalletDrawer = useToggleWalletDrawer()
|
||||
const toggleWalletDrawer = useToggleAccountDrawer()
|
||||
|
||||
const navigateToProfile = useCallback(() => {
|
||||
toggleWalletDrawer()
|
||||
@@ -287,6 +287,16 @@ export default function AuthenticatedHeader({ account, openSettings }: { account
|
||||
<LoadingBubble height="16px" width="100px" margin="4px 0 20px 0" />
|
||||
</Column>
|
||||
)}
|
||||
{!shouldDisableNFTRoutes && (
|
||||
<HeaderButton
|
||||
data-testid="nft-view-self-nfts"
|
||||
onClick={navigateToProfile}
|
||||
size={ButtonSize.medium}
|
||||
emphasis={ButtonEmphasis.medium}
|
||||
>
|
||||
<Trans>View and sell NFTs</Trans>
|
||||
</HeaderButton>
|
||||
)}
|
||||
<HeaderButton
|
||||
size={ButtonSize.medium}
|
||||
emphasis={ButtonEmphasis.medium}
|
||||
@@ -306,16 +316,6 @@ export default function AuthenticatedHeader({ account, openSettings }: { account
|
||||
</>
|
||||
)}
|
||||
</HeaderButton>
|
||||
{!shouldDisableNFTRoutes && (
|
||||
<HeaderButton
|
||||
data-testid="nft-view-self-nfts"
|
||||
onClick={navigateToProfile}
|
||||
size={ButtonSize.medium}
|
||||
emphasis={ButtonEmphasis.medium}
|
||||
>
|
||||
<Trans>View and sell NFTs</Trans>
|
||||
</HeaderButton>
|
||||
)}
|
||||
{Boolean(!fiatOnrampAvailable && fiatOnrampAvailabilityChecked) && (
|
||||
<FiatOnrampNotAvailableText marginTop="8px">
|
||||
<Trans>Not available in your region</Trans>
|
||||
@@ -1,7 +1,7 @@
|
||||
import { t } from '@lingui/macro'
|
||||
import { useAccountDrawer } from 'components/AccountDrawer'
|
||||
import Column from 'components/Column'
|
||||
import { LoadingBubble } from 'components/Tokens/loading'
|
||||
import { useWalletDrawer } from 'components/WalletDropdown'
|
||||
import { getYear, isSameDay, isSameMonth, isSameWeek, isSameYear } from 'date-fns'
|
||||
import { TransactionStatus, useTransactionListQuery } from 'graphql/data/__generated__/types-and-hooks'
|
||||
import { PollingInterval } from 'graphql/data/util'
|
||||
@@ -98,10 +98,10 @@ function combineActivities(localMap: ActivityMap = {}, remoteMap: ActivityMap =
|
||||
const lastFetchedAtom = atom<number | undefined>(0)
|
||||
|
||||
export function ActivityTab({ account }: { account: string }) {
|
||||
const [drawerOpen, toggleWalletDrawer] = useWalletDrawer()
|
||||
const [drawerOpen, toggleWalletDrawer] = useAccountDrawer()
|
||||
const [lastFetched, setLastFetched] = useAtom(lastFetchedAtom)
|
||||
|
||||
const localMap = useLocalActivities()
|
||||
const localMap = useLocalActivities(account)
|
||||
|
||||
const { data, loading, refetch } = useTransactionListQuery({
|
||||
variables: { account },
|
||||
@@ -1,7 +1,7 @@
|
||||
// jest unit tests for the parseLocalActivity function
|
||||
|
||||
import { SupportedChainId, Token, TradeType } from '@uniswap/sdk-core'
|
||||
import { DAI, USDC_MAINNET } from 'constants/tokens'
|
||||
import { SupportedChainId, Token, TradeType as MockTradeType } from '@uniswap/sdk-core'
|
||||
import { DAI as MockDAI, USDC_MAINNET as MockUSDC_MAINNET } from 'constants/tokens'
|
||||
import { TokenAddressMap } from 'state/lists/hooks'
|
||||
import { WrappedTokenInfo } from 'state/lists/wrappedTokenInfo'
|
||||
import {
|
||||
@@ -10,23 +10,24 @@ import {
|
||||
TransactionDetails,
|
||||
TransactionType,
|
||||
} from 'state/transactions/types'
|
||||
import { renderHook } from 'test-utils'
|
||||
|
||||
import { parseLocalActivity } from './parseLocal'
|
||||
import { parseLocalActivity, useLocalActivities } from './parseLocal'
|
||||
|
||||
const oneUSDCRaw = '1000000'
|
||||
const oneDAIRaw = '1000000000000000000'
|
||||
|
||||
function buildSwapInfo(
|
||||
type: TradeType,
|
||||
function mockSwapInfo(
|
||||
type: MockTradeType,
|
||||
inputCurrency: Token,
|
||||
inputCurrencyAmountRaw: string,
|
||||
outputCurrency: Token,
|
||||
outputCurrencyAmountRaw: string
|
||||
): ExactInputSwapTransactionInfo | ExactOutputSwapTransactionInfo {
|
||||
if (type === TradeType.EXACT_INPUT) {
|
||||
if (type === MockTradeType.EXACT_INPUT) {
|
||||
return {
|
||||
type: TransactionType.SWAP,
|
||||
tradeType: TradeType.EXACT_INPUT,
|
||||
tradeType: MockTradeType.EXACT_INPUT,
|
||||
inputCurrencyId: inputCurrency.address,
|
||||
inputCurrencyAmountRaw,
|
||||
outputCurrencyId: outputCurrency.address,
|
||||
@@ -36,7 +37,7 @@ function buildSwapInfo(
|
||||
} else {
|
||||
return {
|
||||
type: TransactionType.SWAP,
|
||||
tradeType: TradeType.EXACT_OUTPUT,
|
||||
tradeType: MockTradeType.EXACT_OUTPUT,
|
||||
inputCurrencyId: inputCurrency.address,
|
||||
expectedInputCurrencyAmountRaw: inputCurrencyAmountRaw,
|
||||
maximumInputCurrencyAmountRaw: inputCurrencyAmountRaw,
|
||||
@@ -46,7 +47,44 @@ function buildSwapInfo(
|
||||
}
|
||||
}
|
||||
|
||||
function buildTokenAddressMap(...tokens: WrappedTokenInfo[]): TokenAddressMap {
|
||||
const mockAccount1 = '0x000000000000000000000000000000000000000001'
|
||||
const mockAccount2 = '0x000000000000000000000000000000000000000002'
|
||||
const mockChainId = SupportedChainId.MAINNET
|
||||
|
||||
jest.mock('../../../../state/transactions/hooks', () => {
|
||||
return {
|
||||
useMultichainTransactions: () => {
|
||||
return [
|
||||
[
|
||||
{
|
||||
info: mockSwapInfo(MockTradeType.EXACT_INPUT, MockUSDC_MAINNET, oneUSDCRaw, MockDAI, oneDAIRaw),
|
||||
hash: '0x123',
|
||||
from: mockAccount1,
|
||||
} as TransactionDetails,
|
||||
mockChainId,
|
||||
],
|
||||
[
|
||||
{
|
||||
info: mockSwapInfo(MockTradeType.EXACT_INPUT, MockUSDC_MAINNET, oneUSDCRaw, MockDAI, oneDAIRaw),
|
||||
hash: '0x456',
|
||||
from: mockAccount2,
|
||||
} as TransactionDetails,
|
||||
mockChainId,
|
||||
],
|
||||
[
|
||||
{
|
||||
info: mockSwapInfo(MockTradeType.EXACT_INPUT, MockUSDC_MAINNET, oneUSDCRaw, MockDAI, oneDAIRaw),
|
||||
hash: '0x789',
|
||||
from: mockAccount2,
|
||||
} as TransactionDetails,
|
||||
mockChainId,
|
||||
],
|
||||
]
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
function mockTokenAddressMap(...tokens: WrappedTokenInfo[]): TokenAddressMap {
|
||||
return {
|
||||
[SupportedChainId.MAINNET]: Object.fromEntries(tokens.map((token) => [token.address, { token }])),
|
||||
}
|
||||
@@ -55,27 +93,27 @@ function buildTokenAddressMap(...tokens: WrappedTokenInfo[]): TokenAddressMap {
|
||||
describe('parseLocalActivity', () => {
|
||||
it('returns swap activity fields with known tokens, exact input', () => {
|
||||
const details = {
|
||||
info: buildSwapInfo(TradeType.EXACT_INPUT, USDC_MAINNET, oneUSDCRaw, DAI, oneDAIRaw),
|
||||
info: mockSwapInfo(MockTradeType.EXACT_INPUT, MockUSDC_MAINNET, oneUSDCRaw, MockDAI, oneDAIRaw),
|
||||
receipt: {
|
||||
transactionHash: '0x123',
|
||||
status: 1,
|
||||
},
|
||||
} as TransactionDetails
|
||||
const chainId = SupportedChainId.MAINNET
|
||||
const tokens = buildTokenAddressMap(USDC_MAINNET as WrappedTokenInfo, DAI as WrappedTokenInfo)
|
||||
const tokens = mockTokenAddressMap(MockUSDC_MAINNET as WrappedTokenInfo, MockDAI as WrappedTokenInfo)
|
||||
expect(parseLocalActivity(details, chainId, tokens)).toEqual({
|
||||
chainId: 1,
|
||||
currencies: [USDC_MAINNET, DAI],
|
||||
currencies: [MockUSDC_MAINNET, MockDAI],
|
||||
descriptor: '1.00 USDC for 1.00 DAI',
|
||||
hash: undefined,
|
||||
receipt: {
|
||||
id: '0x123',
|
||||
info: {
|
||||
type: 1,
|
||||
tradeType: TradeType.EXACT_INPUT,
|
||||
inputCurrencyId: USDC_MAINNET.address,
|
||||
tradeType: MockTradeType.EXACT_INPUT,
|
||||
inputCurrencyId: MockUSDC_MAINNET.address,
|
||||
inputCurrencyAmountRaw: oneUSDCRaw,
|
||||
outputCurrencyId: DAI.address,
|
||||
outputCurrencyId: MockDAI.address,
|
||||
expectedOutputCurrencyAmountRaw: oneDAIRaw,
|
||||
minimumOutputCurrencyAmountRaw: oneDAIRaw,
|
||||
},
|
||||
@@ -91,28 +129,28 @@ describe('parseLocalActivity', () => {
|
||||
|
||||
it('returns swap activity fields with known tokens, exact output', () => {
|
||||
const details = {
|
||||
info: buildSwapInfo(TradeType.EXACT_OUTPUT, USDC_MAINNET, oneUSDCRaw, DAI, oneDAIRaw),
|
||||
info: mockSwapInfo(MockTradeType.EXACT_OUTPUT, MockUSDC_MAINNET, oneUSDCRaw, MockDAI, oneDAIRaw),
|
||||
receipt: {
|
||||
transactionHash: '0x123',
|
||||
status: 1,
|
||||
},
|
||||
} as TransactionDetails
|
||||
const chainId = SupportedChainId.MAINNET
|
||||
const tokens = buildTokenAddressMap(USDC_MAINNET as WrappedTokenInfo, DAI as WrappedTokenInfo)
|
||||
const tokens = mockTokenAddressMap(MockUSDC_MAINNET as WrappedTokenInfo, MockDAI as WrappedTokenInfo)
|
||||
expect(parseLocalActivity(details, chainId, tokens)).toEqual({
|
||||
chainId: 1,
|
||||
currencies: [USDC_MAINNET, DAI],
|
||||
currencies: [MockUSDC_MAINNET, MockDAI],
|
||||
descriptor: '1.00 USDC for 1.00 DAI',
|
||||
hash: undefined,
|
||||
receipt: {
|
||||
id: '0x123',
|
||||
info: {
|
||||
type: 1,
|
||||
tradeType: TradeType.EXACT_OUTPUT,
|
||||
inputCurrencyId: USDC_MAINNET.address,
|
||||
tradeType: MockTradeType.EXACT_OUTPUT,
|
||||
inputCurrencyId: MockUSDC_MAINNET.address,
|
||||
expectedInputCurrencyAmountRaw: oneUSDCRaw,
|
||||
maximumInputCurrencyAmountRaw: oneUSDCRaw,
|
||||
outputCurrencyId: DAI.address,
|
||||
outputCurrencyId: MockDAI.address,
|
||||
outputCurrencyAmountRaw: oneDAIRaw,
|
||||
},
|
||||
receipt: { status: 1, transactionHash: '0x123' },
|
||||
@@ -127,7 +165,7 @@ describe('parseLocalActivity', () => {
|
||||
|
||||
it('returns swap activity fields with unknown tokens', () => {
|
||||
const details = {
|
||||
info: buildSwapInfo(TradeType.EXACT_INPUT, USDC_MAINNET, oneUSDCRaw, DAI, oneDAIRaw),
|
||||
info: mockSwapInfo(MockTradeType.EXACT_INPUT, MockUSDC_MAINNET, oneUSDCRaw, MockDAI, oneDAIRaw),
|
||||
receipt: {
|
||||
transactionHash: '0x123',
|
||||
status: 1,
|
||||
@@ -144,10 +182,10 @@ describe('parseLocalActivity', () => {
|
||||
id: '0x123',
|
||||
info: {
|
||||
type: 1,
|
||||
tradeType: TradeType.EXACT_INPUT,
|
||||
inputCurrencyId: USDC_MAINNET.address,
|
||||
tradeType: MockTradeType.EXACT_INPUT,
|
||||
inputCurrencyId: MockUSDC_MAINNET.address,
|
||||
inputCurrencyAmountRaw: oneUSDCRaw,
|
||||
outputCurrencyId: DAI.address,
|
||||
outputCurrencyId: MockDAI.address,
|
||||
expectedOutputCurrencyAmountRaw: oneDAIRaw,
|
||||
minimumOutputCurrencyAmountRaw: oneDAIRaw,
|
||||
},
|
||||
@@ -160,4 +198,12 @@ describe('parseLocalActivity', () => {
|
||||
title: 'Swapped',
|
||||
})
|
||||
})
|
||||
|
||||
it('only returns activity for the current account', () => {
|
||||
const account1Activites = renderHook(() => useLocalActivities(mockAccount1)).result.current
|
||||
const account2Activites = renderHook(() => useLocalActivities(mockAccount2)).result.current
|
||||
|
||||
expect(Object.values(account1Activites)).toHaveLength(1)
|
||||
expect(Object.values(account2Activites)).toHaveLength(2)
|
||||
})
|
||||
})
|
||||
@@ -2,7 +2,6 @@ import { t } from '@lingui/macro'
|
||||
import { formatCurrencyAmount } from '@uniswap/conedison/format'
|
||||
import { Currency, CurrencyAmount, TradeType } from '@uniswap/sdk-core'
|
||||
import { nativeOnChain } from '@uniswap/smart-order-router'
|
||||
import { useWeb3React } from '@web3-react/core'
|
||||
import { SupportedChainId } from 'constants/chains'
|
||||
import { TransactionPartsFragment, TransactionStatus } from 'graphql/data/__generated__/types-and-hooks'
|
||||
import { useMemo } from 'react'
|
||||
@@ -135,71 +134,69 @@ export function parseLocalActivity(
|
||||
chainId: SupportedChainId,
|
||||
tokens: TokenAddressMap
|
||||
): Activity | undefined {
|
||||
const status = !details.receipt
|
||||
? TransactionStatus.Pending
|
||||
: details.receipt.status === 1 || details.receipt?.status === undefined
|
||||
? TransactionStatus.Confirmed
|
||||
: TransactionStatus.Failed
|
||||
try {
|
||||
const status = !details.receipt
|
||||
? TransactionStatus.Pending
|
||||
: details.receipt.status === 1 || details.receipt?.status === undefined
|
||||
? TransactionStatus.Confirmed
|
||||
: TransactionStatus.Failed
|
||||
|
||||
const receipt: TransactionPartsFragment | undefined = details.receipt
|
||||
? {
|
||||
id: details.receipt.transactionHash,
|
||||
...details.receipt,
|
||||
...details,
|
||||
status,
|
||||
}
|
||||
: undefined
|
||||
const receipt: TransactionPartsFragment | undefined = details.receipt
|
||||
? {
|
||||
id: details.receipt.transactionHash,
|
||||
...details.receipt,
|
||||
...details,
|
||||
status,
|
||||
}
|
||||
: undefined
|
||||
|
||||
const defaultFields = {
|
||||
hash: details.hash,
|
||||
chainId,
|
||||
title: getActivityTitle(details.info.type, status),
|
||||
status,
|
||||
timestamp: (details.confirmedTime ?? details.addedTime) / 1000,
|
||||
receipt,
|
||||
const defaultFields = {
|
||||
hash: details.hash,
|
||||
chainId,
|
||||
title: getActivityTitle(details.info.type, status),
|
||||
status,
|
||||
timestamp: (details.confirmedTime ?? details.addedTime) / 1000,
|
||||
receipt,
|
||||
}
|
||||
|
||||
let additionalFields: Partial<Activity> = {}
|
||||
const info = details.info
|
||||
if (info.type === TransactionType.SWAP) {
|
||||
additionalFields = parseSwap(info, chainId, tokens)
|
||||
} else if (info.type === TransactionType.APPROVAL) {
|
||||
additionalFields = parseApproval(info, chainId, tokens)
|
||||
} else if (info.type === TransactionType.WRAP) {
|
||||
additionalFields = parseWrap(info, chainId, status)
|
||||
} else if (
|
||||
info.type === TransactionType.ADD_LIQUIDITY_V3_POOL ||
|
||||
info.type === TransactionType.REMOVE_LIQUIDITY_V3 ||
|
||||
info.type === TransactionType.ADD_LIQUIDITY_V2_POOL
|
||||
) {
|
||||
additionalFields = parseLP(info, chainId, tokens)
|
||||
} else if (info.type === TransactionType.COLLECT_FEES) {
|
||||
additionalFields = parseCollectFees(info, chainId, tokens)
|
||||
} else if (info.type === TransactionType.MIGRATE_LIQUIDITY_V3 || info.type === TransactionType.CREATE_V3_POOL) {
|
||||
additionalFields = parseMigrateCreateV3(info, chainId, tokens)
|
||||
}
|
||||
|
||||
return { ...defaultFields, ...additionalFields }
|
||||
} catch (error) {
|
||||
console.debug(`Failed to parse transaction ${details.hash}`, error)
|
||||
return undefined
|
||||
}
|
||||
|
||||
let additionalFields: Partial<Activity> = {}
|
||||
const info = details.info
|
||||
if (info.type === TransactionType.SWAP) {
|
||||
additionalFields = parseSwap(info, chainId, tokens)
|
||||
} else if (info.type === TransactionType.APPROVAL) {
|
||||
additionalFields = parseApproval(info, chainId, tokens)
|
||||
} else if (info.type === TransactionType.WRAP) {
|
||||
additionalFields = parseWrap(info, chainId, status)
|
||||
} else if (
|
||||
info.type === TransactionType.ADD_LIQUIDITY_V3_POOL ||
|
||||
info.type === TransactionType.REMOVE_LIQUIDITY_V3 ||
|
||||
info.type === TransactionType.ADD_LIQUIDITY_V2_POOL
|
||||
) {
|
||||
additionalFields = parseLP(info, chainId, tokens)
|
||||
} else if (info.type === TransactionType.COLLECT_FEES) {
|
||||
additionalFields = parseCollectFees(info, chainId, tokens)
|
||||
} else if (info.type === TransactionType.MIGRATE_LIQUIDITY_V3 || info.type === TransactionType.CREATE_V3_POOL) {
|
||||
additionalFields = parseMigrateCreateV3(info, chainId, tokens)
|
||||
}
|
||||
|
||||
return { ...defaultFields, ...additionalFields }
|
||||
}
|
||||
|
||||
export function useLocalActivities(): ActivityMap | undefined {
|
||||
export function useLocalActivities(account: string): ActivityMap {
|
||||
const allTransactions = useMultichainTransactions()
|
||||
const { chainId } = useWeb3React()
|
||||
const tokens = useCombinedActiveList()
|
||||
|
||||
return useMemo(
|
||||
() =>
|
||||
chainId
|
||||
? allTransactions.reduce((acc: { [hash: string]: Activity }, [transaction, chainId]) => {
|
||||
try {
|
||||
const localActivity = parseLocalActivity(transaction, chainId, tokens)
|
||||
if (localActivity) acc[localActivity.hash] = localActivity
|
||||
} catch (error) {
|
||||
console.error('Failed to parse local activity', transaction)
|
||||
}
|
||||
return acc
|
||||
}, {})
|
||||
: undefined,
|
||||
[allTransactions, chainId, tokens]
|
||||
)
|
||||
return useMemo(() => {
|
||||
const activityByHash: ActivityMap = {}
|
||||
for (const [transaction, chainId] of allTransactions) {
|
||||
if (transaction.from !== account) continue
|
||||
|
||||
activityByHash[transaction.hash] = parseLocalActivity(transaction, chainId, tokens)
|
||||
}
|
||||
return activityByHash
|
||||
}, [account, allTransactions, tokens])
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
import { sendAnalyticsEvent, useTrace } from '@uniswap/analytics'
|
||||
import { InterfaceElementName, SharedEventName } from '@uniswap/analytics-events'
|
||||
import { useToggleAccountDrawer } from 'components/AccountDrawer'
|
||||
import Column from 'components/Column'
|
||||
import Row from 'components/Row'
|
||||
import { useToggleWalletDrawer } from 'components/WalletDropdown'
|
||||
import { Box } from 'nft/components/Box'
|
||||
import { NftCard } from 'nft/components/card'
|
||||
import { detailsHref } from 'nft/components/card/utils'
|
||||
@@ -46,7 +46,7 @@ export function NFT({
|
||||
mediaShouldBePlaying: boolean
|
||||
setCurrentTokenPlayingMedia: (tokenId: string | undefined) => void
|
||||
}) {
|
||||
const toggleWalletDrawer = useToggleWalletDrawer()
|
||||
const toggleWalletDrawer = useToggleAccountDrawer()
|
||||
const navigate = useNavigate()
|
||||
const trace = useTrace()
|
||||
|
||||
@@ -5,13 +5,22 @@ import { useState } from 'react'
|
||||
import InfiniteScroll from 'react-infinite-scroll-component'
|
||||
import styled from 'styled-components/macro'
|
||||
|
||||
import { useToggleWalletDrawer } from '..'
|
||||
import { useAccountDrawer } from '..'
|
||||
import { DEFAULT_NFT_QUERY_AMOUNT } from './constants'
|
||||
import { NFT } from './NFT'
|
||||
|
||||
export default function NFTs({ account }: { account: string }) {
|
||||
const { walletAssets, loading, hasNext, loadMore } = useNftBalance(account, [], [], DEFAULT_NFT_QUERY_AMOUNT)
|
||||
const toggleWalletDrawer = useToggleWalletDrawer()
|
||||
const [walletDrawerOpen, toggleWalletDrawer] = useAccountDrawer()
|
||||
const { walletAssets, loading, hasNext, loadMore } = useNftBalance(
|
||||
account,
|
||||
[],
|
||||
[],
|
||||
DEFAULT_NFT_QUERY_AMOUNT,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
!walletDrawerOpen
|
||||
)
|
||||
|
||||
const [currentTokenPlayingMedia, setCurrentTokenPlayingMedia] = useState<string | undefined>()
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
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 Pools from '.'
|
||||
import useMultiChainPositions from './useMultiChainPositions'
|
||||
|
||||
jest.mock('./useMultiChainPositions')
|
||||
const mockUseMultiChainPositions = useMultiChainPositions as jest.MockedFunction<typeof useMultiChainPositions>
|
||||
|
||||
const owner = '0xf5b6bb25f5beaea03dd014c6ef9fa9f3926bf36c'
|
||||
|
||||
const pool = new Pool(
|
||||
USDC_MAINNET,
|
||||
WETH9[SupportedChainId.MAINNET],
|
||||
FeeAmount.MEDIUM,
|
||||
'1851127709498178402383049949138810',
|
||||
'7076437181775065414',
|
||||
201189
|
||||
)
|
||||
|
||||
const position = new Position({
|
||||
pool,
|
||||
liquidity: 1341008833950736,
|
||||
tickLower: 200040,
|
||||
tickUpper: 202560,
|
||||
})
|
||||
const details = {
|
||||
nonce: BigNumber.from('0'),
|
||||
tokenId: BigNumber.from('0'),
|
||||
operator: '0x0',
|
||||
token0: USDC_MAINNET.address,
|
||||
token1: WETH9[SupportedChainId.MAINNET].address,
|
||||
fee: FeeAmount.MEDIUM,
|
||||
tickLower: -100,
|
||||
tickUpper: 100,
|
||||
liquidity: BigNumber.from('9000'),
|
||||
feeGrowthInside0LastX128: BigNumber.from('0'),
|
||||
feeGrowthInside1LastX128: BigNumber.from('0'),
|
||||
tokensOwed0: BigNumber.from('0'),
|
||||
tokensOwed1: BigNumber.from('0'),
|
||||
}
|
||||
const useMultiChainPositionsReturnValue = {
|
||||
positions: [
|
||||
{
|
||||
owner,
|
||||
chainId: SupportedChainId.MAINNET,
|
||||
position,
|
||||
pool,
|
||||
details,
|
||||
inRange: true,
|
||||
closed: false,
|
||||
},
|
||||
],
|
||||
loading: false,
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
mockUseMultiChainPositions.mockReturnValue(useMultiChainPositionsReturnValue)
|
||||
})
|
||||
test('Pools should render LP positions', () => {
|
||||
const props = { account: owner }
|
||||
const { container } = render(<Pools {...props} />)
|
||||
expect(container).not.toBeEmptyDOMElement()
|
||||
})
|
||||
@@ -4,9 +4,9 @@ import { BrowserEvent, InterfaceElementName, SharedEventName } from '@uniswap/an
|
||||
import { formatNumber, NumberType } from '@uniswap/conedison/format'
|
||||
import { Position } from '@uniswap/v3-sdk'
|
||||
import { useWeb3React } from '@web3-react/core'
|
||||
import { useToggleAccountDrawer } from 'components/AccountDrawer'
|
||||
import Row from 'components/Row'
|
||||
import { MouseoverTooltip } from 'components/Tooltip'
|
||||
import { useToggleWalletDrawer } from 'components/WalletDropdown'
|
||||
import { EmptyWalletModule } from 'nft/components/profile/view/EmptyWalletContent'
|
||||
import { useCallback, useMemo, useReducer } from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
@@ -33,7 +33,7 @@ export default function Pools({ account }: { account: string }) {
|
||||
return [openPositions, closedPositions]
|
||||
}, [positions])
|
||||
|
||||
const toggleWalletDrawer = useToggleWalletDrawer()
|
||||
const toggleWalletDrawer = useToggleAccountDrawer()
|
||||
|
||||
if (!positions || loading) {
|
||||
return <PortfolioSkeleton />
|
||||
@@ -93,7 +93,7 @@ function PositionListItem({ positionInfo }: { positionInfo: PositionInfo }) {
|
||||
const liquidityValue = calculcateLiquidityValue(priceA, priceB, position)
|
||||
|
||||
const navigate = useNavigate()
|
||||
const toggleWalletDrawer = useToggleWalletDrawer()
|
||||
const toggleWalletDrawer = useToggleAccountDrawer()
|
||||
const { chainId: walletChainId, connector } = useWeb3React()
|
||||
const onClick = useCallback(async () => {
|
||||
if (walletChainId !== chainId) await switchChain(connector, chainId)
|
||||
@@ -111,15 +111,9 @@ function PositionListItem({ positionInfo }: { positionInfo: PositionInfo }) {
|
||||
[chainId, pool.token0.address, pool.token0.symbol, pool.token1.address, pool.token1.symbol]
|
||||
)
|
||||
|
||||
const containsURL = useMemo(
|
||||
() =>
|
||||
[pool.token0.name, pool.token0.symbol, pool.token1.name, pool.token1.symbol].some((testString) =>
|
||||
hasURL(testString)
|
||||
),
|
||||
[pool]
|
||||
)
|
||||
const shouldHidePosition = hasURL(pool.token0.symbol) || hasURL(pool.token1.symbol)
|
||||
|
||||
if (containsURL) {
|
||||
if (shouldHidePosition) {
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ import { useNavigate } from 'react-router-dom'
|
||||
import styled from 'styled-components/macro'
|
||||
import { EllipsisStyle, ThemedText } from 'theme'
|
||||
|
||||
import { useToggleWalletDrawer } from '..'
|
||||
import { useToggleAccountDrawer } from '..'
|
||||
import { PortfolioArrow } from '../AuthenticatedHeader'
|
||||
import { hideSmallBalancesAtom } from '../SmallBalanceToggle'
|
||||
import { ExpandoRow } from './ExpandoRow'
|
||||
@@ -26,7 +26,7 @@ function meetsThreshold(tokenBalance: TokenBalance, hideSmallBalances: boolean)
|
||||
}
|
||||
|
||||
export default function Tokens({ account }: { account: string }) {
|
||||
const toggleWalletDrawer = useToggleWalletDrawer()
|
||||
const toggleWalletDrawer = useToggleAccountDrawer()
|
||||
const hideSmallBalances = useAtomValue(hideSmallBalancesAtom)
|
||||
const [showHiddenTokens, setShowHiddenTokens] = useState(false)
|
||||
|
||||
@@ -96,7 +96,7 @@ function TokenRow({ token, quantity, denominatedValue, tokenProjectMarket }: Tok
|
||||
const percentChange = tokenProjectMarket?.pricePercentChange?.value ?? 0
|
||||
|
||||
const navigate = useNavigate()
|
||||
const toggleWalletDrawer = useToggleWalletDrawer()
|
||||
const toggleWalletDrawer = useToggleAccountDrawer()
|
||||
const navigateToTokenDetails = useCallback(async () => {
|
||||
navigate(getTokenDetailsURL(token))
|
||||
toggleWalletDrawer()
|
||||
@@ -5,7 +5,7 @@ import { PropsWithChildren, useCallback, useEffect, useMemo, useState } from 're
|
||||
import { useAllTransactions } from 'state/transactions/hooks'
|
||||
import { TransactionDetails } from 'state/transactions/types'
|
||||
|
||||
import { useWalletDrawer } from '.'
|
||||
import { useAccountDrawer } from '.'
|
||||
|
||||
const isTxPending = (tx: TransactionDetails) => !tx.receipt
|
||||
function wasPending(previousTxs: { [hash: string]: TransactionDetails | undefined }, current: TransactionDetails) {
|
||||
@@ -39,7 +39,7 @@ function useHasUpdatedTx() {
|
||||
export default function PrefetchBalancesWrapper({ children }: PropsWithChildren) {
|
||||
const { account } = useWeb3React()
|
||||
const [prefetchPortfolioBalances] = usePortfolioBalancesLazyQuery()
|
||||
const [drawerOpen] = useWalletDrawer()
|
||||
const [drawerOpen] = useAccountDrawer()
|
||||
|
||||
const [hasUnfetchedBalances, setHasUnfetchedBalances] = useState(true)
|
||||
const fetchBalances = useCallback(() => {
|
||||
@@ -18,18 +18,18 @@ const DRAWER_MARGIN = '8px'
|
||||
const DRAWER_OFFSET = '10px'
|
||||
const DRAWER_TOP_MARGIN_MOBILE_WEB = '72px'
|
||||
|
||||
const walletDrawerOpenAtom = atom(false)
|
||||
const accountDrawerOpenAtom = atom(false)
|
||||
|
||||
export function useToggleWalletDrawer() {
|
||||
const updateWalletDrawerOpen = useUpdateAtom(walletDrawerOpenAtom)
|
||||
export function useToggleAccountDrawer() {
|
||||
const updateAccountDrawerOpen = useUpdateAtom(accountDrawerOpenAtom)
|
||||
return useCallback(() => {
|
||||
updateWalletDrawerOpen((open) => !open)
|
||||
}, [updateWalletDrawerOpen])
|
||||
updateAccountDrawerOpen((open) => !open)
|
||||
}, [updateAccountDrawerOpen])
|
||||
}
|
||||
|
||||
export function useWalletDrawer(): [boolean, () => void] {
|
||||
const walletDrawerOpen = useAtomValue(walletDrawerOpenAtom)
|
||||
return [walletDrawerOpen, useToggleWalletDrawer()]
|
||||
export function useAccountDrawer(): [boolean, () => void] {
|
||||
const accountDrawerOpen = useAtomValue(accountDrawerOpenAtom)
|
||||
return [accountDrawerOpen, useToggleAccountDrawer()]
|
||||
}
|
||||
|
||||
const ScrimBackground = styled.div<{ open: boolean }>`
|
||||
@@ -63,7 +63,7 @@ const Scrim = ({ onClick, open }: { onClick: () => void; open: boolean }) => {
|
||||
return <ScrimBackground onClick={onClick} open={open} />
|
||||
}
|
||||
|
||||
const WalletDropdownScrollWrapper = styled.div`
|
||||
const AccountDrawerScrollWrapper = styled.div`
|
||||
overflow: hidden;
|
||||
&:hover {
|
||||
overflow-y: auto;
|
||||
@@ -76,32 +76,45 @@ const WalletDropdownScrollWrapper = styled.div`
|
||||
border-radius: 12px;
|
||||
`
|
||||
|
||||
const WalletDropdownWrapper = styled.div<{ open: boolean }>`
|
||||
const Container = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
height: calc(100% - 2 * ${DRAWER_MARGIN});
|
||||
overflow: hidden;
|
||||
position: fixed;
|
||||
right: ${DRAWER_MARGIN};
|
||||
top: ${DRAWER_MARGIN};
|
||||
right: ${({ open }) => (open ? DRAWER_MARGIN : '-' + DRAWER_WIDTH)};
|
||||
z-index: ${Z_INDEX.fixed};
|
||||
|
||||
overflow: hidden;
|
||||
@media only screen and (max-width: ${({ theme }) => `${theme.breakpoint.sm}px`}) {
|
||||
top: 100%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
width: 100%;
|
||||
overflow: visible;
|
||||
}
|
||||
`
|
||||
|
||||
height: calc(100% - 2 * ${DRAWER_MARGIN});
|
||||
const AccountDrawerWrapper = styled.div<{ open: boolean }>`
|
||||
margin-right: ${({ open }) => (open ? 0 : '-' + DRAWER_WIDTH)};
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
|
||||
@media only screen and (max-width: ${({ theme }) => `${theme.breakpoint.sm}px`}) {
|
||||
z-index: ${Z_INDEX.modal};
|
||||
top: unset;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: ${({ open }) => (open ? 0 : `calc(-1 * (100% - ${DRAWER_TOP_MARGIN_MOBILE_WEB}))`)};
|
||||
position: absolute;
|
||||
margin-right: 0;
|
||||
top: ${({ open }) => (open ? `calc(-1 * (100% - ${DRAWER_TOP_MARGIN_MOBILE_WEB}))` : 0)};
|
||||
|
||||
width: 100%;
|
||||
height: calc(100% - ${DRAWER_TOP_MARGIN_MOBILE_WEB});
|
||||
border-bottom-right-radius: 0px;
|
||||
border-bottom-left-radius: 0px;
|
||||
box-shadow: unset;
|
||||
transition: top ${({ theme }) => theme.transition.duration.medium};
|
||||
}
|
||||
|
||||
@media screen and (min-width: 1440px) {
|
||||
right: ${({ open }) => (open ? DRAWER_MARGIN : '-' + DRAWER_WIDTH_XL)};
|
||||
margin-right: ${({ open }) => (open ? 0 : `-${DRAWER_WIDTH_XL}`)};
|
||||
width: ${DRAWER_WIDTH_XL};
|
||||
}
|
||||
|
||||
@@ -112,8 +125,7 @@ const WalletDropdownWrapper = styled.div<{ open: boolean }>`
|
||||
border: ${({ theme }) => `1px solid ${theme.backgroundOutline}`};
|
||||
|
||||
box-shadow: ${({ theme }) => theme.deepShadow};
|
||||
transition: right ${({ theme }) => theme.transition.duration.medium},
|
||||
bottom ${({ theme }) => theme.transition.duration.medium};
|
||||
transition: margin-right ${({ theme }) => theme.transition.duration.medium};
|
||||
`
|
||||
|
||||
const CloseIcon = styled(ChevronsRight).attrs({ size: 24 })`
|
||||
@@ -123,30 +135,24 @@ const CloseIcon = styled(ChevronsRight).attrs({ size: 24 })`
|
||||
const CloseDrawer = styled.div`
|
||||
${ClickableStyle}
|
||||
cursor: pointer;
|
||||
height: calc(100% - 2 * ${DRAWER_MARGIN});
|
||||
position: fixed;
|
||||
right: calc(${DRAWER_MARGIN} + ${DRAWER_WIDTH} - ${DRAWER_OFFSET});
|
||||
top: 4px;
|
||||
z-index: ${Z_INDEX.dropdown};
|
||||
height: 100%;
|
||||
// When the drawer is not hovered, the icon should be 18px from the edge of the sidebar.
|
||||
padding: 24px calc(18px + ${DRAWER_OFFSET}) 24px 14px;
|
||||
border-radius: 20px 0 0 20px;
|
||||
transition: ${({ theme }) =>
|
||||
`${theme.transition.duration.medium} ${theme.transition.timing.ease} background-color, ${theme.transition.duration.medium} ${theme.transition.timing.ease} margin`};
|
||||
&:hover {
|
||||
margin: 0 -4px 0 0;
|
||||
z-index: -1;
|
||||
margin: 0 -8px 0 0;
|
||||
background-color: ${({ theme }) => theme.stateOverlayHover};
|
||||
}
|
||||
@media only screen and (max-width: ${({ theme }) => `${theme.breakpoint.sm}px`}) {
|
||||
display: none;
|
||||
}
|
||||
@media screen and (min-width: 1440px) {
|
||||
right: calc(${DRAWER_MARGIN} + ${DRAWER_WIDTH_XL} - ${DRAWER_OFFSET});
|
||||
}
|
||||
`
|
||||
|
||||
function WalletDropdown() {
|
||||
const [walletDrawerOpen, toggleWalletDrawer] = useWalletDrawer()
|
||||
function AccountDrawer() {
|
||||
const [walletDrawerOpen, toggleWalletDrawer] = useAccountDrawer()
|
||||
const scrollRef = useRef<HTMLDivElement>(null)
|
||||
useEffect(() => {
|
||||
if (!walletDrawerOpen) {
|
||||
@@ -187,7 +193,7 @@ function WalletDropdown() {
|
||||
}, [walletDrawerOpen, toggleWalletDrawer])
|
||||
|
||||
return (
|
||||
<>
|
||||
<Container>
|
||||
{walletDrawerOpen && (
|
||||
<TraceEvent
|
||||
events={[BrowserEvent.onClick]}
|
||||
@@ -200,14 +206,14 @@ function WalletDropdown() {
|
||||
</TraceEvent>
|
||||
)}
|
||||
<Scrim onClick={toggleWalletDrawer} open={walletDrawerOpen} />
|
||||
<WalletDropdownWrapper open={walletDrawerOpen}>
|
||||
<AccountDrawerWrapper open={walletDrawerOpen}>
|
||||
{/* id used for child InfiniteScrolls to reference when it has reached the bottom of the component */}
|
||||
<WalletDropdownScrollWrapper ref={scrollRef} id="wallet-dropdown-scroll-wrapper">
|
||||
<AccountDrawerScrollWrapper ref={scrollRef} id="wallet-dropdown-scroll-wrapper">
|
||||
<DefaultMenu />
|
||||
</WalletDropdownScrollWrapper>
|
||||
</WalletDropdownWrapper>
|
||||
</>
|
||||
</AccountDrawerScrollWrapper>
|
||||
</AccountDrawerWrapper>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
export default WalletDropdown
|
||||
export default AccountDrawer
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import { useAccountDrawer } from 'components/AccountDrawer'
|
||||
import { DownloadButton, LearnMoreButton } from 'components/AccountDrawer/DownloadButton'
|
||||
import { AutoColumn } from 'components/Column'
|
||||
import Row, { RowBetween } from 'components/Row'
|
||||
import { useWalletDrawer } from 'components/WalletDropdown'
|
||||
import { DownloadButton, LearnMoreButton } from 'components/WalletDropdown/DownloadButton'
|
||||
import { X } from 'react-feather'
|
||||
import { useLocation } from 'react-router-dom'
|
||||
import { useHideUniswapWalletBanner } from 'state/user/hooks'
|
||||
@@ -66,7 +66,7 @@ const StyledXButton = styled(X)`
|
||||
|
||||
export default function UniswapWalletBanner() {
|
||||
const [hideUniswapWalletBanner, toggleHideUniswapWalletBanner] = useHideUniswapWalletBanner()
|
||||
const [walletDrawerOpen] = useWalletDrawer()
|
||||
const [walletDrawerOpen] = useAccountDrawer()
|
||||
|
||||
const theme = useTheme()
|
||||
|
||||
|
||||
@@ -1,13 +1,10 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import * as Sentry from '@sentry/react'
|
||||
import { sendAnalyticsEvent } from '@uniswap/analytics'
|
||||
import { SwapEventName } from '@uniswap/analytics-events'
|
||||
import { ButtonLight, SmallButtonPrimary } from 'components/Button'
|
||||
import { ChevronUpIcon } from 'nft/components/icons'
|
||||
import { useIsMobile } from 'nft/hooks'
|
||||
import React, { PropsWithChildren, useState } from 'react'
|
||||
import { Copy } from 'react-feather'
|
||||
import { useLocation } from 'react-router-dom'
|
||||
import styled from 'styled-components/macro'
|
||||
import { isSentryEnabled } from 'utils/env'
|
||||
|
||||
@@ -220,18 +217,14 @@ const updateServiceWorkerInBackground = async () => {
|
||||
}
|
||||
|
||||
export default function ErrorBoundary({ children }: PropsWithChildren): JSX.Element {
|
||||
const { pathname } = useLocation()
|
||||
return (
|
||||
<Sentry.ErrorBoundary
|
||||
fallback={({ error, eventId }) => <Fallback error={error} eventId={eventId} />}
|
||||
beforeCapture={(scope) => {
|
||||
scope.setLevel('fatal')
|
||||
}}
|
||||
onError={(error) => {
|
||||
onError={() => {
|
||||
updateServiceWorkerInBackground()
|
||||
if (pathname === '/swap') {
|
||||
sendAnalyticsEvent(SwapEventName.SWAP_ERROR, { error })
|
||||
}
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
|
||||
24
src/components/Identicon/StatusIcon.test.tsx
Normal file
24
src/components/Identicon/StatusIcon.test.tsx
Normal file
@@ -0,0 +1,24 @@
|
||||
import { getConnections } from 'connection'
|
||||
import { render } from 'test-utils'
|
||||
|
||||
import StatusIcon from './StatusIcon'
|
||||
|
||||
jest.mock('../../hooks/useSocksBalance', () => ({
|
||||
useHasSocks: () => true,
|
||||
}))
|
||||
|
||||
describe('StatusIcon', () => {
|
||||
it('renders children in correct order, with no account and with socks', () => {
|
||||
const supportedConnections = getConnections()
|
||||
const injectedConnection = supportedConnections[1]
|
||||
const component = render(<StatusIcon connection={injectedConnection} />)
|
||||
expect(component.getByTestId('StatusIconRoot')).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it('renders with no account and showMiniIcons=false', () => {
|
||||
const supportedConnections = getConnections()
|
||||
const injectedConnection = supportedConnections[1]
|
||||
const component = render(<StatusIcon connection={injectedConnection} showMiniIcons={false} />)
|
||||
expect(component.getByTestId('StatusIconRoot').children.length).toEqual(0)
|
||||
})
|
||||
})
|
||||
@@ -90,10 +90,10 @@ export default function StatusIcon({
|
||||
const hasSocks = useHasSocks()
|
||||
|
||||
return (
|
||||
<IconWrapper size={size}>
|
||||
{hasSocks && showMiniIcons && <Socks />}
|
||||
<IconWrapper size={size} data-testid="StatusIconRoot">
|
||||
<MainWalletIcon connection={connection} size={size} />
|
||||
{showMiniIcons && <MiniWalletIcon connection={connection} side="right" />}
|
||||
{hasSocks && showMiniIcons && <Socks />}
|
||||
</IconWrapper>
|
||||
)
|
||||
}
|
||||
|
||||
129
src/components/Identicon/__snapshots__/StatusIcon.test.tsx.snap
Normal file
129
src/components/Identicon/__snapshots__/StatusIcon.test.tsx.snap
Normal file
@@ -0,0 +1,129 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`StatusIcon renders children in correct order, with no account and with socks 1`] = `
|
||||
.c0 {
|
||||
position: relative;
|
||||
display: -webkit-box;
|
||||
display: -webkit-flex;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
-webkit-flex-flow: column nowrap;
|
||||
-ms-flex-flow: column nowrap;
|
||||
flex-flow: column nowrap;
|
||||
-webkit-align-items: center;
|
||||
-webkit-box-align: center;
|
||||
-ms-flex-align: center;
|
||||
align-items: center;
|
||||
-webkit-box-pack: center;
|
||||
-webkit-justify-content: center;
|
||||
-ms-flex-pack: center;
|
||||
justify-content: center;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.c0 > img,
|
||||
.c0 span {
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
}
|
||||
|
||||
.c1 {
|
||||
position: absolute;
|
||||
display: -webkit-box;
|
||||
display: -webkit-flex;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
-webkit-box-pack: center;
|
||||
-webkit-justify-content: center;
|
||||
-ms-flex-pack: center;
|
||||
justify-content: center;
|
||||
-webkit-align-items: center;
|
||||
-webkit-box-align: center;
|
||||
-ms-flex-align: center;
|
||||
align-items: center;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
bottom: -4px;
|
||||
right: -4px;
|
||||
border-radius: 50%;
|
||||
outline: 2px solid #FFFFFF;
|
||||
outline-offset: -0.1px;
|
||||
background-color: #FFFFFF;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.c3 {
|
||||
position: absolute;
|
||||
display: -webkit-box;
|
||||
display: -webkit-flex;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
-webkit-box-pack: center;
|
||||
-webkit-justify-content: center;
|
||||
-ms-flex-pack: center;
|
||||
justify-content: center;
|
||||
-webkit-align-items: center;
|
||||
-webkit-box-align: center;
|
||||
-ms-flex-align: center;
|
||||
align-items: center;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
bottom: -4px;
|
||||
left: -4px;
|
||||
border-radius: 50%;
|
||||
outline: 2px solid #FFFFFF;
|
||||
outline-offset: -0.1px;
|
||||
background-color: #FFFFFF;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.c2 {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
@media (max-width:960px) {
|
||||
.c0 {
|
||||
-webkit-align-items: flex-end;
|
||||
-webkit-box-align: flex-end;
|
||||
-ms-flex-align: flex-end;
|
||||
align-items: flex-end;
|
||||
}
|
||||
}
|
||||
|
||||
@supports (overflow:clip) {
|
||||
.c1 {
|
||||
overflow: clip;
|
||||
}
|
||||
}
|
||||
|
||||
@supports (overflow:clip) {
|
||||
.c3 {
|
||||
overflow: clip;
|
||||
}
|
||||
}
|
||||
|
||||
<div
|
||||
class="c0"
|
||||
data-testid="StatusIconRoot"
|
||||
size="16"
|
||||
>
|
||||
<div
|
||||
class="c1"
|
||||
>
|
||||
<img
|
||||
alt="MetaMask icon"
|
||||
class="c2"
|
||||
src="metamask.svg"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="c3"
|
||||
>
|
||||
<img
|
||||
class="c2"
|
||||
src="socks.svg"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -1,8 +1,8 @@
|
||||
import { useWeb3React } from '@web3-react/core'
|
||||
import { parseLocalActivity } from 'components/AccountDrawer/MiniPortfolio/Activity/parseLocal'
|
||||
import { PortfolioLogo } from 'components/AccountDrawer/MiniPortfolio/PortfolioLogo'
|
||||
import PortfolioRow from 'components/AccountDrawer/MiniPortfolio/PortfolioRow'
|
||||
import Column from 'components/Column'
|
||||
import { parseLocalActivity } from 'components/WalletDropdown/MiniPortfolio/Activity/parseLocal'
|
||||
import { PortfolioLogo } from 'components/WalletDropdown/MiniPortfolio/PortfolioLogo'
|
||||
import PortfolioRow from 'components/WalletDropdown/MiniPortfolio/PortfolioRow'
|
||||
import useENSName from 'hooks/useENSName'
|
||||
import { useCombinedActiveList } from 'state/lists/hooks'
|
||||
import { useTransaction } from 'state/transactions/hooks'
|
||||
|
||||
@@ -1,38 +1,91 @@
|
||||
import { BigNumber } from '@ethersproject/bignumber'
|
||||
import { render, screen } from 'test-utils'
|
||||
import { SupportedChainId, Token, WETH9 } from '@uniswap/sdk-core'
|
||||
import { FeeAmount, Pool } from '@uniswap/v3-sdk'
|
||||
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 { unwrappedToken } from 'utils/unwrappedToken'
|
||||
|
||||
import PositionListItem from '.'
|
||||
|
||||
jest.mock('hooks/Tokens', () => {
|
||||
const originalModule = jest.requireActual('hooks/Tokens')
|
||||
const uniSDK = jest.requireActual('@uniswap/sdk-core')
|
||||
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 />)
|
||||
|
||||
jest.mock('@web3-react/core', () => {
|
||||
const web3React = jest.requireActual('@web3-react/core')
|
||||
return {
|
||||
__esModule: true,
|
||||
...originalModule,
|
||||
useToken: jest.fn(
|
||||
() =>
|
||||
new uniSDK.Token(
|
||||
1,
|
||||
'0x39AA39c021dfbaE8faC545936693aC917d5E7563',
|
||||
8,
|
||||
'https://www.example.com',
|
||||
'example.com coin'
|
||||
)
|
||||
),
|
||||
...web3React,
|
||||
useWeb3React: () => ({
|
||||
chainId: 1,
|
||||
}),
|
||||
}
|
||||
})
|
||||
|
||||
test('PositionListItem should not render when the name contains a url', () => {
|
||||
const susToken0Address = '0x39AA39c021dfbaE8faC545936693aC917d5E7563'
|
||||
|
||||
beforeEach(() => {
|
||||
const susToken0 = new Token(1, susToken0Address, 8, 'https://www.example.com', 'example.com coin')
|
||||
mockUseToken.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([
|
||||
PoolState.EXISTS,
|
||||
new Pool(susToken0, USDC_MAINNET, FeeAmount.HIGH, '2437312313659959819381354528', '10272714736694327408', -69633),
|
||||
])
|
||||
mockUnwrappedToken.mockReturnValue(susToken0)
|
||||
})
|
||||
|
||||
test('PositionListItem should not render when token0 symbol contains a url', () => {
|
||||
const positionDetails = {
|
||||
token0: '0x39AA39c021dfbaE8faC545936693aC917d5E7563',
|
||||
token1: '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2',
|
||||
token0: susToken0Address,
|
||||
token1: USDC_MAINNET.address,
|
||||
tokenId: BigNumber.from(436148),
|
||||
fee: 100,
|
||||
liquidity: BigNumber.from('0x5c985aff8059be04'),
|
||||
tickLower: -800,
|
||||
tickUpper: 1600,
|
||||
}
|
||||
render(<PositionListItem {...positionDetails} />)
|
||||
screen.debug()
|
||||
expect(screen.queryByText('.com', { exact: false })).toBe(null)
|
||||
const { container } = render(<PositionListItem {...positionDetails} />)
|
||||
expect(container).toBeEmptyDOMElement()
|
||||
})
|
||||
|
||||
test('PositionListItem should not render when token1 symbol contains a url', () => {
|
||||
const positionDetails = {
|
||||
token0: USDC_MAINNET.address,
|
||||
token1: susToken0Address,
|
||||
tokenId: BigNumber.from(436148),
|
||||
fee: 100,
|
||||
liquidity: BigNumber.from('0x5c985aff8059be04'),
|
||||
tickLower: -800,
|
||||
tickUpper: 1600,
|
||||
}
|
||||
const { container } = render(<PositionListItem {...positionDetails} />)
|
||||
expect(container).toBeEmptyDOMElement()
|
||||
})
|
||||
|
||||
test('PositionListItem should render a position', () => {
|
||||
const positionDetails = {
|
||||
token0: USDC_MAINNET.address,
|
||||
token1: WETH9[SupportedChainId.MAINNET].address,
|
||||
tokenId: BigNumber.from(436148),
|
||||
fee: 100,
|
||||
liquidity: BigNumber.from('0x5c985aff8059be04'),
|
||||
tickLower: -800,
|
||||
tickUpper: 1600,
|
||||
}
|
||||
const { container } = render(<PositionListItem {...positionDetails} />)
|
||||
expect(container).not.toBeEmptyDOMElement()
|
||||
})
|
||||
|
||||
@@ -203,12 +203,9 @@ export default function PositionListItem({
|
||||
|
||||
const removed = liquidity?.eq(0)
|
||||
|
||||
const containsURL = useMemo(
|
||||
() => [token0?.name, token0?.symbol, token1?.name, token1?.symbol].some((testString) => hasURL(testString)),
|
||||
[token0?.name, token0?.symbol, token1?.name, token1?.symbol]
|
||||
)
|
||||
const shouldHidePosition = hasURL(token0?.symbol) || hasURL(token1?.symbol)
|
||||
|
||||
if (containsURL) {
|
||||
if (shouldHidePosition) {
|
||||
return null
|
||||
}
|
||||
|
||||
|
||||
@@ -1,425 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`renders currency rows correctly when currencies list is non-empty 1`] = `
|
||||
<DocumentFragment>
|
||||
.c0 {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.c1 {
|
||||
width: 100%;
|
||||
display: -webkit-box;
|
||||
display: -webkit-flex;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
padding: 0;
|
||||
-webkit-align-items: center;
|
||||
-webkit-box-align: center;
|
||||
-ms-flex-align: center;
|
||||
align-items: center;
|
||||
-webkit-box-pack: start;
|
||||
-webkit-justify-content: flex-start;
|
||||
-ms-flex-pack: start;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.c2 {
|
||||
-webkit-box-pack: justify;
|
||||
-webkit-justify-content: space-between;
|
||||
-ms-flex-pack: justify;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.c11 {
|
||||
width: -webkit-fit-content;
|
||||
width: -moz-fit-content;
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
.c10 {
|
||||
color: #98A1C0;
|
||||
}
|
||||
|
||||
.c8 {
|
||||
margin-left: 4px;
|
||||
display: -webkit-box;
|
||||
display: -webkit-flex;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
-webkit-box-pack: center;
|
||||
-webkit-justify-content: center;
|
||||
-ms-flex-pack: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.c9 {
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
color: #98A1C0;
|
||||
}
|
||||
|
||||
.c4 {
|
||||
display: -webkit-box;
|
||||
display: -webkit-flex;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
-webkit-flex-direction: column;
|
||||
-ms-flex-direction: column;
|
||||
flex-direction: column;
|
||||
-webkit-box-pack: start;
|
||||
-webkit-justify-content: flex-start;
|
||||
-ms-flex-pack: start;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.c5 {
|
||||
display: grid;
|
||||
grid-auto-rows: auto;
|
||||
}
|
||||
|
||||
.c3 {
|
||||
padding: 4px 20px;
|
||||
height: 56px;
|
||||
display: grid;
|
||||
grid-template-columns: auto minmax(auto,1fr) auto minmax(0,72px);
|
||||
grid-gap: 16px;
|
||||
cursor: pointer;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.c3:hover {
|
||||
background-color: #98A1C014;
|
||||
}
|
||||
|
||||
.c6 {
|
||||
font-weight: 500;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.c7 {
|
||||
margin-left: 0.3em;
|
||||
}
|
||||
|
||||
<div
|
||||
style="padding-right: 4px;"
|
||||
>
|
||||
<div
|
||||
class="CurrencyList_scrollbarStyle__1pi21y70"
|
||||
style="position: relative; height: 10px; width: 100%; overflow: auto; will-change: transform; direction: ltr;"
|
||||
>
|
||||
<div
|
||||
style="height: 168px; width: 100%;"
|
||||
>
|
||||
<div
|
||||
class="c0 c1 c2 c3 token-item-0x6B175474E89094C44Da98b954EedeAC495271d0F"
|
||||
style="position: absolute; left: 0px; top: 0px; height: 56px; width: 100%;"
|
||||
tabindex="0"
|
||||
>
|
||||
<div
|
||||
class="c4"
|
||||
>
|
||||
CurrencyLogo currency=DAI
|
||||
</div>
|
||||
<div
|
||||
class="c5"
|
||||
style="opacity: 1;"
|
||||
>
|
||||
<div
|
||||
class="c0 c1"
|
||||
>
|
||||
<div
|
||||
class="c6 css-vurnku"
|
||||
title="Dai Stablecoin"
|
||||
>
|
||||
Dai Stablecoin
|
||||
</div>
|
||||
<div
|
||||
class="c7"
|
||||
>
|
||||
<div
|
||||
class="c8"
|
||||
>
|
||||
<svg
|
||||
class="c9"
|
||||
fill="none"
|
||||
height="24"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
viewBox="0 0 24 24"
|
||||
width="24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"
|
||||
/>
|
||||
<line
|
||||
x1="12"
|
||||
x2="12"
|
||||
y1="9"
|
||||
y2="13"
|
||||
/>
|
||||
<line
|
||||
x1="12"
|
||||
x2="12.01"
|
||||
y1="17"
|
||||
y2="17"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="c10 css-yfjwjl"
|
||||
>
|
||||
DAI
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="c4"
|
||||
>
|
||||
<div
|
||||
class="c0 c1 c11"
|
||||
style="justify-self: flex-end;"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="c0 c1 c2 c3 token-item-0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"
|
||||
style="position: absolute; left: 0px; top: 56px; height: 56px; width: 100%;"
|
||||
tabindex="0"
|
||||
>
|
||||
<div
|
||||
class="c4"
|
||||
>
|
||||
CurrencyLogo currency=USDC
|
||||
</div>
|
||||
<div
|
||||
class="c5"
|
||||
style="opacity: 1;"
|
||||
>
|
||||
<div
|
||||
class="c0 c1"
|
||||
>
|
||||
<div
|
||||
class="c6 css-vurnku"
|
||||
title="USD//C"
|
||||
>
|
||||
USD//C
|
||||
</div>
|
||||
<div
|
||||
class="c7"
|
||||
>
|
||||
<div
|
||||
class="c8"
|
||||
>
|
||||
<svg
|
||||
class="c9"
|
||||
fill="none"
|
||||
height="24"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
viewBox="0 0 24 24"
|
||||
width="24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"
|
||||
/>
|
||||
<line
|
||||
x1="12"
|
||||
x2="12"
|
||||
y1="9"
|
||||
y2="13"
|
||||
/>
|
||||
<line
|
||||
x1="12"
|
||||
x2="12.01"
|
||||
y1="17"
|
||||
y2="17"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="c10 css-yfjwjl"
|
||||
>
|
||||
USDC
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="c4"
|
||||
>
|
||||
<div
|
||||
class="c0 c1 c11"
|
||||
style="justify-self: flex-end;"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="c0 c1 c2 c3 token-item-0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599"
|
||||
style="position: absolute; left: 0px; top: 112px; height: 56px; width: 100%;"
|
||||
tabindex="0"
|
||||
>
|
||||
<div
|
||||
class="c4"
|
||||
>
|
||||
CurrencyLogo currency=WBTC
|
||||
</div>
|
||||
<div
|
||||
class="c5"
|
||||
style="opacity: 1;"
|
||||
>
|
||||
<div
|
||||
class="c0 c1"
|
||||
>
|
||||
<div
|
||||
class="c6 css-vurnku"
|
||||
title="Wrapped BTC"
|
||||
>
|
||||
Wrapped BTC
|
||||
</div>
|
||||
<div
|
||||
class="c7"
|
||||
>
|
||||
<div
|
||||
class="c8"
|
||||
>
|
||||
<svg
|
||||
class="c9"
|
||||
fill="none"
|
||||
height="24"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
viewBox="0 0 24 24"
|
||||
width="24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"
|
||||
/>
|
||||
<line
|
||||
x1="12"
|
||||
x2="12"
|
||||
y1="9"
|
||||
y2="13"
|
||||
/>
|
||||
<line
|
||||
x1="12"
|
||||
x2="12.01"
|
||||
y1="17"
|
||||
y2="17"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="c10 css-yfjwjl"
|
||||
>
|
||||
WBTC
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="c4"
|
||||
>
|
||||
<div
|
||||
class="c0 c1 c11"
|
||||
style="justify-self: flex-end;"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
||||
exports[`renders loading rows when isLoading is true 1`] = `
|
||||
<DocumentFragment>
|
||||
.c0 {
|
||||
display: grid;
|
||||
}
|
||||
|
||||
.c0 > div {
|
||||
-webkit-animation: fAQEyV 1.5s infinite;
|
||||
animation: fAQEyV 1.5s infinite;
|
||||
-webkit-animation-fill-mode: both;
|
||||
animation-fill-mode: both;
|
||||
background: linear-gradient( to left,#F5F6FC 25%,#E8ECFB 50%,#F5F6FC 75% );
|
||||
background-size: 400%;
|
||||
border-radius: 12px;
|
||||
height: 2.4em;
|
||||
will-change: background-position;
|
||||
}
|
||||
|
||||
.c1 {
|
||||
grid-column-gap: 0.5em;
|
||||
grid-template-columns: repeat(12,1fr);
|
||||
max-width: 960px;
|
||||
padding: 12px 20px;
|
||||
}
|
||||
|
||||
.c1 > div:nth-child(4n + 1) {
|
||||
grid-column: 1 / 8;
|
||||
height: 1em;
|
||||
margin-bottom: 0.25em;
|
||||
}
|
||||
|
||||
.c1 > div:nth-child(4n + 2) {
|
||||
grid-column: 12;
|
||||
height: 1em;
|
||||
margin-top: 0.25em;
|
||||
}
|
||||
|
||||
.c1 > div:nth-child(4n + 3) {
|
||||
grid-column: 1 / 4;
|
||||
height: 0.75em;
|
||||
}
|
||||
|
||||
<div
|
||||
style="padding-right: 4px;"
|
||||
>
|
||||
<div
|
||||
class="CurrencyList_scrollbarStyle__1pi21y70"
|
||||
style="position: relative; height: 10px; width: 100%; overflow: auto; will-change: transform; direction: ltr;"
|
||||
>
|
||||
<div
|
||||
style="height: 560px; width: 100%;"
|
||||
>
|
||||
<div
|
||||
class="c0 c1"
|
||||
>
|
||||
<div />
|
||||
<div />
|
||||
<div />
|
||||
</div>
|
||||
<div
|
||||
class="c0 c1"
|
||||
>
|
||||
<div />
|
||||
<div />
|
||||
<div />
|
||||
</div>
|
||||
<div
|
||||
class="c0 c1"
|
||||
>
|
||||
<div />
|
||||
<div />
|
||||
<div />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
@@ -1,3 +1,4 @@
|
||||
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'
|
||||
@@ -25,11 +26,11 @@ jest.mock(
|
||||
jest.mock('@web3-react/core', () => {
|
||||
const web3React = jest.requireActual('@web3-react/core')
|
||||
return {
|
||||
...web3React,
|
||||
useWeb3React: () => ({
|
||||
account: '123',
|
||||
isActive: true,
|
||||
}),
|
||||
...web3React,
|
||||
}
|
||||
})
|
||||
|
||||
@@ -42,37 +43,38 @@ jest.mock('../../../state/connection/hooks', () => {
|
||||
})
|
||||
|
||||
it('renders loading rows when isLoading is true', () => {
|
||||
const { asFragment } = render(
|
||||
const component = render(
|
||||
<CurrencyList
|
||||
height={10}
|
||||
currencies={[]}
|
||||
otherListTokens={[]}
|
||||
selectedCurrency={null}
|
||||
onCurrencySelect={noOp}
|
||||
showImportView={noOp}
|
||||
setImportToken={noOp}
|
||||
isLoading={true}
|
||||
searchQuery=""
|
||||
isAddressSearch=""
|
||||
/>
|
||||
)
|
||||
expect(asFragment()).toMatchSnapshot()
|
||||
expect(component.findByTestId('loading-rows')).toBeTruthy()
|
||||
expect(screen.queryByText('Wrapped BTC')).not.toBeInTheDocument()
|
||||
expect(screen.queryByText('DAI')).not.toBeInTheDocument()
|
||||
expect(screen.queryByText('USDC')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('renders currency rows correctly when currencies list is non-empty', () => {
|
||||
const { asFragment } = render(
|
||||
render(
|
||||
<CurrencyList
|
||||
height={10}
|
||||
currencies={[DAI, USDC_MAINNET, WBTC]}
|
||||
otherListTokens={[]}
|
||||
selectedCurrency={null}
|
||||
onCurrencySelect={noOp}
|
||||
showImportView={noOp}
|
||||
setImportToken={noOp}
|
||||
isLoading={false}
|
||||
searchQuery=""
|
||||
isAddressSearch=""
|
||||
/>
|
||||
)
|
||||
expect(asFragment()).toMatchSnapshot()
|
||||
expect(screen.getByText('Wrapped BTC')).toBeInTheDocument()
|
||||
expect(screen.getByText('DAI')).toBeInTheDocument()
|
||||
expect(screen.getByText('USDC')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
@@ -20,7 +20,7 @@ import CurrencyLogo from '../../Logo/CurrencyLogo'
|
||||
import Row, { RowFixed } from '../../Row'
|
||||
import { MouseoverTooltip } from '../../Tooltip'
|
||||
import { LoadingRows, MenuItem } from '../styleds'
|
||||
import * as styles from './index.css'
|
||||
import { scrollbarStyle } from './index.css'
|
||||
|
||||
function currencyKey(currency: Currency): string {
|
||||
return currency.isToken ? currency.address : 'ETHER'
|
||||
@@ -65,6 +65,10 @@ const WarningContainer = styled.div`
|
||||
margin-left: 0.3em;
|
||||
`
|
||||
|
||||
const ListWrapper = styled.div`
|
||||
padding-right: 0.25rem;
|
||||
`
|
||||
|
||||
function Balance({ balance }: { balance: CurrencyAmount<Currency> }) {
|
||||
return <StyledBalanceText title={balance.toExact()}>{balance.toSignificant(4)}</StyledBalanceText>
|
||||
}
|
||||
@@ -212,7 +216,7 @@ export const formatAnalyticsEventProperties = (
|
||||
})
|
||||
|
||||
const LoadingRow = () => (
|
||||
<LoadingRows>
|
||||
<LoadingRows data-testid="loading-rows">
|
||||
<div />
|
||||
<div />
|
||||
<div />
|
||||
@@ -290,10 +294,10 @@ export default function CurrencyList({
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div style={{ paddingRight: '4px' }}>
|
||||
<ListWrapper>
|
||||
{isLoading ? (
|
||||
<FixedSizeList
|
||||
className={styles.scrollbarStyle}
|
||||
className={scrollbarStyle}
|
||||
height={height}
|
||||
ref={fixedListRef as any}
|
||||
width="100%"
|
||||
@@ -305,7 +309,7 @@ export default function CurrencyList({
|
||||
</FixedSizeList>
|
||||
) : (
|
||||
<FixedSizeList
|
||||
className={styles.scrollbarStyle}
|
||||
className={scrollbarStyle}
|
||||
height={height}
|
||||
ref={fixedListRef as any}
|
||||
width="100%"
|
||||
@@ -317,6 +321,6 @@ export default function CurrencyList({
|
||||
{Row}
|
||||
</FixedSizeList>
|
||||
)}
|
||||
</div>
|
||||
</ListWrapper>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
import { transparentize } from 'polished'
|
||||
import { ReactNode, useCallback, useEffect, useState } from 'react'
|
||||
import { ReactNode, useEffect, useState } from 'react'
|
||||
import styled from 'styled-components/macro'
|
||||
|
||||
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;
|
||||
padding: 0.6rem 1rem;
|
||||
pointer-events: auto;
|
||||
|
||||
color: ${({ theme }) => theme.textPrimary};
|
||||
font-weight: 400;
|
||||
@@ -25,7 +29,6 @@ interface TooltipProps extends Omit<PopoverProps, 'content'> {
|
||||
text: ReactNode
|
||||
open?: () => void
|
||||
close?: () => void
|
||||
noOp?: () => void
|
||||
disableHover?: boolean // disable the hover and content display
|
||||
timeout?: number
|
||||
}
|
||||
@@ -33,17 +36,19 @@ interface TooltipProps extends Omit<PopoverProps, 'content'> {
|
||||
interface TooltipContentProps extends Omit<PopoverProps, 'content'> {
|
||||
content: ReactNode
|
||||
onOpen?: () => void
|
||||
open?: () => void
|
||||
close?: () => void
|
||||
// whether to wrap the content in a `TooltipContainer`
|
||||
wrap?: boolean
|
||||
disableHover?: boolean // disable the hover and content display
|
||||
}
|
||||
|
||||
export default function Tooltip({ text, open, close, noOp, disableHover, ...rest }: TooltipProps) {
|
||||
export default function Tooltip({ text, open, close, disableHover, ...rest }: TooltipProps) {
|
||||
return (
|
||||
<Popover
|
||||
content={
|
||||
text && (
|
||||
<TooltipContainer onMouseEnter={disableHover ? noOp : open} onMouseLeave={disableHover ? noOp : close}>
|
||||
<TooltipContainer onMouseEnter={disableHover ? noop : open} onMouseLeave={disableHover ? noop : close}>
|
||||
{text}
|
||||
</TooltipContainer>
|
||||
)
|
||||
@@ -53,15 +58,28 @@ export default function Tooltip({ text, open, close, noOp, disableHover, ...rest
|
||||
)
|
||||
}
|
||||
|
||||
function TooltipContent({ content, wrap = false, ...rest }: TooltipContentProps) {
|
||||
return <Popover content={wrap ? <TooltipContainer>{content}</TooltipContainer> : content} {...rest} />
|
||||
function TooltipContent({ content, wrap = false, open, close, disableHover, ...rest }: TooltipContentProps) {
|
||||
return (
|
||||
<Popover
|
||||
content={
|
||||
wrap ? (
|
||||
<TooltipContainer onMouseEnter={disableHover ? noop : open} onMouseLeave={disableHover ? noop : close}>
|
||||
{content}
|
||||
</TooltipContainer>
|
||||
) : (
|
||||
content
|
||||
)
|
||||
}
|
||||
{...rest}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
/** Standard text tooltip. */
|
||||
export function MouseoverTooltip({ text, disableHover, children, timeout, ...rest }: Omit<TooltipProps, 'show'>) {
|
||||
const [show, setShow] = useState(false)
|
||||
const open = useCallback(() => text && setShow(true), [text, setShow])
|
||||
const close = useCallback(() => setShow(false), [setShow])
|
||||
const open = () => text && setShow(true)
|
||||
const close = () => setShow(false)
|
||||
|
||||
useEffect(() => {
|
||||
if (show && timeout) {
|
||||
@@ -76,18 +94,16 @@ export function MouseoverTooltip({ text, disableHover, children, timeout, ...res
|
||||
return
|
||||
}, [timeout, show])
|
||||
|
||||
const noOp = () => null
|
||||
return (
|
||||
<Tooltip
|
||||
{...rest}
|
||||
open={open}
|
||||
close={close}
|
||||
noOp={noOp}
|
||||
disableHover={disableHover}
|
||||
show={show}
|
||||
text={disableHover ? null : text}
|
||||
>
|
||||
<div onMouseEnter={disableHover ? noOp : open} onMouseLeave={disableHover || timeout ? noOp : close}>
|
||||
<div onMouseEnter={disableHover ? noop : open} onMouseLeave={disableHover || timeout ? noop : close}>
|
||||
{children}
|
||||
</div>
|
||||
</Tooltip>
|
||||
@@ -103,18 +119,23 @@ export function MouseoverTooltipContent({
|
||||
...rest
|
||||
}: Omit<TooltipContentProps, 'show'>) {
|
||||
const [show, setShow] = useState(false)
|
||||
const open = useCallback(() => {
|
||||
const open = () => {
|
||||
setShow(true)
|
||||
openCallback?.()
|
||||
}, [openCallback])
|
||||
const close = useCallback(() => setShow(false), [setShow])
|
||||
}
|
||||
const close = () => {
|
||||
setShow(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<TooltipContent {...rest} show={!disableHover && show} content={disableHover ? null : content}>
|
||||
<div
|
||||
style={{ display: 'inline-block', lineHeight: 0, padding: '0.25rem' }}
|
||||
onMouseEnter={open}
|
||||
onMouseLeave={close}
|
||||
>
|
||||
<TooltipContent
|
||||
{...rest}
|
||||
open={open}
|
||||
close={close}
|
||||
show={!disableHover && show}
|
||||
content={disableHover ? null : content}
|
||||
>
|
||||
<div onMouseEnter={open} onMouseLeave={close}>
|
||||
{children}
|
||||
</div>
|
||||
</TooltipContent>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { useWeb3React } from '@web3-react/core'
|
||||
import UniwalletModal from 'components/AccountDrawer/UniwalletModal'
|
||||
import UniswapWalletBanner from 'components/Banner/UniswapWalletBanner'
|
||||
import AddressClaimModal from 'components/claim/AddressClaimModal'
|
||||
import ConnectedAccountBlocked from 'components/ConnectedAccountBlocked'
|
||||
import FiatOnrampModal from 'components/FiatOnrampModal'
|
||||
import UniwalletModal from 'components/WalletDropdown/UniwalletModal'
|
||||
import useAccountRiskCheck from 'hooks/useAccountRiskCheck'
|
||||
import { lazy } from 'react'
|
||||
import { useModalIsOpen, useToggleModal } from 'state/application/hooks'
|
||||
|
||||
@@ -2,11 +2,11 @@ import { sendAnalyticsEvent, user } from '@uniswap/analytics'
|
||||
import { CustomUserProperties, InterfaceEventName, WalletConnectionResult } from '@uniswap/analytics-events'
|
||||
import { getWalletMeta } from '@uniswap/conedison/provider/meta'
|
||||
import { useWeb3React } from '@web3-react/core'
|
||||
import { useAccountDrawer } from 'components/AccountDrawer'
|
||||
import IconButton from 'components/AccountDrawer/IconButton'
|
||||
import { sendEvent } from 'components/analytics'
|
||||
import { AutoColumn } from 'components/Column'
|
||||
import { AutoRow } from 'components/Row'
|
||||
import { useWalletDrawer } from 'components/WalletDropdown'
|
||||
import IconButton from 'components/WalletDropdown/IconButton'
|
||||
import { Connection, ConnectionType, getConnections, networkConnection } from 'connection'
|
||||
import { useGetConnection } from 'connection'
|
||||
import { ErrorCode } from 'connection/utils'
|
||||
@@ -84,7 +84,7 @@ function didUserReject(connection: Connection, error: any): boolean {
|
||||
export default function WalletModal({ openSettings }: { openSettings: () => void }) {
|
||||
const dispatch = useAppDispatch()
|
||||
const { connector, account, chainId, provider } = useWeb3React()
|
||||
const [drawerOpen, toggleWalletDrawer] = useWalletDrawer()
|
||||
const [drawerOpen, toggleWalletDrawer] = useAccountDrawer()
|
||||
|
||||
const [connectedWallets, addWalletToConnectedWallets] = useConnectedWallets()
|
||||
const [lastActiveWalletAddress, setLastActiveWalletAddress] = useState<string | undefined>(account)
|
||||
|
||||
@@ -2,19 +2,17 @@ import { Trans } from '@lingui/macro'
|
||||
import { sendAnalyticsEvent, TraceEvent } from '@uniswap/analytics'
|
||||
import { BrowserEvent, InterfaceElementName, InterfaceEventName } from '@uniswap/analytics-events'
|
||||
import { useWeb3React } from '@web3-react/core'
|
||||
import PortfolioDrawer, { useAccountDrawer } from 'components/AccountDrawer'
|
||||
import PrefetchBalancesWrapper from 'components/AccountDrawer/PrefetchBalancesWrapper'
|
||||
import Loader from 'components/Icons/LoadingSpinner'
|
||||
import { IconWrapper } from 'components/Identicon/StatusIcon'
|
||||
import WalletDropdown, { useWalletDrawer } from 'components/WalletDropdown'
|
||||
import PrefetchBalancesWrapper from 'components/WalletDropdown/PrefetchBalancesWrapper'
|
||||
import { useGetConnection } from 'connection'
|
||||
import { Portal } from 'nft/components/common/Portal'
|
||||
import { useIsNftClaimAvailable } from 'nft/hooks/useIsNftClaimAvailable'
|
||||
import { getIsValidSwapQuote } from 'pages/Swap'
|
||||
import { darken } from 'polished'
|
||||
import { useCallback, useMemo } from 'react'
|
||||
import { AlertTriangle } from 'react-feather'
|
||||
import { useAppSelector } from 'state/hooks'
|
||||
import { useDerivedSwapInfo } from 'state/swap/hooks'
|
||||
import styled from 'styled-components/macro'
|
||||
import { colors } from 'theme/colors'
|
||||
import { flexRowNoWrap } from 'theme/styles'
|
||||
@@ -153,16 +151,11 @@ function Web3StatusInner() {
|
||||
const { account, connector, chainId, ENSName } = useWeb3React()
|
||||
const getConnection = useGetConnection()
|
||||
const connection = getConnection(connector)
|
||||
const {
|
||||
trade: { state: tradeState, trade },
|
||||
inputError: swapInputError,
|
||||
} = useDerivedSwapInfo()
|
||||
const validSwapQuote = getIsValidSwapQuote(trade, tradeState, swapInputError)
|
||||
const [, toggleWalletDrawer] = useWalletDrawer()
|
||||
const [, toggleAccountDrawer] = useAccountDrawer()
|
||||
const handleWalletDropdownClick = useCallback(() => {
|
||||
sendAnalyticsEvent(InterfaceEventName.ACCOUNT_DROPDOWN_BUTTON_CLICKED)
|
||||
toggleWalletDrawer()
|
||||
}, [toggleWalletDrawer])
|
||||
toggleAccountDrawer()
|
||||
}, [toggleAccountDrawer])
|
||||
const isClaimAvailable = useIsNftClaimAvailable((state) => state.isClaimAvailable)
|
||||
|
||||
const error = useAppSelector((state) => state.connection.errorByConnectionType[getConnection(connector).type])
|
||||
@@ -223,7 +216,6 @@ function Web3StatusInner() {
|
||||
<TraceEvent
|
||||
events={[BrowserEvent.onClick]}
|
||||
name={InterfaceEventName.CONNECT_WALLET_BUTTON_CLICKED}
|
||||
properties={{ received_swap_quote: validSwapQuote }}
|
||||
element={InterfaceElementName.CONNECT_WALLET_BUTTON}
|
||||
>
|
||||
<Web3StatusConnectWrapper
|
||||
@@ -241,13 +233,12 @@ function Web3StatusInner() {
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line import/no-unused-modules
|
||||
export default function Web3Status() {
|
||||
return (
|
||||
<PrefetchBalancesWrapper>
|
||||
<Web3StatusInner />
|
||||
<Portal>
|
||||
<WalletDropdown />
|
||||
<PortfolioDrawer />
|
||||
</Portal>
|
||||
</PrefetchBalancesWrapper>
|
||||
)
|
||||
|
||||
@@ -16,7 +16,7 @@ import {
|
||||
SwapWidgetSkeleton,
|
||||
} from '@uniswap/widgets'
|
||||
import { useWeb3React } from '@web3-react/core'
|
||||
import { useToggleWalletDrawer } from 'components/WalletDropdown'
|
||||
import { useToggleAccountDrawer } from 'components/AccountDrawer'
|
||||
import { useActiveLocale } from 'hooks/useActiveLocale'
|
||||
import {
|
||||
formatPercentInBasisPointsNumber,
|
||||
@@ -67,7 +67,7 @@ export default function Widget({
|
||||
const { settings } = useSyncWidgetSettings()
|
||||
const { transactions } = useSyncWidgetTransactions()
|
||||
|
||||
const toggleWalletDrawer = useToggleWalletDrawer()
|
||||
const toggleWalletDrawer = useToggleAccountDrawer()
|
||||
const onConnectWalletClick = useCallback(() => {
|
||||
toggleWalletDrawer()
|
||||
return false // prevents the in-widget wallet modal from opening
|
||||
|
||||
111
src/components/swap/SwapBuyFiatButton.test.tsx
Normal file
111
src/components/swap/SwapBuyFiatButton.test.tsx
Normal file
@@ -0,0 +1,111 @@
|
||||
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 { useFiatOnrampAvailability, useOpenModal } from '../../state/application/hooks'
|
||||
import SwapBuyFiatButton, { MOONPAY_REGION_AVAILABILITY_ARTICLE } from './SwapBuyFiatButton'
|
||||
|
||||
jest.mock('@web3-react/core', () => {
|
||||
const web3React = jest.requireActual('@web3-react/core')
|
||||
return {
|
||||
...web3React,
|
||||
useWeb3React: jest.fn(),
|
||||
}
|
||||
})
|
||||
|
||||
jest.mock('../../state/application/hooks')
|
||||
const mockUseFiatOnrampAvailability = useFiatOnrampAvailability as jest.MockedFunction<typeof useFiatOnrampAvailability>
|
||||
const mockUseOpenModal = useOpenModal as jest.MockedFunction<typeof useOpenModal>
|
||||
|
||||
jest.mock('components/AccountDrawer')
|
||||
const mockuseAccountDrawer = useAccountDrawer as jest.MockedFunction<typeof useAccountDrawer>
|
||||
|
||||
const mockUseFiatOnRampsUnavailable = (shouldCheck: boolean) => {
|
||||
return {
|
||||
available: false,
|
||||
availabilityChecked: shouldCheck,
|
||||
error: null,
|
||||
loading: false,
|
||||
}
|
||||
}
|
||||
|
||||
const mockUseFiatOnRampsAvailable = (shouldCheck: boolean) => {
|
||||
if (shouldCheck) {
|
||||
return {
|
||||
available: true,
|
||||
availabilityChecked: true,
|
||||
error: null,
|
||||
loading: false,
|
||||
}
|
||||
}
|
||||
return {
|
||||
available: false,
|
||||
availabilityChecked: false,
|
||||
error: null,
|
||||
loading: false,
|
||||
}
|
||||
}
|
||||
|
||||
describe('SwapBuyFiatButton.tsx', () => {
|
||||
let toggleWalletDrawer: jest.Mock<any, any>
|
||||
let useOpenModal: jest.Mock<any, any>
|
||||
|
||||
beforeAll(() => {
|
||||
toggleWalletDrawer = jest.fn()
|
||||
useOpenModal = jest.fn()
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks()
|
||||
;(useWeb3React as jest.Mock).mockReturnValue({
|
||||
account: undefined,
|
||||
isActive: false,
|
||||
})
|
||||
})
|
||||
|
||||
it('matches base snapshot', () => {
|
||||
mockUseFiatOnrampAvailability.mockImplementation(mockUseFiatOnRampsUnavailable)
|
||||
mockuseAccountDrawer.mockImplementation(() => [false, toggleWalletDrawer])
|
||||
const { asFragment } = render(<SwapBuyFiatButton />)
|
||||
expect(asFragment()).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it('fiat on ramps available in region, account unconnected', async () => {
|
||||
mockUseFiatOnrampAvailability.mockImplementation(mockUseFiatOnRampsAvailable)
|
||||
mockuseAccountDrawer.mockImplementation(() => [false, toggleWalletDrawer])
|
||||
mockUseOpenModal.mockImplementation(() => useOpenModal)
|
||||
render(<SwapBuyFiatButton />)
|
||||
await userEvent.click(screen.getByTestId('buy-fiat-button'))
|
||||
expect(toggleWalletDrawer).toHaveBeenCalledTimes(1)
|
||||
expect(screen.queryByTestId('fiat-on-ramp-unavailable-tooltip')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('fiat on ramps available in region, account connected', async () => {
|
||||
;(useWeb3React as jest.Mock).mockReturnValue({
|
||||
account: '0x52270d8234b864dcAC9947f510CE9275A8a116Db',
|
||||
isActive: true,
|
||||
})
|
||||
mockUseFiatOnrampAvailability.mockImplementation(mockUseFiatOnRampsAvailable)
|
||||
mockuseAccountDrawer.mockImplementation(() => [false, toggleWalletDrawer])
|
||||
mockUseOpenModal.mockImplementation(() => useOpenModal)
|
||||
render(<SwapBuyFiatButton />)
|
||||
expect(screen.getByTestId('buy-fiat-flow-incomplete-indicator')).toBeInTheDocument()
|
||||
await userEvent.click(screen.getByTestId('buy-fiat-button'))
|
||||
expect(toggleWalletDrawer).toHaveBeenCalledTimes(0)
|
||||
expect(useOpenModal).toHaveBeenCalledTimes(1)
|
||||
expect(screen.queryByTestId('fiat-on-ramp-unavailable-tooltip')).not.toBeInTheDocument()
|
||||
expect(screen.queryByTestId('buy-fiat-flow-incomplete-indicator')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('fiat on ramps unavailable in region', async () => {
|
||||
mockUseFiatOnrampAvailability.mockImplementation(mockUseFiatOnRampsUnavailable)
|
||||
mockuseAccountDrawer.mockImplementation(() => [false, toggleWalletDrawer])
|
||||
render(<SwapBuyFiatButton />)
|
||||
await userEvent.click(screen.getByTestId('buy-fiat-button'))
|
||||
fireEvent.mouseOver(screen.getByTestId('buy-fiat-button'))
|
||||
expect(await screen.findByTestId('fiat-on-ramp-unavailable-tooltip')).toBeInTheDocument()
|
||||
expect(await screen.findByText(/Learn more/i)).toHaveAttribute('href', MOONPAY_REGION_AVAILABILITY_ARTICLE)
|
||||
expect(await screen.findByTestId('buy-fiat-button')).toBeDisabled()
|
||||
})
|
||||
})
|
||||
146
src/components/swap/SwapBuyFiatButton.tsx
Normal file
146
src/components/swap/SwapBuyFiatButton.tsx
Normal file
@@ -0,0 +1,146 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import { TraceEvent } from '@uniswap/analytics'
|
||||
import { BrowserEvent, InterfaceElementName, SharedEventName } from '@uniswap/analytics-events'
|
||||
import { useWeb3React } from '@web3-react/core'
|
||||
import { useAccountDrawer } from 'components/AccountDrawer'
|
||||
import { ButtonText } from 'components/Button'
|
||||
import { MouseoverTooltipContent } from 'components/Tooltip'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { useBuyFiatFlowCompleted } from 'state/user/hooks'
|
||||
import styled from 'styled-components/macro'
|
||||
import { ExternalLink } from 'theme'
|
||||
|
||||
import { useFiatOnrampAvailability, useOpenModal } from '../../state/application/hooks'
|
||||
import { ApplicationModal } from '../../state/application/reducer'
|
||||
|
||||
const Dot = styled.div`
|
||||
height: 8px;
|
||||
width: 8px;
|
||||
background-color: ${({ theme }) => theme.accentActive};
|
||||
border-radius: 50%;
|
||||
`
|
||||
|
||||
export const MOONPAY_REGION_AVAILABILITY_ARTICLE =
|
||||
'https://support.uniswap.org/hc/en-us/articles/11306664890381-Why-isn-t-MoonPay-available-in-my-region-'
|
||||
|
||||
enum BuyFiatFlowState {
|
||||
// Default initial state. User is not actively trying to buy fiat.
|
||||
INACTIVE,
|
||||
// Buy fiat flow is active and region availability has been checked.
|
||||
ACTIVE_CHECKING_REGION,
|
||||
// Buy fiat flow is active, feature is available in user's region & needs wallet connection.
|
||||
ACTIVE_NEEDS_ACCOUNT,
|
||||
}
|
||||
|
||||
const StyledTextButton = styled(ButtonText)`
|
||||
color: ${({ theme }) => theme.textSecondary};
|
||||
gap: 4px;
|
||||
&:focus {
|
||||
text-decoration: none;
|
||||
}
|
||||
&:active {
|
||||
text-decoration: none;
|
||||
}
|
||||
`
|
||||
|
||||
export default function SwapBuyFiatButton() {
|
||||
const { account } = useWeb3React()
|
||||
const openFiatOnRampModal = useOpenModal(ApplicationModal.FIAT_ONRAMP)
|
||||
const [buyFiatFlowCompleted, setBuyFiatFlowCompleted] = useBuyFiatFlowCompleted()
|
||||
const [checkFiatRegionAvailability, setCheckFiatRegionAvailability] = useState(false)
|
||||
const {
|
||||
available: fiatOnrampAvailable,
|
||||
availabilityChecked: fiatOnrampAvailabilityChecked,
|
||||
loading: fiatOnrampAvailabilityLoading,
|
||||
} = useFiatOnrampAvailability(checkFiatRegionAvailability)
|
||||
const [buyFiatFlowState, setBuyFiatFlowState] = useState(BuyFiatFlowState.INACTIVE)
|
||||
const [walletDrawerOpen, toggleWalletDrawer] = useAccountDrawer()
|
||||
|
||||
/*
|
||||
* Depending on the current state of the buy fiat flow the user is in (buyFiatFlowState),
|
||||
* the desired behavior of clicking the 'Buy' button is different.
|
||||
* 1) Initially upon first click, need to check the availability of the feature in the user's
|
||||
* region, and continue the flow.
|
||||
* 2) If the feature is available in the user's region, need to connect a wallet, and continue
|
||||
* the flow.
|
||||
* 3) If the feature is available and a wallet account is connected, show fiat on ramp modal.
|
||||
* 4) If the feature is unavailable, show feature unavailable tooltip.
|
||||
*/
|
||||
const handleBuyCrypto = useCallback(() => {
|
||||
if (!fiatOnrampAvailabilityChecked) {
|
||||
setCheckFiatRegionAvailability(true)
|
||||
setBuyFiatFlowState(BuyFiatFlowState.ACTIVE_CHECKING_REGION)
|
||||
} else if (fiatOnrampAvailable && !account && !walletDrawerOpen) {
|
||||
toggleWalletDrawer()
|
||||
setBuyFiatFlowState(BuyFiatFlowState.ACTIVE_NEEDS_ACCOUNT)
|
||||
} else if (fiatOnrampAvailable && account) {
|
||||
openFiatOnRampModal()
|
||||
setBuyFiatFlowCompleted(true)
|
||||
setBuyFiatFlowState(BuyFiatFlowState.INACTIVE)
|
||||
} else if (!fiatOnrampAvailable) {
|
||||
setBuyFiatFlowCompleted(true)
|
||||
setBuyFiatFlowState(BuyFiatFlowState.INACTIVE)
|
||||
}
|
||||
}, [
|
||||
fiatOnrampAvailabilityChecked,
|
||||
fiatOnrampAvailable,
|
||||
account,
|
||||
walletDrawerOpen,
|
||||
toggleWalletDrawer,
|
||||
openFiatOnRampModal,
|
||||
setBuyFiatFlowCompleted,
|
||||
])
|
||||
|
||||
// Continue buy fiat flow automatically when requisite state changes have occured.
|
||||
useEffect(() => {
|
||||
if (
|
||||
(buyFiatFlowState === BuyFiatFlowState.ACTIVE_CHECKING_REGION && fiatOnrampAvailabilityChecked) ||
|
||||
(account && buyFiatFlowState === BuyFiatFlowState.ACTIVE_NEEDS_ACCOUNT)
|
||||
) {
|
||||
handleBuyCrypto()
|
||||
}
|
||||
}, [account, handleBuyCrypto, buyFiatFlowState, fiatOnrampAvailabilityChecked])
|
||||
|
||||
const buyCryptoButtonDisabled =
|
||||
(!fiatOnrampAvailable && fiatOnrampAvailabilityChecked) ||
|
||||
fiatOnrampAvailabilityLoading ||
|
||||
// When wallet drawer is open AND user is in the connect wallet step of the buy fiat flow, disable buy fiat button.
|
||||
(walletDrawerOpen && buyFiatFlowState === BuyFiatFlowState.ACTIVE_NEEDS_ACCOUNT)
|
||||
|
||||
const fiatOnRampsUnavailableTooltipDisabled =
|
||||
!fiatOnrampAvailabilityChecked || (fiatOnrampAvailabilityChecked && fiatOnrampAvailable)
|
||||
|
||||
return (
|
||||
<MouseoverTooltipContent
|
||||
wrap
|
||||
content={
|
||||
<div data-testid="fiat-on-ramp-unavailable-tooltip">
|
||||
<Trans>Crypto purchases are not available in your region. </Trans>
|
||||
<TraceEvent
|
||||
events={[BrowserEvent.onClick]}
|
||||
name={SharedEventName.ELEMENT_CLICKED}
|
||||
element={InterfaceElementName.FIAT_ON_RAMP_LEARN_MORE_LINK}
|
||||
>
|
||||
<ExternalLink href={MOONPAY_REGION_AVAILABILITY_ARTICLE} style={{ paddingLeft: '4px' }}>
|
||||
<Trans>Learn more</Trans>
|
||||
</ExternalLink>
|
||||
</TraceEvent>
|
||||
</div>
|
||||
}
|
||||
placement="bottom"
|
||||
disableHover={fiatOnRampsUnavailableTooltipDisabled}
|
||||
>
|
||||
<TraceEvent
|
||||
events={[BrowserEvent.onClick]}
|
||||
name={SharedEventName.ELEMENT_CLICKED}
|
||||
element={InterfaceElementName.FIAT_ON_RAMP_BUY_BUTTON}
|
||||
properties={{ account_connected: !!account }}
|
||||
>
|
||||
<StyledTextButton onClick={handleBuyCrypto} disabled={buyCryptoButtonDisabled} data-testid="buy-fiat-button">
|
||||
<Trans>Buy</Trans>
|
||||
{!buyFiatFlowCompleted && <Dot data-testid="buy-fiat-flow-incomplete-indicator" />}
|
||||
</StyledTextButton>
|
||||
</TraceEvent>
|
||||
</MouseoverTooltipContent>
|
||||
)
|
||||
}
|
||||
@@ -1,10 +1,12 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import { Percent } from '@uniswap/sdk-core'
|
||||
import { useFiatOnRampButtonEnabled } from 'featureFlags/flags/fiatOnRampButton'
|
||||
import { subhead } from 'nft/css/common.css'
|
||||
import styled from 'styled-components/macro'
|
||||
|
||||
import { ThemedText } from '../../theme'
|
||||
import { RowBetween, RowFixed } from '../Row'
|
||||
import SettingsTab from '../Settings'
|
||||
import SwapBuyFiatButton from './SwapBuyFiatButton'
|
||||
|
||||
const StyledSwapHeader = styled.div`
|
||||
padding: 8px 12px;
|
||||
@@ -13,14 +15,27 @@ const StyledSwapHeader = styled.div`
|
||||
color: ${({ theme }) => theme.textSecondary};
|
||||
`
|
||||
|
||||
const TextHeader = styled.div`
|
||||
color: ${({ theme }) => theme.textPrimary};
|
||||
margin-right: 8px;
|
||||
display: flex;
|
||||
line-height: 20px;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
`
|
||||
|
||||
export default function SwapHeader({ allowedSlippage }: { allowedSlippage: Percent }) {
|
||||
const fiatOnRampButtonEnabled = useFiatOnRampButtonEnabled()
|
||||
|
||||
return (
|
||||
<StyledSwapHeader>
|
||||
<RowBetween>
|
||||
<RowFixed>
|
||||
<ThemedText.DeprecatedBlack fontWeight={500} fontSize={16} style={{ marginRight: '8px' }}>
|
||||
<RowFixed style={{ gap: '8px' }}>
|
||||
<TextHeader className={subhead}>
|
||||
<Trans>Swap</Trans>
|
||||
</ThemedText.DeprecatedBlack>
|
||||
</TextHeader>
|
||||
{fiatOnRampButtonEnabled && <SwapBuyFiatButton />}
|
||||
</RowFixed>
|
||||
<RowFixed>
|
||||
<SettingsTab placeholderSlippage={allowedSlippage} />
|
||||
|
||||
@@ -0,0 +1,157 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`SwapBuyFiatButton.tsx matches base snapshot 1`] = `
|
||||
<DocumentFragment>
|
||||
.c1 {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
min-width: 0;
|
||||
-webkit-appearance: none;
|
||||
-moz-appearance: none;
|
||||
appearance: none;
|
||||
display: inline-block;
|
||||
text-align: center;
|
||||
line-height: inherit;
|
||||
-webkit-text-decoration: none;
|
||||
text-decoration: none;
|
||||
font-size: inherit;
|
||||
padding-left: 16px;
|
||||
padding-right: 16px;
|
||||
padding-top: 8px;
|
||||
padding-bottom: 8px;
|
||||
color: white;
|
||||
background-color: primary;
|
||||
border: 0;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.c2 {
|
||||
padding: 16px;
|
||||
width: 100%;
|
||||
font-weight: 500;
|
||||
text-align: center;
|
||||
border-radius: 20px;
|
||||
outline: none;
|
||||
border: 1px solid transparent;
|
||||
color: #0D111C;
|
||||
-webkit-text-decoration: none;
|
||||
text-decoration: none;
|
||||
display: -webkit-box;
|
||||
display: -webkit-flex;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
-webkit-box-pack: center;
|
||||
-webkit-justify-content: center;
|
||||
-ms-flex-pack: center;
|
||||
justify-content: center;
|
||||
-webkit-flex-wrap: nowrap;
|
||||
-ms-flex-wrap: nowrap;
|
||||
flex-wrap: nowrap;
|
||||
-webkit-align-items: center;
|
||||
-webkit-box-align: center;
|
||||
-ms-flex-align: center;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
will-change: transform;
|
||||
-webkit-transition: -webkit-transform 450ms ease;
|
||||
-webkit-transition: transform 450ms ease;
|
||||
transition: transform 450ms ease;
|
||||
-webkit-transform: perspective(1px) translateZ(0);
|
||||
-ms-transform: perspective(1px) translateZ(0);
|
||||
transform: perspective(1px) translateZ(0);
|
||||
}
|
||||
|
||||
.c2:disabled {
|
||||
opacity: 50%;
|
||||
cursor: auto;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.c2 > * {
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.c2 > a {
|
||||
-webkit-text-decoration: none;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.c3 {
|
||||
padding: 0;
|
||||
width: -webkit-fit-content;
|
||||
width: -moz-fit-content;
|
||||
width: fit-content;
|
||||
background: none;
|
||||
-webkit-text-decoration: none;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.c3:focus {
|
||||
-webkit-text-decoration: underline;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.c3:hover {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.c3:active {
|
||||
-webkit-text-decoration: underline;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.c3:disabled {
|
||||
opacity: 50%;
|
||||
cursor: auto;
|
||||
}
|
||||
|
||||
.c0 {
|
||||
display: inline-block;
|
||||
height: inherit;
|
||||
}
|
||||
|
||||
.c5 {
|
||||
height: 8px;
|
||||
width: 8px;
|
||||
background-color: #4C82FB;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.c4 {
|
||||
color: #7780A0;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.c4:focus {
|
||||
-webkit-text-decoration: none;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.c4:active {
|
||||
-webkit-text-decoration: none;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
<div
|
||||
class="c0"
|
||||
>
|
||||
<div>
|
||||
<button
|
||||
class="c1 c2 c3 c4"
|
||||
data-testid="buy-fiat-button"
|
||||
>
|
||||
Buy
|
||||
<div
|
||||
class="c5"
|
||||
data-testid="buy-fiat-flow-incomplete-indicator"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
@@ -5,6 +5,7 @@ export enum FeatureFlag {
|
||||
traceJsonRpc = 'traceJsonRpc',
|
||||
permit2 = 'permit2',
|
||||
payWithAnyToken = 'payWithAnyToken',
|
||||
fiatOnRampButtonOnSwap = 'fiat_on_ramp_button_on_swap_page',
|
||||
swapWidget = 'swap_widget_replacement_enabled',
|
||||
statsigDummy = 'web_dummy_gate_amplitude_id',
|
||||
nftGraphql = 'nft_graphql_migration',
|
||||
|
||||
9
src/featureFlags/flags/fiatOnRampButton.ts
Normal file
9
src/featureFlags/flags/fiatOnRampButton.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { BaseVariant, FeatureFlag, useBaseFlag } from '../index'
|
||||
|
||||
function useFiatOnRampButtonFlag(): BaseVariant {
|
||||
return useBaseFlag(FeatureFlag.fiatOnRampButtonOnSwap)
|
||||
}
|
||||
|
||||
export function useFiatOnRampButtonEnabled(): boolean {
|
||||
return useFiatOnRampButtonFlag() === BaseVariant.Enabled
|
||||
}
|
||||
@@ -39,33 +39,18 @@ gql`
|
||||
node {
|
||||
id
|
||||
name
|
||||
ownerAddress
|
||||
image {
|
||||
url
|
||||
}
|
||||
smallImage {
|
||||
url
|
||||
}
|
||||
originalImage {
|
||||
url
|
||||
}
|
||||
tokenId
|
||||
description
|
||||
animationUrl
|
||||
suspiciousFlag
|
||||
collection {
|
||||
name
|
||||
isVerified
|
||||
image {
|
||||
url
|
||||
}
|
||||
creator {
|
||||
address
|
||||
profileImage {
|
||||
url
|
||||
}
|
||||
isVerified
|
||||
}
|
||||
nftContracts {
|
||||
address
|
||||
standard
|
||||
@@ -98,11 +83,8 @@ gql`
|
||||
}
|
||||
}
|
||||
rarities {
|
||||
provider
|
||||
rank
|
||||
score
|
||||
}
|
||||
metadataUrl
|
||||
}
|
||||
cursor
|
||||
}
|
||||
|
||||
@@ -122,7 +122,8 @@ export function useNftBalance(
|
||||
first?: number,
|
||||
after?: string,
|
||||
last?: number,
|
||||
before?: string
|
||||
before?: string,
|
||||
skip = false
|
||||
) {
|
||||
const { data, loading, fetchMore } = useNftBalanceQuery({
|
||||
variables: {
|
||||
@@ -140,6 +141,7 @@ export function useNftBalance(
|
||||
last,
|
||||
before,
|
||||
},
|
||||
skip,
|
||||
})
|
||||
|
||||
const hasNext = data?.nftBalances?.pageInfo?.hasNextPage
|
||||
|
||||
@@ -18,7 +18,9 @@ describe('fetchTokenList', () => {
|
||||
fetch.mockOnceIf(url, () => {
|
||||
throw new Error()
|
||||
})
|
||||
await expect(fetchTokenList(url, resolver)).rejects.toThrow(`failed to fetch list: ${url}`)
|
||||
await expect(fetchTokenList(url, resolver)).rejects.toThrow(
|
||||
`No valid token list found at any URLs derived from ${url}.`
|
||||
)
|
||||
expect(console.debug).toHaveBeenCalled()
|
||||
expect(resolver).not.toHaveBeenCalled()
|
||||
})
|
||||
@@ -33,9 +35,63 @@ describe('fetchTokenList', () => {
|
||||
expect(resolver).toHaveBeenCalledWith(url)
|
||||
})
|
||||
|
||||
it('throws an error when the ENS resolver throws', async () => {
|
||||
const url = 'example.eth'
|
||||
const error = new Error('ENS resolver error')
|
||||
resolver.mockRejectedValue(error)
|
||||
await expect(fetchTokenList(url, resolver)).rejects.toThrow(`failed to resolve ENS name: ${url}`)
|
||||
expect(resolver).toHaveBeenCalledWith(url)
|
||||
})
|
||||
|
||||
it('fetches and validates a list from an ENS address', async () => {
|
||||
jest.mock('../../utils/contenthashToUri', () =>
|
||||
jest.fn().mockImplementation(() => 'ipfs://QmPgEqyV3m8SB52BS2j2mJpu9zGprhj2BGCHtRiiw2fdM1')
|
||||
)
|
||||
const url = 'example.eth'
|
||||
const contenthash = '0xe3010170122013e051d1cfff20606de36845d4fe28deb9861a319a5bc8596fa4e610e8803918'
|
||||
const translatedUri = 'https://cloudflare-ipfs.com/ipfs/QmPgEqyV3m8SB52BS2j2mJpu9zGprhj2BGCHtRiiw2fdM1/'
|
||||
resolver.mockResolvedValue(contenthash)
|
||||
fetch.mockOnceIf(translatedUri, () => Promise.resolve(JSON.stringify(defaultTokenList)))
|
||||
await expect(fetchTokenList(url, resolver)).resolves.toStrictEqual(defaultTokenList)
|
||||
})
|
||||
|
||||
it('throws for an unrecognized list URL protocol', async () => {
|
||||
const url = 'unknown://example.com/invalid-tokenlist.json'
|
||||
fetch.mockOnceIf(url, () => Promise.resolve(''))
|
||||
await expect(fetchTokenList(url, resolver)).rejects.toThrow(`Unrecognized list URL protocol.`)
|
||||
})
|
||||
|
||||
it('logs a debug statement if the response is not successful', async () => {
|
||||
const url = 'https://example.com/invalid-tokenlist.json'
|
||||
fetch.mockOnceIf(url, () => Promise.resolve({ status: 404 }))
|
||||
await expect(fetchTokenList(url, resolver)).rejects.toThrow(
|
||||
`No valid token list found at any URLs derived from ${url}.`
|
||||
)
|
||||
expect(console.debug).toHaveBeenCalled()
|
||||
expect(resolver).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('fetches and validates the default token list', async () => {
|
||||
fetch.mockOnceIf(DEFAULT_TOKEN_LIST, () => Promise.resolve(JSON.stringify(defaultTokenList)))
|
||||
await expect(fetchTokenList(DEFAULT_TOKEN_LIST, resolver)).resolves.toStrictEqual(defaultTokenList)
|
||||
expect(resolver).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('throws for a list with invalid json response', async () => {
|
||||
const url = 'https://example.com/invalid-tokenlist.json'
|
||||
fetch.mockOnceIf(url, () => Promise.resolve('invalid json'))
|
||||
await expect(fetchTokenList(url, resolver)).rejects.toThrow(
|
||||
`No valid token list found at any URLs derived from ${url}.`
|
||||
)
|
||||
expect(console.debug).toHaveBeenCalled()
|
||||
expect(resolver).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('uses cached value the second time', async () => {
|
||||
const url = 'https://example.com/invalid-tokenlist.json'
|
||||
fetch.mockOnceIf(url, () => Promise.resolve(JSON.stringify(defaultTokenList)))
|
||||
await expect(fetchTokenList(url, resolver)).resolves.toStrictEqual(defaultTokenList)
|
||||
await expect(fetchTokenList(url, resolver)).resolves.toStrictEqual(defaultTokenList)
|
||||
expect(fetch).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -8,7 +8,11 @@ export const DEFAULT_TOKEN_LIST = 'https://gateway.ipfs.io/ipns/tokens.uniswap.o
|
||||
|
||||
const listCache = new Map<string, TokenList>()
|
||||
|
||||
/** Fetches and validates a token list. */
|
||||
/**
|
||||
* Fetches and validates a token list.
|
||||
* For a given token list URL, we try to fetch the list from all the possible HTTP URLs.
|
||||
* For example, IPFS URLs can be fetched through multiple gateways.
|
||||
*/
|
||||
export default async function fetchTokenList(
|
||||
listUrl: string,
|
||||
resolveENSContentHash: (ensName: string) => Promise<string>,
|
||||
@@ -43,31 +47,38 @@ export default async function fetchTokenList(
|
||||
urls = uriToHttp(listUrl)
|
||||
}
|
||||
|
||||
if (urls.length === 0) {
|
||||
throw new Error('Unrecognized list URL protocol.')
|
||||
}
|
||||
|
||||
// Try each of the derived URLs until one succeeds.
|
||||
for (let i = 0; i < urls.length; i++) {
|
||||
const url = urls[i]
|
||||
const isLast = i === urls.length - 1
|
||||
let response
|
||||
try {
|
||||
response = await fetch(url, { credentials: 'omit' })
|
||||
} catch (error) {
|
||||
const message = `failed to fetch list: ${listUrl}`
|
||||
console.debug(message, error)
|
||||
if (isLast) throw new Error(message)
|
||||
console.debug(`failed to fetch list: ${listUrl} (${url})`, error)
|
||||
continue
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
const message = `failed to fetch list: ${listUrl}`
|
||||
console.debug(message, response.statusText)
|
||||
if (isLast) throw new Error(message)
|
||||
console.debug(`failed to fetch list ${listUrl} (${url})`, response.statusText)
|
||||
continue
|
||||
}
|
||||
|
||||
const json = await response.json()
|
||||
const list = skipValidation ? json : await validateTokenList(json)
|
||||
listCache?.set(listUrl, list)
|
||||
return list
|
||||
try {
|
||||
// The content of the result is sometimes invalid even with a 200 status code.
|
||||
// A response can be invalid if it's not a valid JSON or if it doesn't match the TokenList schema.
|
||||
const json = await response.json()
|
||||
const list = skipValidation ? json : await validateTokenList(json)
|
||||
listCache?.set(listUrl, list)
|
||||
return list
|
||||
} catch (error) {
|
||||
console.debug(`failed to parse and validate list response: ${listUrl} (${url})`, error)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error('Unrecognized list URL protocol.')
|
||||
throw new Error(`No valid token list found at any URLs derived from ${listUrl}.`)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { i18n } from '@lingui/core'
|
||||
import { I18nProvider } from '@lingui/react'
|
||||
import * as Sentry from '@sentry/react'
|
||||
import { DEFAULT_LOCALE, SupportedLocale } from 'constants/locales'
|
||||
import {
|
||||
af,
|
||||
@@ -83,9 +82,8 @@ export async function dynamicActivate(locale: SupportedLocale) {
|
||||
const catalog = await import(`locales/${locale}.js`)
|
||||
// Bundlers will either export it as default or as a named export named default.
|
||||
i18n.load(locale, catalog.messages || catalog.default.messages)
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
Sentry.captureException(new Error(`Unable to load locale (${locale})`))
|
||||
} catch (error: unknown) {
|
||||
console.error(new Error(`Unable to load locale (${locale}): ${error}`))
|
||||
}
|
||||
i18n.activate(locale)
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import { sendAnalyticsEvent, TraceEvent } from '@uniswap/analytics'
|
||||
import { BrowserEvent, InterfaceElementName, NFTEventName } from '@uniswap/analytics-events'
|
||||
import { Currency, CurrencyAmount, Token, TradeType } from '@uniswap/sdk-core'
|
||||
import { useWeb3React } from '@web3-react/core'
|
||||
import { useToggleAccountDrawer } from 'components/AccountDrawer'
|
||||
import Column from 'components/Column'
|
||||
import Loader from 'components/Icons/LoadingSpinner'
|
||||
import CurrencyLogo from 'components/Logo/CurrencyLogo'
|
||||
@@ -12,7 +13,6 @@ import Row from 'components/Row'
|
||||
import CurrencySearchModal from 'components/SearchModal/CurrencySearchModal'
|
||||
import { LoadingBubble } from 'components/Tokens/loading'
|
||||
import { MouseoverTooltip } from 'components/Tooltip'
|
||||
import { useToggleWalletDrawer } from 'components/WalletDropdown'
|
||||
import { SupportedChainId } from 'constants/chains'
|
||||
import { usePayWithAnyTokenEnabled } from 'featureFlags/flags/payWithAnyToken'
|
||||
import { useCurrency } from 'hooks/Tokens'
|
||||
@@ -287,7 +287,7 @@ const PENDING_BAG_STATUSES = [
|
||||
]
|
||||
|
||||
export const BagFooter = ({ setModalIsOpen, eventProperties }: BagFooterProps) => {
|
||||
const toggleWalletDrawer = useToggleWalletDrawer()
|
||||
const toggleWalletDrawer = useToggleAccountDrawer()
|
||||
const theme = useTheme()
|
||||
const { account, chainId, connector } = useWeb3React()
|
||||
const connected = Boolean(account && chainId)
|
||||
|
||||
@@ -5,11 +5,11 @@ import Bag from './Bag'
|
||||
jest.mock('@web3-react/core', () => {
|
||||
const web3React = jest.requireActual('@web3-react/core')
|
||||
return {
|
||||
...web3React,
|
||||
useWeb3React: () => ({
|
||||
account: '0x52270d8234b864dcAC9947f510CE9275A8a116Db',
|
||||
isActive: true,
|
||||
}),
|
||||
...web3React,
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -2,8 +2,8 @@ import { Trans } from '@lingui/macro'
|
||||
import { Trace } from '@uniswap/analytics'
|
||||
import { InterfacePageName } from '@uniswap/analytics-events'
|
||||
import { useWeb3React } from '@web3-react/core'
|
||||
import { useToggleAccountDrawer } from 'components/AccountDrawer'
|
||||
import { ButtonPrimary } from 'components/Button'
|
||||
import { useToggleWalletDrawer } from 'components/WalletDropdown'
|
||||
import { XXXL_BAG_WIDTH } from 'nft/components/bag/Bag'
|
||||
import { ListPage } from 'nft/components/profile/list/ListPage'
|
||||
import { ProfilePage } from 'nft/components/profile/view/ProfilePage'
|
||||
@@ -67,7 +67,7 @@ const ProfileContent = () => {
|
||||
|
||||
const { account } = useWeb3React()
|
||||
const accountRef = useRef(account)
|
||||
const toggleWalletDrawer = useToggleWalletDrawer()
|
||||
const toggleWalletDrawer = useToggleAccountDrawer()
|
||||
|
||||
useEffect(() => {
|
||||
if (accountRef.current !== account) {
|
||||
|
||||
@@ -6,10 +6,10 @@ import { BrowserEvent, InterfaceElementName, InterfaceEventName } from '@uniswap
|
||||
import { Currency, CurrencyAmount, Percent } from '@uniswap/sdk-core'
|
||||
import { FeeAmount, NonfungiblePositionManager } from '@uniswap/v3-sdk'
|
||||
import { useWeb3React } from '@web3-react/core'
|
||||
import { useToggleAccountDrawer } from 'components/AccountDrawer'
|
||||
import OwnershipWarning from 'components/addLiquidity/OwnershipWarning'
|
||||
import { sendEvent } from 'components/analytics'
|
||||
import UnsupportedCurrencyFooter from 'components/swap/UnsupportedCurrencyFooter'
|
||||
import { useToggleWalletDrawer } from 'components/WalletDropdown'
|
||||
import usePrevious from 'hooks/usePrevious'
|
||||
import { useSingleCallResult } from 'lib/hooks/multicall'
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
@@ -91,7 +91,7 @@ export default function AddLiquidity() {
|
||||
const { account, chainId, provider } = useWeb3React()
|
||||
const theme = useTheme()
|
||||
|
||||
const toggleWalletDrawer = useToggleWalletDrawer() // toggle wallet when disconnected
|
||||
const toggleWalletDrawer = useToggleAccountDrawer() // toggle wallet when disconnected
|
||||
const expertMode = useIsExpertMode()
|
||||
const addTransaction = useTransactionAdder()
|
||||
const positionManager = useV3NFTPositionManagerContract()
|
||||
|
||||
@@ -5,10 +5,10 @@ import { TraceEvent } from '@uniswap/analytics'
|
||||
import { BrowserEvent, InterfaceElementName, InterfaceEventName } from '@uniswap/analytics-events'
|
||||
import { Currency, CurrencyAmount, Percent } from '@uniswap/sdk-core'
|
||||
import { useWeb3React } from '@web3-react/core'
|
||||
import { useToggleAccountDrawer } from 'components/AccountDrawer'
|
||||
import { sendEvent } from 'components/analytics'
|
||||
import UnsupportedCurrencyFooter from 'components/swap/UnsupportedCurrencyFooter'
|
||||
import { SwitchLocaleLink } from 'components/SwitchLocaleLink'
|
||||
import { useToggleWalletDrawer } from 'components/WalletDropdown'
|
||||
import { useCallback, useState } from 'react'
|
||||
import { Plus } from 'react-feather'
|
||||
import { useLocation, useNavigate, useParams } from 'react-router-dom'
|
||||
@@ -67,7 +67,7 @@ export default function AddLiquidity() {
|
||||
((currencyA && currencyA.equals(wrappedNativeCurrency)) || (currencyB && currencyB.equals(wrappedNativeCurrency)))
|
||||
)
|
||||
|
||||
const toggleWalletDrawer = useToggleWalletDrawer() // toggle wallet when disconnected
|
||||
const toggleWalletDrawer = useToggleAccountDrawer() // toggle wallet when disconnected
|
||||
|
||||
const expertMode = useIsExpertMode()
|
||||
|
||||
|
||||
@@ -2,13 +2,13 @@ import { Trans } from '@lingui/macro'
|
||||
import { Trace, TraceEvent } from '@uniswap/analytics'
|
||||
import { BrowserEvent, InterfaceElementName, InterfaceEventName, InterfacePageName } from '@uniswap/analytics-events'
|
||||
import { useWeb3React } from '@web3-react/core'
|
||||
import { useToggleAccountDrawer } from 'components/AccountDrawer'
|
||||
import { ButtonGray, ButtonPrimary, ButtonText } from 'components/Button'
|
||||
import { AutoColumn } from 'components/Column'
|
||||
import { FlyoutAlignment, Menu } from 'components/Menu'
|
||||
import PositionList from 'components/PositionList'
|
||||
import { RowBetween, RowFixed } from 'components/Row'
|
||||
import { SwitchLocaleLink } from 'components/SwitchLocaleLink'
|
||||
import { useToggleWalletDrawer } from 'components/WalletDropdown'
|
||||
import { isSupportedChain } from 'constants/chains'
|
||||
import { useV3Positions } from 'hooks/useV3Positions'
|
||||
import { useMemo } from 'react'
|
||||
@@ -196,7 +196,7 @@ function WrongNetworkCard() {
|
||||
|
||||
export default function Pool() {
|
||||
const { account, chainId } = useWeb3React()
|
||||
const toggleWalletDrawer = useToggleWalletDrawer()
|
||||
const toggleWalletDrawer = useToggleAccountDrawer()
|
||||
|
||||
const theme = useTheme()
|
||||
const [userHideClosedPositions, setUserHideClosedPositions] = useUserHideClosedPositions()
|
||||
|
||||
@@ -6,8 +6,8 @@ import { TraceEvent } from '@uniswap/analytics'
|
||||
import { BrowserEvent, InterfaceElementName, InterfaceEventName } from '@uniswap/analytics-events'
|
||||
import { Currency, Percent } from '@uniswap/sdk-core'
|
||||
import { useWeb3React } from '@web3-react/core'
|
||||
import { useToggleAccountDrawer } from 'components/AccountDrawer'
|
||||
import { sendEvent } from 'components/analytics'
|
||||
import { useToggleWalletDrawer } from 'components/WalletDropdown'
|
||||
import { useV2LiquidityTokenPermit } from 'hooks/useV2LiquidityTokenPermit'
|
||||
import { useCallback, useMemo, useState } from 'react'
|
||||
import { ArrowDown, Plus } from 'react-feather'
|
||||
@@ -57,7 +57,7 @@ export default function RemoveLiquidity() {
|
||||
const theme = useTheme()
|
||||
|
||||
// toggle wallet when disconnected
|
||||
const toggleWalletDrawer = useToggleWalletDrawer()
|
||||
const toggleWalletDrawer = useToggleAccountDrawer()
|
||||
|
||||
// burn state
|
||||
const { independentField, typedValue } = useBurnState()
|
||||
|
||||
@@ -12,6 +12,7 @@ import { Trade } from '@uniswap/router-sdk'
|
||||
import { Currency, CurrencyAmount, Percent, Token, TradeType } from '@uniswap/sdk-core'
|
||||
import { UNIVERSAL_ROUTER_ADDRESS } from '@uniswap/universal-router-sdk'
|
||||
import { useWeb3React } from '@web3-react/core'
|
||||
import { useToggleAccountDrawer } from 'components/AccountDrawer'
|
||||
import { sendEvent } from 'components/analytics'
|
||||
import Loader from 'components/Icons/LoadingSpinner'
|
||||
import { NetworkAlert } from 'components/NetworkAlert/NetworkAlert'
|
||||
@@ -20,7 +21,6 @@ import SwapDetailsDropdown from 'components/swap/SwapDetailsDropdown'
|
||||
import UnsupportedCurrencyFooter from 'components/swap/UnsupportedCurrencyFooter'
|
||||
import TokenSafetyModal from 'components/TokenSafety/TokenSafetyModal'
|
||||
import { MouseoverTooltip } from 'components/Tooltip'
|
||||
import { useToggleWalletDrawer } from 'components/WalletDropdown'
|
||||
import Widget from 'components/Widget'
|
||||
import { isSupportedChain } from 'constants/chains'
|
||||
import { useSwapWidgetEnabled } from 'featureFlags/flags/swapWidget'
|
||||
@@ -127,7 +127,7 @@ const DetailsSwapSection = styled(SwapSection)`
|
||||
border-top-right-radius: 0;
|
||||
`
|
||||
|
||||
export function getIsValidSwapQuote(
|
||||
function getIsValidSwapQuote(
|
||||
trade: InterfaceTrade<Currency, Currency, TradeType> | undefined,
|
||||
tradeState: TradeState,
|
||||
swapInputError?: ReactNode
|
||||
@@ -194,7 +194,7 @@ export default function Swap({ className }: { className?: string }) {
|
||||
const theme = useTheme()
|
||||
|
||||
// toggle wallet when disconnected
|
||||
const toggleWalletDrawer = useToggleWalletDrawer()
|
||||
const toggleWalletDrawer = useToggleAccountDrawer()
|
||||
|
||||
// for expert mode
|
||||
const [isExpertMode] = useExpertModeManager()
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { getVersionUpgrade, minVersionBump, VersionUpgrade } from '@uniswap/token-lists'
|
||||
import { getVersionUpgrade, VersionUpgrade } from '@uniswap/token-lists'
|
||||
import { useWeb3React } from '@web3-react/core'
|
||||
import { DEFAULT_LIST_OF_LISTS, UNSUPPORTED_LIST_URLS } from 'constants/lists'
|
||||
import useInterval from 'lib/hooks/useInterval'
|
||||
@@ -10,6 +10,8 @@ import { useAllLists } from 'state/lists/hooks'
|
||||
import { useFetchListCallback } from '../../hooks/useFetchListCallback'
|
||||
import useIsWindowVisible from '../../hooks/useIsWindowVisible'
|
||||
import { acceptListUpdate } from './actions'
|
||||
import { shouldAcceptVersionUpdate } from './utils'
|
||||
|
||||
export default function Updater(): null {
|
||||
const { provider } = useWeb3React()
|
||||
const dispatch = useAppDispatch()
|
||||
@@ -64,14 +66,8 @@ export default function Updater(): null {
|
||||
throw new Error('unexpected no version bump')
|
||||
case VersionUpgrade.PATCH:
|
||||
case VersionUpgrade.MINOR: {
|
||||
const min = minVersionBump(list.current.tokens, list.pendingUpdate.tokens)
|
||||
// automatically update minor/patch as long as bump matches the min update
|
||||
if (bump >= min) {
|
||||
if (shouldAcceptVersionUpdate(listUrl, list.current, list.pendingUpdate, bump)) {
|
||||
dispatch(acceptListUpdate(listUrl))
|
||||
} else {
|
||||
console.error(
|
||||
`List at url ${listUrl} could not automatically update because the version bump was only PATCH/MINOR while the update had breaking changes and should have been MAJOR`
|
||||
)
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
64
src/state/lists/utils.test.ts
Normal file
64
src/state/lists/utils.test.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
import { TokenList, VersionUpgrade } from '@uniswap/token-lists'
|
||||
|
||||
import { shouldAcceptVersionUpdate } from './utils'
|
||||
|
||||
function buildTokenList(count: number): TokenList {
|
||||
const tokens = []
|
||||
for (let i = 0; i < count; i++) {
|
||||
tokens.push({
|
||||
name: `Token ${i}`,
|
||||
address: `0x${i.toString().padStart(40, '0')}`,
|
||||
symbol: `T${i}`,
|
||||
decimals: 18,
|
||||
chainId: 1,
|
||||
logoURI: `https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0x${i
|
||||
.toString()
|
||||
.padStart(40, '0')}/logo.png`,
|
||||
})
|
||||
}
|
||||
return {
|
||||
name: 'Defi',
|
||||
logoURI:
|
||||
'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0x514910771AF9Ca656af840dff83E8264EcF986CA/logo.png',
|
||||
keywords: ['defi', 'uniswap'],
|
||||
timestamp: '2021-03-12T00:00:00.000Z',
|
||||
version: {
|
||||
major: 1,
|
||||
minor: 0,
|
||||
patch: 0,
|
||||
},
|
||||
tokens,
|
||||
}
|
||||
}
|
||||
|
||||
describe('shouldAcceptMinorVersionUpdate', () => {
|
||||
it('returns false for patch when tokens have changed', () => {
|
||||
expect(shouldAcceptVersionUpdate('test_list', buildTokenList(1), buildTokenList(2), VersionUpgrade.PATCH)).toEqual(
|
||||
false
|
||||
)
|
||||
})
|
||||
|
||||
it('returns true for patch when tokens are the same', () => {
|
||||
expect(shouldAcceptVersionUpdate('test_list', buildTokenList(1), buildTokenList(1), VersionUpgrade.PATCH)).toEqual(
|
||||
true
|
||||
)
|
||||
})
|
||||
|
||||
it('returns true for minor version bump with tokens added', () => {
|
||||
expect(shouldAcceptVersionUpdate('test_list', buildTokenList(1), buildTokenList(2), VersionUpgrade.MINOR)).toEqual(
|
||||
true
|
||||
)
|
||||
})
|
||||
|
||||
it('returns true for no version bump', () => {
|
||||
expect(shouldAcceptVersionUpdate('test_list', buildTokenList(1), buildTokenList(2), VersionUpgrade.MINOR)).toEqual(
|
||||
true
|
||||
)
|
||||
})
|
||||
|
||||
it('returns false for minor version bump with tokens removed', () => {
|
||||
expect(shouldAcceptVersionUpdate('test_list', buildTokenList(2), buildTokenList(1), VersionUpgrade.MINOR)).toEqual(
|
||||
false
|
||||
)
|
||||
})
|
||||
})
|
||||
19
src/state/lists/utils.ts
Normal file
19
src/state/lists/utils.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { minVersionBump, TokenList, VersionUpgrade } from '@uniswap/token-lists'
|
||||
|
||||
export function shouldAcceptVersionUpdate(
|
||||
listUrl: string,
|
||||
current: TokenList,
|
||||
update: TokenList,
|
||||
targetBump: VersionUpgrade.PATCH | VersionUpgrade.MINOR
|
||||
): boolean {
|
||||
const min = minVersionBump(current.tokens, update.tokens)
|
||||
// Automatically update minor/patch as long as bump matches the min update.
|
||||
if (targetBump >= min) {
|
||||
return true
|
||||
} else {
|
||||
console.debug(
|
||||
`List at url ${listUrl} could not automatically update because the version bump was only PATCH/MINOR while the update had breaking changes and should have been MAJOR`
|
||||
)
|
||||
return false
|
||||
}
|
||||
}
|
||||
@@ -1,58 +1,20 @@
|
||||
import { sendAnalyticsEvent } from '@uniswap/analytics'
|
||||
import { SwapEventName } from '@uniswap/analytics-events'
|
||||
import { Currency, Percent, TradeType } from '@uniswap/sdk-core'
|
||||
import { useWeb3React } from '@web3-react/core'
|
||||
import { DEFAULT_TXN_DISMISS_MS, L2_TXN_DISMISS_MS } from 'constants/misc'
|
||||
import LibUpdater from 'lib/hooks/transactions/updater'
|
||||
import { formatPercentInBasisPointsNumber, formatToDecimal, getTokenAddress } from 'lib/utils/analytics'
|
||||
import { useCallback, useMemo } from 'react'
|
||||
import { useAppDispatch, useAppSelector } from 'state/hooks'
|
||||
import { InterfaceTrade } from 'state/routing/types'
|
||||
import { TransactionType } from 'state/transactions/types'
|
||||
import { computeRealizedPriceImpact } from 'utils/prices'
|
||||
|
||||
import { L2_CHAIN_IDS } from '../../constants/chains'
|
||||
import { useDerivedSwapInfo } from '../../state/swap/hooks'
|
||||
import { useAddPopup } from '../application/hooks'
|
||||
import { checkedTransaction, finalizeTransaction } from './reducer'
|
||||
import { SerializableTransactionReceipt } from './types'
|
||||
|
||||
interface AnalyticsEventProps {
|
||||
trade: InterfaceTrade<Currency, Currency, TradeType>
|
||||
hash: string | undefined
|
||||
allowedSlippage: Percent
|
||||
succeeded: boolean
|
||||
}
|
||||
|
||||
const formatAnalyticsEventProperties = ({ trade, hash, allowedSlippage, succeeded }: AnalyticsEventProps) => ({
|
||||
estimated_network_fee_usd: trade.gasUseEstimateUSD ? formatToDecimal(trade.gasUseEstimateUSD, 2) : undefined,
|
||||
transaction_hash: hash,
|
||||
token_in_address: getTokenAddress(trade.inputAmount.currency),
|
||||
token_out_address: getTokenAddress(trade.outputAmount.currency),
|
||||
token_in_symbol: trade.inputAmount.currency.symbol,
|
||||
token_out_symbol: trade.outputAmount.currency.symbol,
|
||||
token_in_amount: formatToDecimal(trade.inputAmount, trade.inputAmount.currency.decimals),
|
||||
token_out_amount: formatToDecimal(trade.outputAmount, trade.outputAmount.currency.decimals),
|
||||
price_impact_basis_points: formatPercentInBasisPointsNumber(computeRealizedPriceImpact(trade)),
|
||||
allowed_slippage_basis_points: formatPercentInBasisPointsNumber(allowedSlippage),
|
||||
chain_id:
|
||||
trade.inputAmount.currency.chainId === trade.outputAmount.currency.chainId
|
||||
? trade.inputAmount.currency.chainId
|
||||
: undefined,
|
||||
swap_quote_block_number: trade.blockNumber,
|
||||
succeeded,
|
||||
})
|
||||
|
||||
export default function Updater() {
|
||||
const { chainId } = useWeb3React()
|
||||
const addPopup = useAddPopup()
|
||||
// speed up popup dismisall time if on L2
|
||||
const isL2 = Boolean(chainId && L2_CHAIN_IDS.includes(chainId))
|
||||
const transactions = useAppSelector((state) => state.transactions)
|
||||
const {
|
||||
trade: { trade },
|
||||
allowedSlippage,
|
||||
} = useDerivedSwapInfo()
|
||||
|
||||
const dispatch = useAppDispatch()
|
||||
const onCheck = useCallback(
|
||||
@@ -79,19 +41,6 @@ export default function Updater() {
|
||||
})
|
||||
)
|
||||
|
||||
const tx = transactions[chainId]?.[hash]
|
||||
|
||||
if (tx.info.type === TransactionType.SWAP && trade) {
|
||||
sendAnalyticsEvent(
|
||||
SwapEventName.SWAP_TRANSACTION_COMPLETED,
|
||||
formatAnalyticsEventProperties({
|
||||
trade,
|
||||
hash,
|
||||
allowedSlippage,
|
||||
succeeded: receipt.status === 1,
|
||||
})
|
||||
)
|
||||
}
|
||||
addPopup(
|
||||
{
|
||||
txn: { hash },
|
||||
@@ -100,7 +49,7 @@ export default function Updater() {
|
||||
isL2 ? L2_TXN_DISMISS_MS : DEFAULT_TXN_DISMISS_MS
|
||||
)
|
||||
},
|
||||
[addPopup, allowedSlippage, dispatch, isL2, trade, transactions]
|
||||
[addPopup, dispatch, isL2]
|
||||
)
|
||||
|
||||
const pendingTransactions = useMemo(() => (chainId ? transactions[chainId] ?? {} : {}), [chainId, transactions])
|
||||
|
||||
21
src/state/user/hooks.test.tsx
Normal file
21
src/state/user/hooks.test.tsx
Normal file
@@ -0,0 +1,21 @@
|
||||
import { USDC_MAINNET } from 'constants/tokens'
|
||||
|
||||
import { deserializeToken, serializeToken } from './hooks'
|
||||
|
||||
describe('serializeToken', () => {
|
||||
it('serializes the token', () => {
|
||||
expect(serializeToken(USDC_MAINNET)).toEqual({
|
||||
chainId: 1,
|
||||
decimals: 6,
|
||||
name: 'USD//C',
|
||||
symbol: 'USDC',
|
||||
address: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48',
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('deserializeToken', () => {
|
||||
it('deserializes the token', () => {
|
||||
expect(deserializeToken(serializeToken(USDC_MAINNET))).toEqual(USDC_MAINNET)
|
||||
})
|
||||
})
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
addSerializedToken,
|
||||
updateHideClosedPositions,
|
||||
updateHideUniswapWalletBanner,
|
||||
updateUserBuyFiatFlowCompleted,
|
||||
updateUserClientSideRouter,
|
||||
updateUserDeadline,
|
||||
updateUserExpertMode,
|
||||
@@ -68,6 +69,18 @@ export function useIsExpertMode(): boolean {
|
||||
return useAppSelector((state) => state.user.userExpertMode)
|
||||
}
|
||||
|
||||
export function useBuyFiatFlowCompleted(): [boolean | undefined, (buyFiatFlowCompleted: boolean) => void] {
|
||||
const dispatch = useAppDispatch()
|
||||
const buyFiatFlowCompleted = useAppSelector((state) => state.user.buyFiatFlowCompleted)
|
||||
const setBuyFiatFlowCompleted = useCallback(
|
||||
(buyFiatFlowCompleted: boolean) => {
|
||||
dispatch(updateUserBuyFiatFlowCompleted(buyFiatFlowCompleted))
|
||||
},
|
||||
[dispatch]
|
||||
)
|
||||
return [buyFiatFlowCompleted, setBuyFiatFlowCompleted]
|
||||
}
|
||||
|
||||
export function useExpertModeManager(): [boolean, () => void] {
|
||||
const dispatch = useAppDispatch()
|
||||
const expertMode = useIsExpertMode()
|
||||
|
||||
@@ -2,7 +2,33 @@ import { createStore, Store } from 'redux'
|
||||
|
||||
import { DEFAULT_DEADLINE_FROM_NOW } from '../../constants/misc'
|
||||
import { updateVersion } from '../global/actions'
|
||||
import reducer, { initialState, UserState } from './reducer'
|
||||
import reducer, {
|
||||
addSerializedPair,
|
||||
addSerializedToken,
|
||||
initialState,
|
||||
updateHideClosedPositions,
|
||||
updateHideUniswapWalletBanner,
|
||||
updateSelectedWallet,
|
||||
updateUserClientSideRouter,
|
||||
updateUserDeadline,
|
||||
updateUserExpertMode,
|
||||
updateUserLocale,
|
||||
updateUserSlippageTolerance,
|
||||
UserState,
|
||||
} from './reducer'
|
||||
|
||||
function buildSerializedPair(token0Address: string, token1Address: string, chainId: number) {
|
||||
return {
|
||||
token0: {
|
||||
chainId,
|
||||
address: token0Address,
|
||||
},
|
||||
token1: {
|
||||
chainId,
|
||||
address: token1Address,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
describe('swap reducer', () => {
|
||||
let store: Store<UserState>
|
||||
@@ -30,5 +56,176 @@ describe('swap reducer', () => {
|
||||
expect(store.getState().userDeadline).toEqual(DEFAULT_DEADLINE_FROM_NOW)
|
||||
expect(store.getState().userSlippageTolerance).toEqual('auto')
|
||||
})
|
||||
it('sets allowed slippage and deadline to auto', () => {
|
||||
store = createStore(reducer, {
|
||||
...initialState,
|
||||
userSlippageTolerance: 10,
|
||||
userSlippageToleranceHasBeenMigratedToAuto: undefined,
|
||||
} as any)
|
||||
store.dispatch(updateVersion())
|
||||
expect(store.getState().userSlippageToleranceHasBeenMigratedToAuto).toEqual(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('updateSelectedWallet', () => {
|
||||
it('updates the selected wallet', () => {
|
||||
store.dispatch(updateSelectedWallet({ wallet: 'metamask' }))
|
||||
expect(store.getState().selectedWallet).toEqual('metamask')
|
||||
})
|
||||
})
|
||||
|
||||
describe('updateUserExpertMode', () => {
|
||||
it('updates the userExpertMode', () => {
|
||||
store.dispatch(updateUserExpertMode({ userExpertMode: true }))
|
||||
expect(store.getState().userExpertMode).toEqual(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('updateUserLocale', () => {
|
||||
it('updates the userLocale', () => {
|
||||
store.dispatch(updateUserLocale({ userLocale: 'en' }))
|
||||
expect(store.getState().userLocale).toEqual('en')
|
||||
})
|
||||
})
|
||||
|
||||
describe('updateUserSlippageTolerance', () => {
|
||||
it('updates the userSlippageTolerance', () => {
|
||||
store.dispatch(updateUserSlippageTolerance({ userSlippageTolerance: '0.5' }))
|
||||
expect(store.getState().userSlippageTolerance).toEqual('0.5')
|
||||
})
|
||||
})
|
||||
|
||||
describe('updateUserDeadline', () => {
|
||||
it('updates the userDeadline', () => {
|
||||
store.dispatch(updateUserDeadline({ userDeadline: 5 }))
|
||||
expect(store.getState().userDeadline).toEqual(5)
|
||||
})
|
||||
})
|
||||
|
||||
describe('updateUserClientSideRouter', () => {
|
||||
it('updates the userClientSideRouter', () => {
|
||||
store.dispatch(updateUserClientSideRouter({ userClientSideRouter: true }))
|
||||
expect(store.getState().userClientSideRouter).toEqual(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('updateHideClosedPositions', () => {
|
||||
it('updates the userHideClosedPositions', () => {
|
||||
store.dispatch(updateHideClosedPositions({ userHideClosedPositions: true }))
|
||||
expect(store.getState().userHideClosedPositions).toEqual(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('updateHideUniswapWalletBanner', () => {
|
||||
it('updates the hideUniswapWalletBanner', () => {
|
||||
store.dispatch(updateHideUniswapWalletBanner({ hideUniswapWalletBanner: true }))
|
||||
expect(store.getState().hideUniswapWalletBanner).toEqual(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('addSerializedToken', () => {
|
||||
it('adds a token to the uninitialized list', () => {
|
||||
store = createStore(reducer, {
|
||||
...initialState,
|
||||
tokens: undefined as any,
|
||||
})
|
||||
store.dispatch(
|
||||
addSerializedToken({
|
||||
serializedToken: {
|
||||
chainId: 1,
|
||||
address: '0x123',
|
||||
},
|
||||
})
|
||||
)
|
||||
expect(store.getState().tokens).toEqual({ 1: { '0x123': { address: '0x123', chainId: 1 } } })
|
||||
})
|
||||
it('adds a token to the initialized list, no duplicates', () => {
|
||||
store.dispatch(addSerializedToken({ serializedToken: { chainId: 1, address: '0x123' } }))
|
||||
store.dispatch(addSerializedToken({ serializedToken: { chainId: 1, address: '0x123' } }))
|
||||
expect(store.getState().tokens).toEqual({ 1: { '0x123': { address: '0x123', chainId: 1 } } })
|
||||
})
|
||||
|
||||
it('adds a new token to the initialized list', () => {
|
||||
store.dispatch(addSerializedToken({ serializedToken: { chainId: 1, address: '0x123' } }))
|
||||
store.dispatch(addSerializedToken({ serializedToken: { chainId: 1, address: '0x456' } }))
|
||||
expect(store.getState().tokens).toEqual({
|
||||
1: {
|
||||
'0x123': { address: '0x123', chainId: 1 },
|
||||
'0x456': { address: '0x456', chainId: 1 },
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('addSerializedPair', () => {
|
||||
it('adds a pair to the uninitialized list', () => {
|
||||
store = createStore(reducer, {
|
||||
...initialState,
|
||||
})
|
||||
store.dispatch(
|
||||
addSerializedPair({
|
||||
serializedPair: buildSerializedPair('0x123', '0x456', 1),
|
||||
})
|
||||
)
|
||||
expect(store.getState().pairs).toEqual({
|
||||
1: { '0x123;0x456': buildSerializedPair('0x123', '0x456', 1) },
|
||||
})
|
||||
})
|
||||
|
||||
it('adds two pair to the initialized list, no duplicates', () => {
|
||||
store.dispatch(
|
||||
addSerializedPair({
|
||||
serializedPair: buildSerializedPair('0x123', '0x456', 1),
|
||||
})
|
||||
)
|
||||
store.dispatch(
|
||||
addSerializedPair({
|
||||
serializedPair: buildSerializedPair('0x123', '0x456', 1),
|
||||
})
|
||||
)
|
||||
expect(store.getState().pairs).toEqual({
|
||||
1: { '0x123;0x456': buildSerializedPair('0x123', '0x456', 1) },
|
||||
})
|
||||
})
|
||||
|
||||
it('adds two new pairs to the initialized list, same chain', () => {
|
||||
store.dispatch(
|
||||
addSerializedPair({
|
||||
serializedPair: buildSerializedPair('0x123', '0x456', 1),
|
||||
})
|
||||
)
|
||||
store.dispatch(
|
||||
addSerializedPair({
|
||||
serializedPair: buildSerializedPair('0x123', '0x789', 1),
|
||||
})
|
||||
)
|
||||
expect(store.getState().pairs).toEqual({
|
||||
1: {
|
||||
'0x123;0x456': buildSerializedPair('0x123', '0x456', 1),
|
||||
'0x123;0x789': buildSerializedPair('0x123', '0x789', 1),
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('adds two new pairs to the initialized list, different chains', () => {
|
||||
store.dispatch(
|
||||
addSerializedPair({
|
||||
serializedPair: buildSerializedPair('0x123', '0x456', 1),
|
||||
})
|
||||
)
|
||||
store.dispatch(
|
||||
addSerializedPair({
|
||||
serializedPair: buildSerializedPair('0x123', '0x456', 5),
|
||||
})
|
||||
)
|
||||
expect(store.getState().pairs).toEqual({
|
||||
1: {
|
||||
'0x123;0x456': buildSerializedPair('0x123', '0x456', 1),
|
||||
},
|
||||
5: {
|
||||
'0x123;0x456': buildSerializedPair('0x123', '0x456', 5),
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -9,6 +9,8 @@ import { SerializedPair, SerializedToken } from './types'
|
||||
const currentTimestamp = () => new Date().getTime()
|
||||
|
||||
export interface UserState {
|
||||
buyFiatFlowCompleted: boolean | undefined
|
||||
|
||||
selectedWallet?: ConnectionType
|
||||
|
||||
// the timestamp of the last updateVersion action
|
||||
@@ -55,6 +57,7 @@ function pairKey(token0Address: string, token1Address: string) {
|
||||
}
|
||||
|
||||
export const initialState: UserState = {
|
||||
buyFiatFlowCompleted: undefined,
|
||||
selectedWallet: undefined,
|
||||
userExpertMode: false,
|
||||
userLocale: null,
|
||||
@@ -75,6 +78,9 @@ const userSlice = createSlice({
|
||||
name: 'user',
|
||||
initialState,
|
||||
reducers: {
|
||||
updateUserBuyFiatFlowCompleted(state, action) {
|
||||
state.buyFiatFlowCompleted = action.payload
|
||||
},
|
||||
updateSelectedWallet(state, { payload: { wallet } }) {
|
||||
state.selectedWallet = wallet
|
||||
},
|
||||
@@ -163,6 +169,7 @@ const userSlice = createSlice({
|
||||
export const {
|
||||
addSerializedPair,
|
||||
addSerializedToken,
|
||||
updateUserBuyFiatFlowCompleted,
|
||||
updateSelectedWallet,
|
||||
updateHideClosedPositions,
|
||||
updateUserClientSideRouter,
|
||||
|
||||
22
src/state/wallets/hooks.test.tsx
Normal file
22
src/state/wallets/hooks.test.tsx
Normal file
@@ -0,0 +1,22 @@
|
||||
import { act, renderHook } from 'test-utils'
|
||||
|
||||
import { useConnectedWallets } from './hooks'
|
||||
import { Wallet } from './types'
|
||||
|
||||
describe('useConnectedWallets', () => {
|
||||
it('should return the connected wallets', () => {
|
||||
const { result } = renderHook(() => useConnectedWallets())
|
||||
expect(result.current[0]).toEqual([])
|
||||
})
|
||||
it('should add a wallet', () => {
|
||||
const { result } = renderHook(() => useConnectedWallets())
|
||||
const wallet: Wallet = {
|
||||
walletType: 'injected',
|
||||
account: '0x123',
|
||||
}
|
||||
act(() => {
|
||||
result.current[1](wallet)
|
||||
})
|
||||
expect(result.current[0]).toEqual([wallet])
|
||||
})
|
||||
})
|
||||
40
src/state/wallets/reducer.test.ts
Normal file
40
src/state/wallets/reducer.test.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import walletsReducer from './reducer'
|
||||
import { Wallet } from './types'
|
||||
|
||||
describe('walletsSlice reducers', () => {
|
||||
it('should add a connected wallet', () => {
|
||||
const initialState = {
|
||||
connectedWallets: [],
|
||||
}
|
||||
const wallet = {
|
||||
address: '0x123',
|
||||
chainId: 1,
|
||||
}
|
||||
const action = {
|
||||
type: 'wallets/addConnectedWallet',
|
||||
payload: wallet,
|
||||
}
|
||||
const expectedState = {
|
||||
connectedWallets: [wallet],
|
||||
}
|
||||
expect(walletsReducer(initialState, action)).toEqual(expectedState)
|
||||
})
|
||||
|
||||
it('should remove a connected wallet', () => {
|
||||
const wallet: Wallet = {
|
||||
walletType: 'metamask',
|
||||
account: '0x123',
|
||||
}
|
||||
const initialState = {
|
||||
connectedWallets: [wallet],
|
||||
}
|
||||
const action = {
|
||||
type: 'wallets/removeConnectedWallet',
|
||||
payload: wallet,
|
||||
}
|
||||
const expectedState = {
|
||||
connectedWallets: [],
|
||||
}
|
||||
expect(walletsReducer(initialState, action)).toEqual(expectedState)
|
||||
})
|
||||
})
|
||||
22
src/tracing/errors.test.ts
Normal file
22
src/tracing/errors.test.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { ErrorEvent } from '@sentry/types'
|
||||
|
||||
import { filterKnownErrors } from './errors'
|
||||
|
||||
describe('filterKnownErrors', () => {
|
||||
const ERROR = {} as ErrorEvent
|
||||
it('propagates an error', () => {
|
||||
expect(filterKnownErrors(ERROR, {})).toBe(ERROR)
|
||||
})
|
||||
|
||||
it('filters block number polling errors', () => {
|
||||
const originalException = new (class extends Error {
|
||||
requestBody = JSON.stringify({ method: 'eth_blockNumber' })
|
||||
})()
|
||||
expect(filterKnownErrors(ERROR, { originalException })).toBe(null)
|
||||
})
|
||||
|
||||
it('filters network change errors', () => {
|
||||
const originalException = new Error('underlying network changed')
|
||||
expect(filterKnownErrors(ERROR, { originalException })).toBe(null)
|
||||
})
|
||||
})
|
||||
27
src/tracing/errors.ts
Normal file
27
src/tracing/errors.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { ClientOptions, ErrorEvent, EventHint } from '@sentry/types'
|
||||
|
||||
/** Identifies ethers request errors (as thrown by {@type import(@ethersproject/web).fetchJson}). */
|
||||
function isEthersRequestError(error: Error): error is Error & { requestBody: string } {
|
||||
return 'requestBody' in error && typeof (error as unknown as Record<'requestBody', unknown>).requestBody === 'string'
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters known (ignorable) errors out before sending them to Sentry.
|
||||
* Intended as a {@link ClientOptions.beforeSend} callback. Returning null filters the error from Sentry.
|
||||
*/
|
||||
export const filterKnownErrors: Required<ClientOptions>['beforeSend'] = (event: ErrorEvent, hint: EventHint) => {
|
||||
const error = hint.originalException
|
||||
if (error instanceof Error) {
|
||||
// ethers aggressively polls for block number, and it sometimes fails (whether spuriously or through rate-limiting).
|
||||
// If block number polling, it should not be considered an exception.
|
||||
if (isEthersRequestError(error)) {
|
||||
const method = JSON.parse(error.requestBody).method
|
||||
if (method === 'eth_blockNumber') return null
|
||||
}
|
||||
|
||||
// If the error is a network change, it should not be considered an exception.
|
||||
if (error.message.match(/underlying network changed/)) return null
|
||||
}
|
||||
|
||||
return event
|
||||
}
|
||||
@@ -7,6 +7,8 @@ import { SharedEventName } from '@uniswap/analytics-events'
|
||||
import { isSentryEnabled } from 'utils/env'
|
||||
import { getEnvName, isProductionEnv } from 'utils/env'
|
||||
|
||||
import { filterKnownErrors } from './errors'
|
||||
|
||||
export { trace } from './trace'
|
||||
|
||||
// Dump some metadata into the window to allow client verification.
|
||||
@@ -17,13 +19,10 @@ const AMPLITUDE_DUMMY_KEY = '00000000000000000000000000000000'
|
||||
export const STATSIG_DUMMY_KEY = 'client-0000000000000000000000000000000000000000000'
|
||||
|
||||
Sentry.init({
|
||||
// General configuration:
|
||||
dsn: process.env.REACT_APP_SENTRY_DSN,
|
||||
release: process.env.REACT_APP_GIT_COMMIT_HASH,
|
||||
environment: getEnvName(),
|
||||
// Exception reporting configuration:
|
||||
enabled: isSentryEnabled(),
|
||||
// Performance tracing configuration:
|
||||
tracesSampleRate: Number(process.env.REACT_APP_SENTRY_TRACES_SAMPLE_RATE ?? 0),
|
||||
integrations: [
|
||||
new BrowserTracing({
|
||||
@@ -31,6 +30,7 @@ Sentry.init({
|
||||
startTransactionOnPageLoad: true,
|
||||
}),
|
||||
],
|
||||
beforeSend: filterKnownErrors,
|
||||
})
|
||||
|
||||
initializeAnalytics(AMPLITUDE_DUMMY_KEY, OriginApplication.INTERFACE, {
|
||||
|
||||
@@ -122,6 +122,9 @@ describe('prices', () => {
|
||||
it('0 for undefined', () => {
|
||||
expect(warningSeverity(undefined)).toEqual(0)
|
||||
})
|
||||
it('0 for negative', () => {
|
||||
expect(warningSeverity(new Percent(-1))).toEqual(0)
|
||||
})
|
||||
it('correct for 0', () => {
|
||||
expect(warningSeverity(new Percent(0))).toEqual(0)
|
||||
})
|
||||
|
||||
@@ -85,6 +85,12 @@ const IMPACT_TIERS = [
|
||||
type WarningSeverity = 0 | 1 | 2 | 3 | 4
|
||||
export function warningSeverity(priceImpact: Percent | undefined): WarningSeverity {
|
||||
if (!priceImpact) return 0
|
||||
// This function is used to calculate the Severity level for % changes in USD value and Price Impact.
|
||||
// Price Impact is always an absolute value (conceptually always negative, but represented in code with a positive value)
|
||||
// The USD value change can be positive or negative, and it follows the same standard as Price Impact (positive value is the typical case of a loss due to slippage).
|
||||
// We don't want to return a warning level for a favorable/profitable change, so when the USD value change is negative we return 0.
|
||||
// TODO (WEB-3133): Disambiguate Price Impact and USD value change, and flip the sign of USD Value change.
|
||||
if (priceImpact.lessThan(0)) return 0
|
||||
let impact: WarningSeverity = IMPACT_TIERS.length as WarningSeverity
|
||||
for (const impactLevel of IMPACT_TIERS) {
|
||||
if (impactLevel.lessThan(priceImpact)) return impact
|
||||
|
||||
31
yarn.lock
31
yarn.lock
@@ -3566,6 +3566,11 @@
|
||||
"@testing-library/dom" "^8.5.0"
|
||||
"@types/react-dom" "^18.0.0"
|
||||
|
||||
"@testing-library/user-event@^14.4.3":
|
||||
version "14.4.3"
|
||||
resolved "https://registry.yarnpkg.com/@testing-library/user-event/-/user-event-14.4.3.tgz#af975e367743fa91989cd666666aec31a8f50591"
|
||||
integrity sha512-kCUc5MEwaEMakkO5x7aoD+DLi02ehmEM2QCGWvNqAS1dV/fAvORWEjnjsEIvml59M7Y5kCkWN6fCCyPOe8OL6Q==
|
||||
|
||||
"@tootallnate/once@1":
|
||||
version "1.1.2"
|
||||
resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-1.1.2.tgz#ccb91445360179a04e7fe6aff78c00ffc1eeaf82"
|
||||
@@ -4562,10 +4567,10 @@
|
||||
"@typescript-eslint/types" "5.47.0"
|
||||
eslint-visitor-keys "^3.3.0"
|
||||
|
||||
"@uniswap/analytics-events@^2.8.0":
|
||||
version "2.8.0"
|
||||
resolved "https://registry.yarnpkg.com/@uniswap/analytics-events/-/analytics-events-2.8.0.tgz#651eb08913b1a47c79814f0536b46cd91a6102d3"
|
||||
integrity sha512-unaNUxPYGoaPsPS+j6UuQRnxikha6Dr9Knv9jBVY/vIj03f8AOisVM7Zw9493QZP14lq2guARddkN3NzlttuwQ==
|
||||
"@uniswap/analytics-events@^2.9.0":
|
||||
version "2.9.0"
|
||||
resolved "https://registry.yarnpkg.com/@uniswap/analytics-events/-/analytics-events-2.9.0.tgz#80c634bfae850da33b446df0a9fb325869fa8ffa"
|
||||
integrity sha512-pgrr44L26/0MhRNKC7u8NwjIjVv5tKFqKre2h+TYpzRamZsOD37sR9mnovlF6FA91jNPRMPKK+kHouiimMfmrA==
|
||||
|
||||
"@uniswap/analytics@^1.3.1":
|
||||
version "1.3.1"
|
||||
@@ -9065,9 +9070,9 @@ domelementtype@1:
|
||||
integrity sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w==
|
||||
|
||||
domelementtype@^2.0.1, domelementtype@^2.2.0:
|
||||
version "2.2.0"
|
||||
resolved "https://registry.npmjs.org/domelementtype/-/domelementtype-2.2.0.tgz"
|
||||
integrity sha512-DtBMo82pv1dFtUmHyr48beiuq792Sxohr+8Hm9zoxklYPfa6n0Z3Byjj2IV7bmr2IyqClnqEQhfgHJJ5QF0R5A==
|
||||
version "2.3.0"
|
||||
resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.3.0.tgz#5c45e8e869952626331d7aab326d01daf65d589d"
|
||||
integrity sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==
|
||||
|
||||
domexception@^2.0.1:
|
||||
version "2.0.1"
|
||||
@@ -14135,9 +14140,9 @@ nth-check@^1.0.2:
|
||||
boolbase "~1.0.0"
|
||||
|
||||
nth-check@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-2.0.0.tgz#1bb4f6dac70072fc313e8c9cd1417b5074c0a125"
|
||||
integrity sha512-i4sc/Kj8htBrAiH1viZ0TgU8Y5XqCaV/FziYK6TBczxmeKm3AEFWqqF3195yKudrarqy7Zu80Ra5dobFjn9X/Q==
|
||||
version "2.1.1"
|
||||
resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-2.1.1.tgz#c9eab428effce36cd6b92c924bdb000ef1f1ed1d"
|
||||
integrity sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==
|
||||
dependencies:
|
||||
boolbase "^1.0.0"
|
||||
|
||||
@@ -14219,9 +14224,9 @@ object-copy@^0.1.0:
|
||||
kind-of "^3.0.3"
|
||||
|
||||
object-inspect@^1.12.2, object-inspect@^1.9.0:
|
||||
version "1.12.2"
|
||||
resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.12.2.tgz#c0641f26394532f28ab8d796ab954e43c009a8ea"
|
||||
integrity sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ==
|
||||
version "1.12.3"
|
||||
resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.12.3.tgz#ba62dffd67ee256c8c086dfae69e016cd1f198b9"
|
||||
integrity sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==
|
||||
|
||||
object-is@^1.0.1:
|
||||
version "1.1.5"
|
||||
|
||||
Reference in New Issue
Block a user